@gpc-cli/core 0.9.6 → 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
@@ -492,8 +492,13 @@ async function uploadRelease(client, packageName, filePath, options) {
492
492
  };
493
493
  }
494
494
  if (!validation.valid) {
495
- throw new Error(`File validation failed:
496
- ${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
+ );
497
502
  }
498
503
  const edit = await client.edits.insert(packageName);
499
504
  try {
@@ -559,10 +564,20 @@ async function promoteRelease(client, packageName, fromTrack, toTrack, options)
559
564
  (r) => r.status === "completed" || r.status === "inProgress"
560
565
  );
561
566
  if (!currentRelease) {
562
- 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
+ );
563
573
  }
564
574
  if (options?.userFraction && (options.userFraction <= 0 || options.userFraction > 1)) {
565
- 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
+ );
566
581
  }
567
582
  const release = {
568
583
  versionCodes: currentRelease.versionCodes,
@@ -593,15 +608,30 @@ async function updateRollout(client, packageName, track, action, userFraction) {
593
608
  (r) => r.status === "inProgress" || r.status === "halted"
594
609
  );
595
610
  if (!currentRelease) {
596
- 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
+ );
597
617
  }
598
618
  let newStatus;
599
619
  let newFraction;
600
620
  switch (action) {
601
621
  case "increase":
602
- 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
+ );
603
628
  if (userFraction <= 0 || userFraction > 1) {
604
- 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
+ );
605
635
  }
606
636
  newStatus = "inProgress";
607
637
  newFraction = userFraction;
@@ -898,7 +928,12 @@ function diffListings(local, remote) {
898
928
  // src/commands/listings.ts
899
929
  function validateLanguage(lang) {
900
930
  if (!isValidBcp47(lang)) {
901
- 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
+ );
902
937
  }
903
938
  }
904
939
  async function getListings(client, packageName, language) {
@@ -963,7 +998,12 @@ async function pullListings(client, packageName, dir) {
963
998
  async function pushListings(client, packageName, dir, options) {
964
999
  const localListings = await readListingsFromDir(dir);
965
1000
  if (localListings.length === 0) {
966
- 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
+ );
967
1007
  }
968
1008
  for (const listing of localListings) {
969
1009
  validateLanguage(listing.language);
@@ -1009,7 +1049,12 @@ async function uploadImage(client, packageName, language, imageType, filePath) {
1009
1049
  validateLanguage(language);
1010
1050
  const imageCheck = await validateImage(filePath, imageType);
1011
1051
  if (!imageCheck.valid) {
1012
- 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
+ );
1013
1058
  }
1014
1059
  if (imageCheck.warnings.length > 0) {
1015
1060
  for (const w of imageCheck.warnings) {
@@ -1041,6 +1086,19 @@ async function deleteImage(client, packageName, language, imageType, imageId) {
1041
1086
  throw error;
1042
1087
  }
1043
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
+ }
1044
1102
  async function getCountryAvailability(client, packageName, track) {
1045
1103
  const edit = await client.edits.insert(packageName);
1046
1104
  try {
@@ -1076,7 +1134,12 @@ async function readReleaseNotesFromDir(dir) {
1076
1134
  try {
1077
1135
  entries = await readdir2(dir);
1078
1136
  } catch {
1079
- 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
+ );
1080
1143
  }
1081
1144
  const notes = [];
1082
1145
  for (const entry of entries) {
@@ -1273,12 +1336,20 @@ async function getReview(client, packageName, reviewId, translationLanguage) {
1273
1336
  var MAX_REPLY_LENGTH = 350;
1274
1337
  async function replyToReview(client, packageName, reviewId, replyText) {
1275
1338
  if (replyText.length > MAX_REPLY_LENGTH) {
1276
- throw new Error(
1277
- `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}.`
1278
1344
  );
1279
1345
  }
1280
1346
  if (replyText.length === 0) {
1281
- 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
+ );
1282
1353
  }
1283
1354
  return client.reviews.reply(packageName, reviewId, replyText);
1284
1355
  }
@@ -1611,7 +1682,12 @@ async function syncInAppProducts(client, packageName, dir, options) {
1611
1682
  try {
1612
1683
  localProducts.push(JSON.parse(content));
1613
1684
  } catch {
1614
- 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
+ );
1615
1691
  }
1616
1692
  }
1617
1693
  const response = await client.inappproducts.list(packageName);
@@ -1745,12 +1821,22 @@ function isValidStatsDimension(dim) {
1745
1821
  function parseMonth(monthStr) {
1746
1822
  const match = /^(\d{4})-(\d{2})$/.exec(monthStr);
1747
1823
  if (!match) {
1748
- 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
+ );
1749
1830
  }
1750
1831
  const year = Number(match[1]);
1751
1832
  const month = Number(match[2]);
1752
1833
  if (month < 1 || month > 12) {
1753
- 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
+ );
1754
1840
  }
1755
1841
  return { year, month };
1756
1842
  }
@@ -1760,21 +1846,31 @@ async function listReports(client, packageName, reportType, year, month) {
1760
1846
  }
1761
1847
  async function downloadReport(client, packageName, reportType, year, month) {
1762
1848
  const reports = await listReports(client, packageName, reportType, year, month);
1849
+ const monthPadded = String(month).padStart(2, "0");
1763
1850
  if (reports.length === 0) {
1764
- throw new Error(
1765
- `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.`
1766
1856
  );
1767
1857
  }
1768
1858
  const bucket = reports[0];
1769
1859
  if (!bucket) {
1770
- throw new Error(
1771
- `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.`
1772
1865
  );
1773
1866
  }
1774
1867
  const uri = bucket.uri;
1775
1868
  const response = await fetch(uri);
1776
1869
  if (!response.ok) {
1777
- 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
+ );
1778
1874
  }
1779
1875
  return response.text();
1780
1876
  }
@@ -1825,8 +1921,11 @@ async function removeUser(client, developerId, userId) {
1825
1921
  function parseGrantArg(grantStr) {
1826
1922
  const colonIdx = grantStr.indexOf(":");
1827
1923
  if (colonIdx === -1) {
1828
- throw new Error(
1829
- `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"
1830
1929
  );
1831
1930
  }
1832
1931
  const packageName = grantStr.slice(0, colonIdx);
@@ -1887,12 +1986,189 @@ async function importTestersFromCsv(client, packageName, track, csvPath) {
1887
1986
  const content = await readFile5(csvPath, "utf-8");
1888
1987
  const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
1889
1988
  if (emails.length === 0) {
1890
- 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
+ );
1891
1995
  }
1892
1996
  const testers = await addTesters(client, packageName, track, emails);
1893
1997
  return { added: emails.length, testers };
1894
1998
  }
1895
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
+
1896
2172
  // src/utils/safe-path.ts
1897
2173
  import { resolve, normalize } from "path";
1898
2174
  function safePath(userPath) {
@@ -1902,13 +2178,58 @@ function safePathWithin(userPath, baseDir) {
1902
2178
  const resolved = safePath(userPath);
1903
2179
  const base = safePath(baseDir);
1904
2180
  if (!resolved.startsWith(base + "/") && resolved !== base) {
1905
- 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
+ );
1906
2187
  }
1907
2188
  return resolved;
1908
2189
  }
1909
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
+
1910
2231
  // src/commands/plugin-scaffold.ts
1911
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2232
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
1912
2233
  import { join as join4 } from "path";
1913
2234
  async function scaffoldPlugin(options) {
1914
2235
  const { name, dir, description = `GPC plugin: ${name}` } = options;
@@ -1951,7 +2272,7 @@ async function scaffoldPlugin(options) {
1951
2272
  vitest: "^3.0.0"
1952
2273
  }
1953
2274
  };
1954
- 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");
1955
2276
  files.push("package.json");
1956
2277
  const tsconfig = {
1957
2278
  compilerOptions: {
@@ -1967,7 +2288,7 @@ async function scaffoldPlugin(options) {
1967
2288
  },
1968
2289
  include: ["src"]
1969
2290
  };
1970
- 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");
1971
2292
  files.push("tsconfig.json");
1972
2293
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
1973
2294
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -2000,7 +2321,7 @@ export const plugin = definePlugin({
2000
2321
  },
2001
2322
  });
2002
2323
  `;
2003
- await writeFile2(join4(srcDir, "index.ts"), srcContent);
2324
+ await writeFile3(join4(srcDir, "index.ts"), srcContent);
2004
2325
  files.push("src/index.ts");
2005
2326
  const testContent = `import { describe, it, expect, vi } from "vitest";
2006
2327
  import { plugin } from "../src/index";
@@ -2025,7 +2346,7 @@ describe("${pluginName}", () => {
2025
2346
  });
2026
2347
  });
2027
2348
  `;
2028
- await writeFile2(join4(testDir, "plugin.test.ts"), testContent);
2349
+ await writeFile3(join4(testDir, "plugin.test.ts"), testContent);
2029
2350
  files.push("tests/plugin.test.ts");
2030
2351
  return { dir, files };
2031
2352
  }
@@ -2078,6 +2399,111 @@ function createAuditEntry(command, args, app) {
2078
2399
  args
2079
2400
  };
2080
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
+ }
2081
2507
  export {
2082
2508
  ApiError,
2083
2509
  ConfigError,
@@ -2090,12 +2516,14 @@ export {
2090
2516
  activateBasePlan,
2091
2517
  activateOffer,
2092
2518
  addTesters,
2519
+ cancelRecoveryAction,
2093
2520
  cancelSubscriptionPurchase,
2094
2521
  checkThreshold,
2095
2522
  compareVitalsTrend,
2096
2523
  consumeProductPurchase,
2097
2524
  convertRegionPrices,
2098
2525
  createAuditEntry,
2526
+ createExternalTransaction,
2099
2527
  createInAppProduct,
2100
2528
  createOffer,
2101
2529
  createSubscription,
@@ -2108,14 +2536,23 @@ export {
2108
2536
  deleteListing,
2109
2537
  deleteOffer,
2110
2538
  deleteSubscription,
2539
+ deployRecoveryAction,
2111
2540
  detectOutputFormat,
2112
2541
  diffListings,
2542
+ diffListingsCommand,
2113
2543
  discoverPlugins,
2114
2544
  downloadReport,
2545
+ exportDataSafety,
2115
2546
  exportReviews,
2547
+ formatCustomPayload,
2548
+ formatDiscordPayload,
2116
2549
  formatOutput,
2550
+ formatSlackPayload,
2551
+ generateNotesFromGit,
2117
2552
  getAppInfo,
2118
2553
  getCountryAvailability,
2554
+ getDataSafety,
2555
+ getExternalTransaction,
2119
2556
  getInAppProduct,
2120
2557
  getListings,
2121
2558
  getOffer,
@@ -2133,6 +2570,7 @@ export {
2133
2570
  getVitalsOverview,
2134
2571
  getVitalsRendering,
2135
2572
  getVitalsStartup,
2573
+ importDataSafety,
2136
2574
  importTestersFromCsv,
2137
2575
  initAudit,
2138
2576
  inviteUser,
@@ -2144,6 +2582,7 @@ export {
2144
2582
  listImages,
2145
2583
  listInAppProducts,
2146
2584
  listOffers,
2585
+ listRecoveryActions,
2147
2586
  listReports,
2148
2587
  listReviews,
2149
2588
  listSubscriptions,
@@ -2161,6 +2600,7 @@ export {
2161
2600
  readListingsFromDir,
2162
2601
  readReleaseNotesFromDir,
2163
2602
  redactSensitive,
2603
+ refundExternalTransaction,
2164
2604
  refundOrder,
2165
2605
  removeTesters,
2166
2606
  removeUser,
@@ -2170,8 +2610,11 @@ export {
2170
2610
  safePathWithin,
2171
2611
  scaffoldPlugin,
2172
2612
  searchVitalsErrors,
2613
+ sendWebhook,
2614
+ sortResults,
2173
2615
  syncInAppProducts,
2174
2616
  updateAppDetails,
2617
+ updateDataSafety,
2175
2618
  updateInAppProduct,
2176
2619
  updateListing,
2177
2620
  updateOffer,