@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.d.ts +60 -3
- package/dist/index.js +474 -31
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
|
496
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1765
|
-
`No ${reportType} reports found for ${year}-${
|
|
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
|
|
1771
|
-
`No ${reportType} reports found for ${year}-${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|