@gpc-cli/core 0.9.6 → 0.9.8
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 +91 -3
- package/dist/index.js +746 -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,368 @@ 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
|
+
async function createRecoveryAction(client, packageName, request) {
|
|
2118
|
+
return client.appRecovery.create(packageName, request);
|
|
2119
|
+
}
|
|
2120
|
+
async function addRecoveryTargeting(client, packageName, actionId, targeting) {
|
|
2121
|
+
return client.appRecovery.addTargeting(packageName, actionId, targeting);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/commands/data-safety.ts
|
|
2125
|
+
import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
2126
|
+
async function getDataSafety(client, packageName) {
|
|
2127
|
+
const edit = await client.edits.insert(packageName);
|
|
2128
|
+
try {
|
|
2129
|
+
const dataSafety = await client.dataSafety.get(packageName, edit.id);
|
|
2130
|
+
await client.edits.delete(packageName, edit.id);
|
|
2131
|
+
return dataSafety;
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
2134
|
+
});
|
|
2135
|
+
throw error;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
async function updateDataSafety(client, packageName, data) {
|
|
2139
|
+
const edit = await client.edits.insert(packageName);
|
|
2140
|
+
try {
|
|
2141
|
+
const result = await client.dataSafety.update(packageName, edit.id, data);
|
|
2142
|
+
await client.edits.validate(packageName, edit.id);
|
|
2143
|
+
await client.edits.commit(packageName, edit.id);
|
|
2144
|
+
return result;
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
2147
|
+
});
|
|
2148
|
+
throw error;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async function exportDataSafety(client, packageName, outputPath) {
|
|
2152
|
+
const dataSafety = await getDataSafety(client, packageName);
|
|
2153
|
+
await writeFile2(outputPath, JSON.stringify(dataSafety, null, 2) + "\n", "utf-8");
|
|
2154
|
+
return dataSafety;
|
|
2155
|
+
}
|
|
2156
|
+
async function importDataSafety(client, packageName, filePath) {
|
|
2157
|
+
const content = await readFile6(filePath, "utf-8");
|
|
2158
|
+
let data;
|
|
2159
|
+
try {
|
|
2160
|
+
data = JSON.parse(content);
|
|
2161
|
+
} catch {
|
|
2162
|
+
throw new Error(`Failed to parse data safety JSON from "${filePath}"`);
|
|
2163
|
+
}
|
|
2164
|
+
return updateDataSafety(client, packageName, data);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// src/commands/external-transactions.ts
|
|
2168
|
+
async function createExternalTransaction(client, packageName, data) {
|
|
2169
|
+
return client.externalTransactions.create(packageName, data);
|
|
2170
|
+
}
|
|
2171
|
+
async function getExternalTransaction(client, packageName, transactionId) {
|
|
2172
|
+
return client.externalTransactions.get(packageName, transactionId);
|
|
2173
|
+
}
|
|
2174
|
+
async function refundExternalTransaction(client, packageName, transactionId, refundData) {
|
|
2175
|
+
return client.externalTransactions.refund(packageName, transactionId, refundData);
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/commands/device-tiers.ts
|
|
2179
|
+
async function listDeviceTiers(client, packageName) {
|
|
2180
|
+
if (!packageName) {
|
|
2181
|
+
throw new GpcError(
|
|
2182
|
+
"Package name is required",
|
|
2183
|
+
"MISSING_PACKAGE_NAME",
|
|
2184
|
+
2,
|
|
2185
|
+
"Provide a package name with --app or set it in config."
|
|
2186
|
+
);
|
|
2187
|
+
}
|
|
2188
|
+
return client.deviceTiers.list(packageName);
|
|
2189
|
+
}
|
|
2190
|
+
async function getDeviceTier(client, packageName, configId) {
|
|
2191
|
+
if (!packageName) {
|
|
2192
|
+
throw new GpcError(
|
|
2193
|
+
"Package name is required",
|
|
2194
|
+
"MISSING_PACKAGE_NAME",
|
|
2195
|
+
2,
|
|
2196
|
+
"Provide a package name with --app or set it in config."
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
if (!configId) {
|
|
2200
|
+
throw new GpcError(
|
|
2201
|
+
"Config ID is required",
|
|
2202
|
+
"MISSING_CONFIG_ID",
|
|
2203
|
+
2,
|
|
2204
|
+
"Provide a device tier config ID."
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
return client.deviceTiers.get(packageName, configId);
|
|
2208
|
+
}
|
|
2209
|
+
async function createDeviceTier(client, packageName, config) {
|
|
2210
|
+
if (!packageName) {
|
|
2211
|
+
throw new GpcError(
|
|
2212
|
+
"Package name is required",
|
|
2213
|
+
"MISSING_PACKAGE_NAME",
|
|
2214
|
+
2,
|
|
2215
|
+
"Provide a package name with --app or set it in config."
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
if (!config || !config.deviceGroups || config.deviceGroups.length === 0) {
|
|
2219
|
+
throw new GpcError(
|
|
2220
|
+
"Device tier config must include at least one device group",
|
|
2221
|
+
"INVALID_DEVICE_TIER_CONFIG",
|
|
2222
|
+
2,
|
|
2223
|
+
"Provide a valid config with deviceGroups."
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
return client.deviceTiers.create(packageName, config);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// src/commands/one-time-products.ts
|
|
2230
|
+
async function listOneTimeProducts(client, packageName) {
|
|
2231
|
+
try {
|
|
2232
|
+
return await client.oneTimeProducts.list(packageName);
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
throw new GpcError(
|
|
2235
|
+
`Failed to list one-time products: ${error instanceof Error ? error.message : String(error)}`,
|
|
2236
|
+
"OTP_LIST_FAILED",
|
|
2237
|
+
4,
|
|
2238
|
+
"Check your package name and API credentials."
|
|
2239
|
+
);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async function getOneTimeProduct(client, packageName, productId) {
|
|
2243
|
+
try {
|
|
2244
|
+
return await client.oneTimeProducts.get(packageName, productId);
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
throw new GpcError(
|
|
2247
|
+
`Failed to get one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2248
|
+
"OTP_GET_FAILED",
|
|
2249
|
+
4,
|
|
2250
|
+
"Check that the product ID exists."
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
async function createOneTimeProduct(client, packageName, data) {
|
|
2255
|
+
try {
|
|
2256
|
+
return await client.oneTimeProducts.create(packageName, data);
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
throw new GpcError(
|
|
2259
|
+
`Failed to create one-time product: ${error instanceof Error ? error.message : String(error)}`,
|
|
2260
|
+
"OTP_CREATE_FAILED",
|
|
2261
|
+
4,
|
|
2262
|
+
"Verify the product data and ensure the product ID is unique."
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
async function updateOneTimeProduct(client, packageName, productId, data) {
|
|
2267
|
+
try {
|
|
2268
|
+
return await client.oneTimeProducts.update(packageName, productId, data);
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
throw new GpcError(
|
|
2271
|
+
`Failed to update one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2272
|
+
"OTP_UPDATE_FAILED",
|
|
2273
|
+
4,
|
|
2274
|
+
"Check that the product ID exists and the data is valid."
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
async function deleteOneTimeProduct(client, packageName, productId) {
|
|
2279
|
+
try {
|
|
2280
|
+
await client.oneTimeProducts.delete(packageName, productId);
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
throw new GpcError(
|
|
2283
|
+
`Failed to delete one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2284
|
+
"OTP_DELETE_FAILED",
|
|
2285
|
+
4,
|
|
2286
|
+
"Check that the product ID exists and is not active."
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
async function listOneTimeOffers(client, packageName, productId) {
|
|
2291
|
+
try {
|
|
2292
|
+
return await client.oneTimeProducts.listOffers(packageName, productId);
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
throw new GpcError(
|
|
2295
|
+
`Failed to list offers for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2296
|
+
"OTP_OFFERS_LIST_FAILED",
|
|
2297
|
+
4,
|
|
2298
|
+
"Check the product ID and your API credentials."
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
async function getOneTimeOffer(client, packageName, productId, offerId) {
|
|
2303
|
+
try {
|
|
2304
|
+
return await client.oneTimeProducts.getOffer(packageName, productId, offerId);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
throw new GpcError(
|
|
2307
|
+
`Failed to get offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2308
|
+
"OTP_OFFER_GET_FAILED",
|
|
2309
|
+
4,
|
|
2310
|
+
"Check that the product and offer IDs exist."
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
async function createOneTimeOffer(client, packageName, productId, data) {
|
|
2315
|
+
try {
|
|
2316
|
+
return await client.oneTimeProducts.createOffer(packageName, productId, data);
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
throw new GpcError(
|
|
2319
|
+
`Failed to create offer for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2320
|
+
"OTP_OFFER_CREATE_FAILED",
|
|
2321
|
+
4,
|
|
2322
|
+
"Verify the offer data and ensure the offer ID is unique."
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
async function updateOneTimeOffer(client, packageName, productId, offerId, data) {
|
|
2327
|
+
try {
|
|
2328
|
+
return await client.oneTimeProducts.updateOffer(packageName, productId, offerId, data);
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
throw new GpcError(
|
|
2331
|
+
`Failed to update offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2332
|
+
"OTP_OFFER_UPDATE_FAILED",
|
|
2333
|
+
4,
|
|
2334
|
+
"Check that the product and offer IDs exist and the data is valid."
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
async function deleteOneTimeOffer(client, packageName, productId, offerId) {
|
|
2339
|
+
try {
|
|
2340
|
+
await client.oneTimeProducts.deleteOffer(packageName, productId, offerId);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
throw new GpcError(
|
|
2343
|
+
`Failed to delete offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
|
|
2344
|
+
"OTP_OFFER_DELETE_FAILED",
|
|
2345
|
+
4,
|
|
2346
|
+
"Check that the product and offer IDs exist."
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
1896
2351
|
// src/utils/safe-path.ts
|
|
1897
2352
|
import { resolve, normalize } from "path";
|
|
1898
2353
|
function safePath(userPath) {
|
|
@@ -1902,13 +2357,58 @@ function safePathWithin(userPath, baseDir) {
|
|
|
1902
2357
|
const resolved = safePath(userPath);
|
|
1903
2358
|
const base = safePath(baseDir);
|
|
1904
2359
|
if (!resolved.startsWith(base + "/") && resolved !== base) {
|
|
1905
|
-
throw new
|
|
2360
|
+
throw new GpcError(
|
|
2361
|
+
`Path "${userPath}" resolves outside the expected directory "${baseDir}"`,
|
|
2362
|
+
"PATH_TRAVERSAL",
|
|
2363
|
+
2,
|
|
2364
|
+
"The path must stay within the target directory. Remove any ../ segments or use an absolute path within the expected directory."
|
|
2365
|
+
);
|
|
1906
2366
|
}
|
|
1907
2367
|
return resolved;
|
|
1908
2368
|
}
|
|
1909
2369
|
|
|
2370
|
+
// src/utils/sort.ts
|
|
2371
|
+
function getNestedValue(obj, path) {
|
|
2372
|
+
const parts = path.split(".");
|
|
2373
|
+
let current = obj;
|
|
2374
|
+
for (const part of parts) {
|
|
2375
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
2376
|
+
return void 0;
|
|
2377
|
+
}
|
|
2378
|
+
current = current[part];
|
|
2379
|
+
}
|
|
2380
|
+
return current;
|
|
2381
|
+
}
|
|
2382
|
+
function compare(a, b) {
|
|
2383
|
+
if (a === void 0 && b === void 0) return 0;
|
|
2384
|
+
if (a === void 0) return 1;
|
|
2385
|
+
if (b === void 0) return -1;
|
|
2386
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
2387
|
+
return a - b;
|
|
2388
|
+
}
|
|
2389
|
+
return String(a).localeCompare(String(b));
|
|
2390
|
+
}
|
|
2391
|
+
function sortResults(items, sortSpec) {
|
|
2392
|
+
if (!sortSpec || items.length === 0) {
|
|
2393
|
+
return items;
|
|
2394
|
+
}
|
|
2395
|
+
const descending = sortSpec.startsWith("-");
|
|
2396
|
+
const field = descending ? sortSpec.slice(1) : sortSpec;
|
|
2397
|
+
const hasField = items.some((item) => getNestedValue(item, field) !== void 0);
|
|
2398
|
+
if (!hasField) {
|
|
2399
|
+
return items;
|
|
2400
|
+
}
|
|
2401
|
+
const sorted = [...items].sort((a, b) => {
|
|
2402
|
+
const aVal = getNestedValue(a, field);
|
|
2403
|
+
const bVal = getNestedValue(b, field);
|
|
2404
|
+
const result = compare(aVal, bVal);
|
|
2405
|
+
return descending ? -result : result;
|
|
2406
|
+
});
|
|
2407
|
+
return sorted;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
1910
2410
|
// src/commands/plugin-scaffold.ts
|
|
1911
|
-
import { mkdir as mkdir2, writeFile as
|
|
2411
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
1912
2412
|
import { join as join4 } from "path";
|
|
1913
2413
|
async function scaffoldPlugin(options) {
|
|
1914
2414
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
@@ -1951,7 +2451,7 @@ async function scaffoldPlugin(options) {
|
|
|
1951
2451
|
vitest: "^3.0.0"
|
|
1952
2452
|
}
|
|
1953
2453
|
};
|
|
1954
|
-
await
|
|
2454
|
+
await writeFile3(join4(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
1955
2455
|
files.push("package.json");
|
|
1956
2456
|
const tsconfig = {
|
|
1957
2457
|
compilerOptions: {
|
|
@@ -1967,7 +2467,7 @@ async function scaffoldPlugin(options) {
|
|
|
1967
2467
|
},
|
|
1968
2468
|
include: ["src"]
|
|
1969
2469
|
};
|
|
1970
|
-
await
|
|
2470
|
+
await writeFile3(join4(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
1971
2471
|
files.push("tsconfig.json");
|
|
1972
2472
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
1973
2473
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -2000,7 +2500,7 @@ export const plugin = definePlugin({
|
|
|
2000
2500
|
},
|
|
2001
2501
|
});
|
|
2002
2502
|
`;
|
|
2003
|
-
await
|
|
2503
|
+
await writeFile3(join4(srcDir, "index.ts"), srcContent);
|
|
2004
2504
|
files.push("src/index.ts");
|
|
2005
2505
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
2006
2506
|
import { plugin } from "../src/index";
|
|
@@ -2025,7 +2525,7 @@ describe("${pluginName}", () => {
|
|
|
2025
2525
|
});
|
|
2026
2526
|
});
|
|
2027
2527
|
`;
|
|
2028
|
-
await
|
|
2528
|
+
await writeFile3(join4(testDir, "plugin.test.ts"), testContent);
|
|
2029
2529
|
files.push("tests/plugin.test.ts");
|
|
2030
2530
|
return { dir, files };
|
|
2031
2531
|
}
|
|
@@ -2078,6 +2578,186 @@ function createAuditEntry(command, args, app) {
|
|
|
2078
2578
|
args
|
|
2079
2579
|
};
|
|
2080
2580
|
}
|
|
2581
|
+
|
|
2582
|
+
// src/utils/webhooks.ts
|
|
2583
|
+
function formatSlackPayload(payload) {
|
|
2584
|
+
const status = payload.success ? "\u2713" : "\u2717";
|
|
2585
|
+
const color = payload.success ? "#2eb886" : "#e01e5a";
|
|
2586
|
+
const durationSec = (payload.duration / 1e3).toFixed(1);
|
|
2587
|
+
const fields = [
|
|
2588
|
+
{ title: "Command", value: payload.command, short: true },
|
|
2589
|
+
{ title: "Duration", value: `${durationSec}s`, short: true }
|
|
2590
|
+
];
|
|
2591
|
+
if (payload.app) {
|
|
2592
|
+
fields.push({ title: "App", value: payload.app, short: true });
|
|
2593
|
+
}
|
|
2594
|
+
if (payload.error) {
|
|
2595
|
+
fields.push({ title: "Error", value: payload.error, short: false });
|
|
2596
|
+
}
|
|
2597
|
+
if (payload.details) {
|
|
2598
|
+
for (const [key, value] of Object.entries(payload.details)) {
|
|
2599
|
+
fields.push({ title: key, value: String(value), short: true });
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return {
|
|
2603
|
+
attachments: [
|
|
2604
|
+
{
|
|
2605
|
+
color,
|
|
2606
|
+
fallback: `GPC: ${payload.command} ${status}`,
|
|
2607
|
+
title: `GPC: ${payload.command} ${status}`,
|
|
2608
|
+
fields,
|
|
2609
|
+
footer: "GPC CLI",
|
|
2610
|
+
ts: Math.floor(Date.now() / 1e3)
|
|
2611
|
+
}
|
|
2612
|
+
]
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
function formatDiscordPayload(payload) {
|
|
2616
|
+
const status = payload.success ? "\u2713" : "\u2717";
|
|
2617
|
+
const color = payload.success ? 3061894 : 14687834;
|
|
2618
|
+
const durationSec = (payload.duration / 1e3).toFixed(1);
|
|
2619
|
+
const fields = [
|
|
2620
|
+
{ name: "Command", value: payload.command, inline: true },
|
|
2621
|
+
{ name: "Duration", value: `${durationSec}s`, inline: true }
|
|
2622
|
+
];
|
|
2623
|
+
if (payload.app) {
|
|
2624
|
+
fields.push({ name: "App", value: payload.app, inline: true });
|
|
2625
|
+
}
|
|
2626
|
+
if (payload.error) {
|
|
2627
|
+
fields.push({ name: "Error", value: payload.error, inline: false });
|
|
2628
|
+
}
|
|
2629
|
+
if (payload.details) {
|
|
2630
|
+
for (const [key, value] of Object.entries(payload.details)) {
|
|
2631
|
+
fields.push({ name: key, value: String(value), inline: true });
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return {
|
|
2635
|
+
embeds: [
|
|
2636
|
+
{
|
|
2637
|
+
title: `GPC: ${payload.command} ${status}`,
|
|
2638
|
+
color,
|
|
2639
|
+
fields,
|
|
2640
|
+
footer: { text: "GPC CLI" },
|
|
2641
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2642
|
+
}
|
|
2643
|
+
]
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
function formatCustomPayload(payload) {
|
|
2647
|
+
return { ...payload };
|
|
2648
|
+
}
|
|
2649
|
+
var FORMATTERS = {
|
|
2650
|
+
slack: formatSlackPayload,
|
|
2651
|
+
discord: formatDiscordPayload,
|
|
2652
|
+
custom: formatCustomPayload
|
|
2653
|
+
};
|
|
2654
|
+
var WEBHOOK_TIMEOUT_MS = 5e3;
|
|
2655
|
+
async function sendSingle(url, body) {
|
|
2656
|
+
const controller = new AbortController();
|
|
2657
|
+
const timer = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);
|
|
2658
|
+
try {
|
|
2659
|
+
await fetch(url, {
|
|
2660
|
+
method: "POST",
|
|
2661
|
+
headers: { "Content-Type": "application/json" },
|
|
2662
|
+
body: JSON.stringify(body),
|
|
2663
|
+
signal: controller.signal
|
|
2664
|
+
});
|
|
2665
|
+
} finally {
|
|
2666
|
+
clearTimeout(timer);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
async function sendWebhook(config, payload, target) {
|
|
2670
|
+
try {
|
|
2671
|
+
const targets = target ? [target] : Object.keys(FORMATTERS);
|
|
2672
|
+
const promises = [];
|
|
2673
|
+
for (const t of targets) {
|
|
2674
|
+
const url = config[t];
|
|
2675
|
+
if (!url) continue;
|
|
2676
|
+
const formatter = FORMATTERS[t];
|
|
2677
|
+
if (!formatter) continue;
|
|
2678
|
+
const body = formatter(payload);
|
|
2679
|
+
promises.push(sendSingle(url, body).catch(() => {
|
|
2680
|
+
}));
|
|
2681
|
+
}
|
|
2682
|
+
await Promise.all(promises);
|
|
2683
|
+
} catch {
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
// src/commands/internal-sharing.ts
|
|
2688
|
+
import { extname as extname4 } from "path";
|
|
2689
|
+
async function uploadInternalSharing(client, packageName, filePath, fileType) {
|
|
2690
|
+
const resolvedType = fileType ?? detectFileType(filePath);
|
|
2691
|
+
const validation = await validateUploadFile(filePath);
|
|
2692
|
+
if (!validation.valid) {
|
|
2693
|
+
throw new GpcError(
|
|
2694
|
+
`File validation failed:
|
|
2695
|
+
${validation.errors.join("\n")}`,
|
|
2696
|
+
"INTERNAL_SHARING_INVALID_FILE",
|
|
2697
|
+
2,
|
|
2698
|
+
"Check that the file is a valid AAB or APK and is not corrupted."
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
let artifact;
|
|
2702
|
+
if (resolvedType === "bundle") {
|
|
2703
|
+
artifact = await client.internalAppSharing.uploadBundle(packageName, filePath);
|
|
2704
|
+
} else {
|
|
2705
|
+
artifact = await client.internalAppSharing.uploadApk(packageName, filePath);
|
|
2706
|
+
}
|
|
2707
|
+
return {
|
|
2708
|
+
downloadUrl: artifact.downloadUrl,
|
|
2709
|
+
sha256: artifact.sha256,
|
|
2710
|
+
certificateFingerprint: artifact.certificateFingerprint,
|
|
2711
|
+
fileType: resolvedType
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function detectFileType(filePath) {
|
|
2715
|
+
const ext = extname4(filePath).toLowerCase();
|
|
2716
|
+
if (ext === ".aab") return "bundle";
|
|
2717
|
+
if (ext === ".apk") return "apk";
|
|
2718
|
+
throw new GpcError(
|
|
2719
|
+
`Cannot detect file type from extension "${ext}". Use --type to specify bundle or apk.`,
|
|
2720
|
+
"INTERNAL_SHARING_UNKNOWN_TYPE",
|
|
2721
|
+
2,
|
|
2722
|
+
"Use --type bundle for .aab files or --type apk for .apk files."
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/commands/generated-apks.ts
|
|
2727
|
+
import { writeFile as writeFile4 } from "fs/promises";
|
|
2728
|
+
async function listGeneratedApks(client, packageName, versionCode) {
|
|
2729
|
+
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
|
2730
|
+
throw new GpcError(
|
|
2731
|
+
`Invalid version code: ${versionCode}`,
|
|
2732
|
+
"GENERATED_APKS_INVALID_VERSION",
|
|
2733
|
+
2,
|
|
2734
|
+
"Provide a positive integer version code."
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
return client.generatedApks.list(packageName, versionCode);
|
|
2738
|
+
}
|
|
2739
|
+
async function downloadGeneratedApk(client, packageName, versionCode, apkId, outputPath) {
|
|
2740
|
+
if (!Number.isInteger(versionCode) || versionCode <= 0) {
|
|
2741
|
+
throw new GpcError(
|
|
2742
|
+
`Invalid version code: ${versionCode}`,
|
|
2743
|
+
"GENERATED_APKS_INVALID_VERSION",
|
|
2744
|
+
2,
|
|
2745
|
+
"Provide a positive integer version code."
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
if (!apkId) {
|
|
2749
|
+
throw new GpcError(
|
|
2750
|
+
"APK ID is required",
|
|
2751
|
+
"GENERATED_APKS_MISSING_ID",
|
|
2752
|
+
2,
|
|
2753
|
+
"Provide the generated APK ID. Use 'gpc generated-apks list <version-code>' to see available APKs."
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
|
|
2757
|
+
const bytes = new Uint8Array(buffer);
|
|
2758
|
+
await writeFile4(outputPath, bytes);
|
|
2759
|
+
return { path: outputPath, sizeBytes: bytes.byteLength };
|
|
2760
|
+
}
|
|
2081
2761
|
export {
|
|
2082
2762
|
ApiError,
|
|
2083
2763
|
ConfigError,
|
|
@@ -2089,15 +2769,22 @@ export {
|
|
|
2089
2769
|
acknowledgeProductPurchase,
|
|
2090
2770
|
activateBasePlan,
|
|
2091
2771
|
activateOffer,
|
|
2772
|
+
addRecoveryTargeting,
|
|
2092
2773
|
addTesters,
|
|
2774
|
+
cancelRecoveryAction,
|
|
2093
2775
|
cancelSubscriptionPurchase,
|
|
2094
2776
|
checkThreshold,
|
|
2095
2777
|
compareVitalsTrend,
|
|
2096
2778
|
consumeProductPurchase,
|
|
2097
2779
|
convertRegionPrices,
|
|
2098
2780
|
createAuditEntry,
|
|
2781
|
+
createDeviceTier,
|
|
2782
|
+
createExternalTransaction,
|
|
2099
2783
|
createInAppProduct,
|
|
2100
2784
|
createOffer,
|
|
2785
|
+
createOneTimeOffer,
|
|
2786
|
+
createOneTimeProduct,
|
|
2787
|
+
createRecoveryAction,
|
|
2101
2788
|
createSubscription,
|
|
2102
2789
|
deactivateBasePlan,
|
|
2103
2790
|
deactivateOffer,
|
|
@@ -2107,18 +2794,33 @@ export {
|
|
|
2107
2794
|
deleteInAppProduct,
|
|
2108
2795
|
deleteListing,
|
|
2109
2796
|
deleteOffer,
|
|
2797
|
+
deleteOneTimeOffer,
|
|
2798
|
+
deleteOneTimeProduct,
|
|
2110
2799
|
deleteSubscription,
|
|
2800
|
+
deployRecoveryAction,
|
|
2111
2801
|
detectOutputFormat,
|
|
2112
2802
|
diffListings,
|
|
2803
|
+
diffListingsCommand,
|
|
2113
2804
|
discoverPlugins,
|
|
2805
|
+
downloadGeneratedApk,
|
|
2114
2806
|
downloadReport,
|
|
2807
|
+
exportDataSafety,
|
|
2115
2808
|
exportReviews,
|
|
2809
|
+
formatCustomPayload,
|
|
2810
|
+
formatDiscordPayload,
|
|
2116
2811
|
formatOutput,
|
|
2812
|
+
formatSlackPayload,
|
|
2813
|
+
generateNotesFromGit,
|
|
2117
2814
|
getAppInfo,
|
|
2118
2815
|
getCountryAvailability,
|
|
2816
|
+
getDataSafety,
|
|
2817
|
+
getDeviceTier,
|
|
2818
|
+
getExternalTransaction,
|
|
2119
2819
|
getInAppProduct,
|
|
2120
2820
|
getListings,
|
|
2121
2821
|
getOffer,
|
|
2822
|
+
getOneTimeOffer,
|
|
2823
|
+
getOneTimeProduct,
|
|
2122
2824
|
getProductPurchase,
|
|
2123
2825
|
getReleasesStatus,
|
|
2124
2826
|
getReview,
|
|
@@ -2133,6 +2835,7 @@ export {
|
|
|
2133
2835
|
getVitalsOverview,
|
|
2134
2836
|
getVitalsRendering,
|
|
2135
2837
|
getVitalsStartup,
|
|
2838
|
+
importDataSafety,
|
|
2136
2839
|
importTestersFromCsv,
|
|
2137
2840
|
initAudit,
|
|
2138
2841
|
inviteUser,
|
|
@@ -2141,9 +2844,14 @@ export {
|
|
|
2141
2844
|
isValidBcp47,
|
|
2142
2845
|
isValidReportType,
|
|
2143
2846
|
isValidStatsDimension,
|
|
2847
|
+
listDeviceTiers,
|
|
2848
|
+
listGeneratedApks,
|
|
2144
2849
|
listImages,
|
|
2145
2850
|
listInAppProducts,
|
|
2146
2851
|
listOffers,
|
|
2852
|
+
listOneTimeOffers,
|
|
2853
|
+
listOneTimeProducts,
|
|
2854
|
+
listRecoveryActions,
|
|
2147
2855
|
listReports,
|
|
2148
2856
|
listReviews,
|
|
2149
2857
|
listSubscriptions,
|
|
@@ -2161,6 +2869,7 @@ export {
|
|
|
2161
2869
|
readListingsFromDir,
|
|
2162
2870
|
readReleaseNotesFromDir,
|
|
2163
2871
|
redactSensitive,
|
|
2872
|
+
refundExternalTransaction,
|
|
2164
2873
|
refundOrder,
|
|
2165
2874
|
removeTesters,
|
|
2166
2875
|
removeUser,
|
|
@@ -2170,15 +2879,21 @@ export {
|
|
|
2170
2879
|
safePathWithin,
|
|
2171
2880
|
scaffoldPlugin,
|
|
2172
2881
|
searchVitalsErrors,
|
|
2882
|
+
sendWebhook,
|
|
2883
|
+
sortResults,
|
|
2173
2884
|
syncInAppProducts,
|
|
2174
2885
|
updateAppDetails,
|
|
2886
|
+
updateDataSafety,
|
|
2175
2887
|
updateInAppProduct,
|
|
2176
2888
|
updateListing,
|
|
2177
2889
|
updateOffer,
|
|
2890
|
+
updateOneTimeOffer,
|
|
2891
|
+
updateOneTimeProduct,
|
|
2178
2892
|
updateRollout,
|
|
2179
2893
|
updateSubscription,
|
|
2180
2894
|
updateUser,
|
|
2181
2895
|
uploadImage,
|
|
2896
|
+
uploadInternalSharing,
|
|
2182
2897
|
uploadRelease,
|
|
2183
2898
|
validateImage,
|
|
2184
2899
|
validatePreSubmission,
|