@gpc-cli/core 0.9.7 → 0.9.9

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
@@ -54,6 +54,8 @@ function formatOutput(data, format, redact = true) {
54
54
  return formatMarkdown(safe);
55
55
  case "table":
56
56
  return formatTable(safe);
57
+ case "junit":
58
+ return formatJunit(safe);
57
59
  default:
58
60
  return formatJson(safe);
59
61
  }
@@ -182,6 +184,73 @@ function toRows(data) {
182
184
  }
183
185
  return [];
184
186
  }
187
+ function escapeXml(str) {
188
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
189
+ }
190
+ function toTestCases(data, commandName) {
191
+ const cases = [];
192
+ let failures = 0;
193
+ if (Array.isArray(data)) {
194
+ for (const item of data) {
195
+ const tc = buildTestCase(item, commandName);
196
+ cases.push(tc.xml);
197
+ if (tc.failed) failures++;
198
+ }
199
+ } else if (typeof data === "object" && data !== null) {
200
+ const tc = buildTestCase(data, commandName);
201
+ cases.push(tc.xml);
202
+ if (tc.failed) failures++;
203
+ } else if (typeof data === "string") {
204
+ cases.push(
205
+ ` <testcase name="${escapeXml(data)}" classname="gpc.${escapeXml(commandName)}" />`
206
+ );
207
+ }
208
+ return { cases, failures };
209
+ }
210
+ function buildTestCase(item, commandName) {
211
+ if (typeof item !== "object" || item === null) {
212
+ const text = String(item);
213
+ return {
214
+ xml: ` <testcase name="${escapeXml(text)}" classname="gpc.${escapeXml(commandName)}" />`,
215
+ failed: false
216
+ };
217
+ }
218
+ const record = item;
219
+ const name = escapeXml(
220
+ String(record["name"] ?? record["title"] ?? record["sku"] ?? record["id"] ?? JSON.stringify(item))
221
+ );
222
+ const classname = `gpc.${escapeXml(commandName)}`;
223
+ const breached = record["breached"];
224
+ if (breached === true) {
225
+ const message = escapeXml(String(record["message"] ?? "threshold breached"));
226
+ const details = escapeXml(
227
+ String(record["details"] ?? record["metric"] ?? JSON.stringify(item))
228
+ );
229
+ return {
230
+ xml: ` <testcase name="${name}" classname="${classname}">
231
+ <failure message="${message}">${details}</failure>
232
+ </testcase>`,
233
+ failed: true
234
+ };
235
+ }
236
+ return {
237
+ xml: ` <testcase name="${name}" classname="${classname}" />`,
238
+ failed: false
239
+ };
240
+ }
241
+ function formatJunit(data, commandName = "command") {
242
+ const { cases, failures } = toTestCases(data, commandName);
243
+ const tests = cases.length;
244
+ const lines = [
245
+ '<?xml version="1.0" encoding="UTF-8"?>',
246
+ `<testsuites name="gpc" tests="${tests}" failures="${failures}" time="0">`,
247
+ ` <testsuite name="${escapeXml(commandName)}" tests="${tests}" failures="${failures}">`,
248
+ ...cases,
249
+ " </testsuite>",
250
+ "</testsuites>"
251
+ ];
252
+ return lines.join("\n");
253
+ }
185
254
 
186
255
  // src/plugins.ts
187
256
  var PluginManager = class {
@@ -682,6 +751,90 @@ async function listTracks(client, packageName) {
682
751
  throw error;
683
752
  }
684
753
  }
754
+ async function createTrack(client, packageName, trackName) {
755
+ if (!trackName || trackName.trim().length === 0) {
756
+ throw new GpcError(
757
+ "Track name must not be empty",
758
+ "TRACK_INVALID_NAME",
759
+ 2,
760
+ "Provide a valid custom track name, e.g.: gpc tracks create my-qa-track"
761
+ );
762
+ }
763
+ const edit = await client.edits.insert(packageName);
764
+ try {
765
+ const track = await client.tracks.create(packageName, edit.id, trackName);
766
+ await client.edits.validate(packageName, edit.id);
767
+ await client.edits.commit(packageName, edit.id);
768
+ return track;
769
+ } catch (error) {
770
+ await client.edits.delete(packageName, edit.id).catch(() => {
771
+ });
772
+ throw error;
773
+ }
774
+ }
775
+ async function updateTrackConfig(client, packageName, trackName, config) {
776
+ if (!trackName || trackName.trim().length === 0) {
777
+ throw new GpcError(
778
+ "Track name must not be empty",
779
+ "TRACK_INVALID_NAME",
780
+ 2,
781
+ "Provide a valid track name."
782
+ );
783
+ }
784
+ const edit = await client.edits.insert(packageName);
785
+ try {
786
+ const release = {
787
+ versionCodes: config["versionCodes"] || [],
788
+ status: config["status"] || "completed"
789
+ };
790
+ if (config["userFraction"] !== void 0) {
791
+ release.userFraction = config["userFraction"];
792
+ }
793
+ if (config["releaseNotes"]) {
794
+ release.releaseNotes = config["releaseNotes"];
795
+ }
796
+ if (config["name"]) {
797
+ release.name = config["name"];
798
+ }
799
+ const track = await client.tracks.update(packageName, edit.id, trackName, release);
800
+ await client.edits.validate(packageName, edit.id);
801
+ await client.edits.commit(packageName, edit.id);
802
+ return track;
803
+ } catch (error) {
804
+ await client.edits.delete(packageName, edit.id).catch(() => {
805
+ });
806
+ throw error;
807
+ }
808
+ }
809
+ async function uploadExternallyHosted(client, packageName, data) {
810
+ if (!data.externallyHostedUrl) {
811
+ throw new GpcError(
812
+ "externallyHostedUrl is required",
813
+ "EXTERNAL_APK_MISSING_URL",
814
+ 2,
815
+ "Provide a valid URL for the externally hosted APK."
816
+ );
817
+ }
818
+ if (!data.packageName) {
819
+ throw new GpcError(
820
+ "packageName is required in externally hosted APK data",
821
+ "EXTERNAL_APK_MISSING_PACKAGE",
822
+ 2,
823
+ "Include the packageName field in the APK configuration."
824
+ );
825
+ }
826
+ const edit = await client.edits.insert(packageName);
827
+ try {
828
+ const result = await client.apks.addExternallyHosted(packageName, edit.id, data);
829
+ await client.edits.validate(packageName, edit.id);
830
+ await client.edits.commit(packageName, edit.id);
831
+ return result;
832
+ } catch (error) {
833
+ await client.edits.delete(packageName, edit.id).catch(() => {
834
+ });
835
+ throw error;
836
+ }
837
+ }
685
838
 
686
839
  // src/utils/bcp47.ts
687
840
  var GOOGLE_PLAY_LANGUAGES = [
@@ -1111,6 +1264,75 @@ async function getCountryAvailability(client, packageName, track) {
1111
1264
  throw error;
1112
1265
  }
1113
1266
  }
1267
+ var ALL_IMAGE_TYPES = [
1268
+ "phoneScreenshots",
1269
+ "sevenInchScreenshots",
1270
+ "tenInchScreenshots",
1271
+ "tvScreenshots",
1272
+ "wearScreenshots",
1273
+ "icon",
1274
+ "featureGraphic",
1275
+ "tvBanner"
1276
+ ];
1277
+ async function exportImages(client, packageName, dir, options) {
1278
+ const { mkdir: mkdir5, writeFile: writeFile6 } = await import("fs/promises");
1279
+ const { join: join7 } = await import("path");
1280
+ const edit = await client.edits.insert(packageName);
1281
+ try {
1282
+ let languages;
1283
+ if (options?.lang) {
1284
+ validateLanguage(options.lang);
1285
+ languages = [options.lang];
1286
+ } else {
1287
+ const listings = await client.listings.list(packageName, edit.id);
1288
+ languages = listings.map((l) => l.language);
1289
+ }
1290
+ const imageTypes = options?.type ? [options.type] : ALL_IMAGE_TYPES;
1291
+ let totalImages = 0;
1292
+ let totalSize = 0;
1293
+ const tasks = [];
1294
+ for (const language of languages) {
1295
+ for (const imageType of imageTypes) {
1296
+ const images = await client.images.list(packageName, edit.id, language, imageType);
1297
+ for (let i = 0; i < images.length; i++) {
1298
+ const img = images[i];
1299
+ if (img && img.url) {
1300
+ tasks.push({ language, imageType, url: img.url, index: i + 1 });
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ const concurrency = 5;
1306
+ for (let i = 0; i < tasks.length; i += concurrency) {
1307
+ const batch = tasks.slice(i, i + concurrency);
1308
+ const results = await Promise.all(
1309
+ batch.map(async (task) => {
1310
+ const dirPath = join7(dir, task.language, task.imageType);
1311
+ await mkdir5(dirPath, { recursive: true });
1312
+ const response = await fetch(task.url);
1313
+ const buffer = Buffer.from(await response.arrayBuffer());
1314
+ const filePath = join7(dirPath, `${task.index}.png`);
1315
+ await writeFile6(filePath, buffer);
1316
+ return buffer.length;
1317
+ })
1318
+ );
1319
+ for (const size of results) {
1320
+ totalImages++;
1321
+ totalSize += size;
1322
+ }
1323
+ }
1324
+ await client.edits.delete(packageName, edit.id);
1325
+ return {
1326
+ languages: languages.length,
1327
+ images: totalImages,
1328
+ totalSize
1329
+ };
1330
+ } catch (error) {
1331
+ await client.edits.delete(packageName, edit.id).catch(() => {
1332
+ });
1333
+ throw error;
1334
+ }
1335
+ }
1114
1336
  async function updateAppDetails(client, packageName, details) {
1115
1337
  const edit = await client.edits.insert(packageName);
1116
1338
  try {
@@ -1125,14 +1347,202 @@ async function updateAppDetails(client, packageName, details) {
1125
1347
  }
1126
1348
  }
1127
1349
 
1350
+ // src/commands/migrate.ts
1351
+ import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
1352
+ import { join as join2 } from "path";
1353
+ async function fileExists(path) {
1354
+ try {
1355
+ await access(path);
1356
+ return true;
1357
+ } catch {
1358
+ return false;
1359
+ }
1360
+ }
1361
+ async function detectFastlane(cwd) {
1362
+ const result = {
1363
+ hasFastfile: false,
1364
+ hasAppfile: false,
1365
+ hasMetadata: false,
1366
+ hasGemfile: false,
1367
+ lanes: [],
1368
+ metadataLanguages: []
1369
+ };
1370
+ const fastlaneDir = join2(cwd, "fastlane");
1371
+ const hasFastlaneDir = await fileExists(fastlaneDir);
1372
+ const fastfilePath = hasFastlaneDir ? join2(fastlaneDir, "Fastfile") : join2(cwd, "Fastfile");
1373
+ const appfilePath = hasFastlaneDir ? join2(fastlaneDir, "Appfile") : join2(cwd, "Appfile");
1374
+ result.hasFastfile = await fileExists(fastfilePath);
1375
+ result.hasAppfile = await fileExists(appfilePath);
1376
+ result.hasGemfile = await fileExists(join2(cwd, "Gemfile"));
1377
+ const metadataDir = hasFastlaneDir ? join2(fastlaneDir, "metadata", "android") : join2(cwd, "metadata", "android");
1378
+ result.hasMetadata = await fileExists(metadataDir);
1379
+ if (result.hasMetadata) {
1380
+ try {
1381
+ const entries = await readdir2(metadataDir, { withFileTypes: true });
1382
+ result.metadataLanguages = entries.filter((e) => e.isDirectory()).map((e) => e.name);
1383
+ } catch {
1384
+ }
1385
+ }
1386
+ if (result.hasFastfile) {
1387
+ try {
1388
+ const content = await readFile3(fastfilePath, "utf-8");
1389
+ result.lanes = parseFastfile(content);
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ if (result.hasAppfile) {
1394
+ try {
1395
+ const content = await readFile3(appfilePath, "utf-8");
1396
+ const parsed = parseAppfile(content);
1397
+ result.packageName = parsed.packageName;
1398
+ result.jsonKeyPath = parsed.jsonKeyPath;
1399
+ } catch {
1400
+ }
1401
+ }
1402
+ return result;
1403
+ }
1404
+ function parseFastfile(content) {
1405
+ const lanes = [];
1406
+ const laneRegex = /lane\s+:(\w+)\s+do([\s\S]*?)(?=\bend\b)/g;
1407
+ let match;
1408
+ while ((match = laneRegex.exec(content)) !== null) {
1409
+ const name = match[1] ?? "";
1410
+ const body = match[2] ?? "";
1411
+ const actions = [];
1412
+ const actionRegex = /\b(supply|upload_to_play_store|capture_android_screenshots|deliver|gradle)\b/g;
1413
+ let actionMatch;
1414
+ while ((actionMatch = actionRegex.exec(body)) !== null) {
1415
+ const action = actionMatch[1] ?? "";
1416
+ if (!actions.includes(action)) {
1417
+ actions.push(action);
1418
+ }
1419
+ }
1420
+ const gpcEquivalent = mapLaneToGpc(name, actions, body);
1421
+ lanes.push({ name, actions, gpcEquivalent });
1422
+ }
1423
+ return lanes;
1424
+ }
1425
+ function mapLaneToGpc(name, actions, body) {
1426
+ if (actions.includes("upload_to_play_store") || actions.includes("supply")) {
1427
+ const trackMatch = body.match(/track\s*:\s*["'](\w+)["']/);
1428
+ const rolloutMatch = body.match(/rollout\s*:\s*["']?([\d.]+)["']?/);
1429
+ if (rolloutMatch) {
1430
+ const percentage = Math.round(parseFloat(rolloutMatch[1] ?? "0") * 100);
1431
+ return `gpc releases promote --rollout ${percentage}`;
1432
+ }
1433
+ if (trackMatch) {
1434
+ return `gpc releases upload --track ${trackMatch[1]}`;
1435
+ }
1436
+ if (body.match(/skip_upload_apk\s*:\s*true/) || body.match(/skip_upload_aab\s*:\s*true/)) {
1437
+ return "gpc listings push";
1438
+ }
1439
+ return "gpc releases upload";
1440
+ }
1441
+ if (actions.includes("capture_android_screenshots")) {
1442
+ return void 0;
1443
+ }
1444
+ return void 0;
1445
+ }
1446
+ function parseAppfile(content) {
1447
+ const result = {};
1448
+ const pkgMatch = content.match(/package_name\s*\(?\s*["']([^"']+)["']\s*\)?/);
1449
+ if (pkgMatch) {
1450
+ result.packageName = pkgMatch[1];
1451
+ }
1452
+ const keyMatch = content.match(/json_key_file\s*\(?\s*["']([^"']+)["']\s*\)?/);
1453
+ if (keyMatch) {
1454
+ result.jsonKeyPath = keyMatch[1];
1455
+ }
1456
+ return result;
1457
+ }
1458
+ function generateMigrationPlan(detection) {
1459
+ const config = {};
1460
+ const checklist = [];
1461
+ const warnings = [];
1462
+ if (detection.packageName) {
1463
+ config["app"] = detection.packageName;
1464
+ } else {
1465
+ checklist.push("Set your package name: gpc config set app <package-name>");
1466
+ }
1467
+ if (detection.jsonKeyPath) {
1468
+ config["auth"] = { serviceAccount: detection.jsonKeyPath };
1469
+ } else {
1470
+ checklist.push("Configure authentication: gpc auth setup");
1471
+ }
1472
+ for (const lane of detection.lanes) {
1473
+ if (lane.gpcEquivalent) {
1474
+ checklist.push(`Replace Fastlane lane "${lane.name}" with: ${lane.gpcEquivalent}`);
1475
+ }
1476
+ if (lane.actions.includes("capture_android_screenshots")) {
1477
+ warnings.push(
1478
+ `Lane "${lane.name}" uses capture_android_screenshots which has no GPC equivalent. You will need to continue using Fastlane for screenshot capture or use a separate tool.`
1479
+ );
1480
+ }
1481
+ }
1482
+ if (detection.hasMetadata && detection.metadataLanguages.length > 0) {
1483
+ checklist.push(
1484
+ `Migrate metadata for ${detection.metadataLanguages.length} language(s): gpc listings pull --dir metadata`
1485
+ );
1486
+ checklist.push("Review and push metadata: gpc listings push --dir metadata");
1487
+ }
1488
+ checklist.push("Run gpc doctor to verify your setup");
1489
+ checklist.push("Test with --dry-run before making real changes");
1490
+ if (detection.hasGemfile) {
1491
+ checklist.push("Remove Fastlane from your Gemfile once migration is complete");
1492
+ }
1493
+ if (detection.lanes.some((l) => l.actions.includes("supply") || l.actions.includes("upload_to_play_store"))) {
1494
+ checklist.push("Update CI/CD pipelines to use gpc commands instead of Fastlane lanes");
1495
+ }
1496
+ return { config, checklist, warnings };
1497
+ }
1498
+ async function writeMigrationOutput(result, dir) {
1499
+ await mkdir2(dir, { recursive: true });
1500
+ const files = [];
1501
+ const configPath = join2(dir, ".gpcrc.json");
1502
+ await writeFile2(configPath, JSON.stringify(result.config, null, 2) + "\n", "utf-8");
1503
+ files.push(configPath);
1504
+ const migrationPath = join2(dir, "MIGRATION.md");
1505
+ const lines = [
1506
+ "# Fastlane to GPC Migration",
1507
+ "",
1508
+ "## Migration Checklist",
1509
+ ""
1510
+ ];
1511
+ for (const item of result.checklist) {
1512
+ lines.push(`- [ ] ${item}`);
1513
+ }
1514
+ if (result.warnings.length > 0) {
1515
+ lines.push("");
1516
+ lines.push("## Warnings");
1517
+ lines.push("");
1518
+ for (const warning of result.warnings) {
1519
+ lines.push(`- ${warning}`);
1520
+ }
1521
+ }
1522
+ lines.push("");
1523
+ lines.push("## Quick Reference");
1524
+ lines.push("");
1525
+ lines.push("| Fastlane | GPC |");
1526
+ lines.push("|----------|-----|");
1527
+ lines.push("| `fastlane supply` | `gpc releases upload` / `gpc listings push` |");
1528
+ lines.push("| `upload_to_play_store` | `gpc releases upload` |");
1529
+ lines.push('| `supply(track: "internal")` | `gpc releases upload --track internal` |');
1530
+ lines.push('| `supply(rollout: "0.1")` | `gpc releases promote --rollout 10` |');
1531
+ lines.push("| `capture_android_screenshots` | No equivalent (use separate tool) |");
1532
+ lines.push("");
1533
+ await writeFile2(migrationPath, lines.join("\n"), "utf-8");
1534
+ files.push(migrationPath);
1535
+ return files;
1536
+ }
1537
+
1128
1538
  // src/utils/release-notes.ts
1129
- import { readdir as readdir2, readFile as readFile3, stat as stat4 } from "fs/promises";
1130
- import { extname as extname3, basename, join as join2 } from "path";
1539
+ import { readdir as readdir3, readFile as readFile4, stat as stat4 } from "fs/promises";
1540
+ import { extname as extname3, basename, join as join3 } from "path";
1131
1541
  var MAX_NOTES_LENGTH = 500;
1132
1542
  async function readReleaseNotesFromDir(dir) {
1133
1543
  let entries;
1134
1544
  try {
1135
- entries = await readdir2(dir);
1545
+ entries = await readdir3(dir);
1136
1546
  } catch {
1137
1547
  throw new GpcError(
1138
1548
  `Release notes directory not found: ${dir}`,
@@ -1145,10 +1555,10 @@ async function readReleaseNotesFromDir(dir) {
1145
1555
  for (const entry of entries) {
1146
1556
  if (extname3(entry) !== ".txt") continue;
1147
1557
  const language = basename(entry, ".txt");
1148
- const filePath = join2(dir, entry);
1558
+ const filePath = join3(dir, entry);
1149
1559
  const stats = await stat4(filePath);
1150
1560
  if (!stats.isFile()) continue;
1151
- const text = (await readFile3(filePath, "utf-8")).trim();
1561
+ const text = (await readFile4(filePath, "utf-8")).trim();
1152
1562
  if (text.length === 0) continue;
1153
1563
  notes.push({ language, text });
1154
1564
  }
@@ -1633,8 +2043,8 @@ async function deactivateOffer(client, packageName, productId, basePlanId, offer
1633
2043
  }
1634
2044
 
1635
2045
  // src/commands/iap.ts
1636
- import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
1637
- import { join as join3 } from "path";
2046
+ import { readdir as readdir4, readFile as readFile5 } from "fs/promises";
2047
+ import { join as join4 } from "path";
1638
2048
  import { paginateAll as paginateAll3 } from "@gpc-cli/api";
1639
2049
  async function listInAppProducts(client, packageName, options) {
1640
2050
  if (options?.limit || options?.nextPage) {
@@ -1671,14 +2081,59 @@ async function deleteInAppProduct(client, packageName, sku) {
1671
2081
  return client.inappproducts.delete(packageName, sku);
1672
2082
  }
1673
2083
  async function syncInAppProducts(client, packageName, dir, options) {
1674
- const files = await readdir3(dir);
1675
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
1676
- if (jsonFiles.length === 0) {
2084
+ const localProducts = await readProductsFromDir(dir);
2085
+ if (localProducts.length === 0) {
1677
2086
  return { created: 0, updated: 0, unchanged: 0, skus: [] };
1678
2087
  }
2088
+ const response = await client.inappproducts.list(packageName);
2089
+ const remoteSkus = new Set((response.inappproduct || []).map((p) => p.sku));
2090
+ const toUpdate = localProducts.filter((p) => remoteSkus.has(p.sku));
2091
+ const toCreate = localProducts.filter((p) => !remoteSkus.has(p.sku));
2092
+ const skus = localProducts.map((p) => p.sku);
2093
+ if (options?.dryRun) {
2094
+ return { created: toCreate.length, updated: toUpdate.length, unchanged: 0, skus };
2095
+ }
2096
+ if (toUpdate.length > 1) {
2097
+ try {
2098
+ await batchUpdateProducts(client, packageName, toUpdate);
2099
+ } catch {
2100
+ for (const product of toUpdate) {
2101
+ await client.inappproducts.update(packageName, product.sku, product);
2102
+ }
2103
+ }
2104
+ } else {
2105
+ for (const product of toUpdate) {
2106
+ await client.inappproducts.update(packageName, product.sku, product);
2107
+ }
2108
+ }
2109
+ for (const product of toCreate) {
2110
+ await client.inappproducts.create(packageName, product);
2111
+ }
2112
+ return { created: toCreate.length, updated: toUpdate.length, unchanged: 0, skus };
2113
+ }
2114
+ var BATCH_CHUNK_SIZE = 100;
2115
+ async function batchUpdateProducts(client, packageName, products) {
2116
+ const results = [];
2117
+ for (let i = 0; i < products.length; i += BATCH_CHUNK_SIZE) {
2118
+ const chunk = products.slice(i, i + BATCH_CHUNK_SIZE);
2119
+ const request = {
2120
+ requests: chunk.map((p) => ({
2121
+ inappproduct: p,
2122
+ packageName,
2123
+ sku: p.sku
2124
+ }))
2125
+ };
2126
+ const response = await client.inappproducts.batchUpdate(packageName, request);
2127
+ results.push(...response.inappproducts || []);
2128
+ }
2129
+ return results;
2130
+ }
2131
+ async function readProductsFromDir(dir) {
2132
+ const files = await readdir4(dir);
2133
+ const jsonFiles = files.filter((f) => f.endsWith(".json"));
1679
2134
  const localProducts = [];
1680
2135
  for (const file of jsonFiles) {
1681
- const content = await readFile4(join3(dir, file), "utf-8");
2136
+ const content = await readFile5(join4(dir, file), "utf-8");
1682
2137
  try {
1683
2138
  localProducts.push(JSON.parse(content));
1684
2139
  } catch {
@@ -1690,27 +2145,56 @@ async function syncInAppProducts(client, packageName, dir, options) {
1690
2145
  );
1691
2146
  }
1692
2147
  }
2148
+ return localProducts;
2149
+ }
2150
+ async function batchSyncInAppProducts(client, packageName, dir, options) {
2151
+ const localProducts = await readProductsFromDir(dir);
2152
+ if (localProducts.length === 0) {
2153
+ return { created: 0, updated: 0, unchanged: 0, skus: [], batchUsed: false, batchErrors: 0 };
2154
+ }
1693
2155
  const response = await client.inappproducts.list(packageName);
1694
2156
  const remoteSkus = new Set((response.inappproduct || []).map((p) => p.sku));
1695
- let created = 0;
1696
- let updated = 0;
1697
- const unchanged = 0;
1698
- const skus = [];
1699
- for (const product of localProducts) {
1700
- skus.push(product.sku);
1701
- if (remoteSkus.has(product.sku)) {
1702
- if (!options?.dryRun) {
2157
+ const toUpdate = localProducts.filter((p) => remoteSkus.has(p.sku));
2158
+ const toCreate = localProducts.filter((p) => !remoteSkus.has(p.sku));
2159
+ const skus = localProducts.map((p) => p.sku);
2160
+ if (options?.dryRun) {
2161
+ return {
2162
+ created: toCreate.length,
2163
+ updated: toUpdate.length,
2164
+ unchanged: 0,
2165
+ skus,
2166
+ batchUsed: toUpdate.length > 1,
2167
+ batchErrors: 0
2168
+ };
2169
+ }
2170
+ let batchUsed = false;
2171
+ let batchErrors = 0;
2172
+ if (toUpdate.length > 1) {
2173
+ batchUsed = true;
2174
+ try {
2175
+ await batchUpdateProducts(client, packageName, toUpdate);
2176
+ } catch {
2177
+ batchErrors++;
2178
+ for (const product of toUpdate) {
1703
2179
  await client.inappproducts.update(packageName, product.sku, product);
1704
2180
  }
1705
- updated++;
1706
- } else {
1707
- if (!options?.dryRun) {
1708
- await client.inappproducts.create(packageName, product);
1709
- }
1710
- created++;
1711
2181
  }
2182
+ } else {
2183
+ for (const product of toUpdate) {
2184
+ await client.inappproducts.update(packageName, product.sku, product);
2185
+ }
2186
+ }
2187
+ for (const product of toCreate) {
2188
+ await client.inappproducts.create(packageName, product);
1712
2189
  }
1713
- return { created, updated, unchanged, skus };
2190
+ return {
2191
+ created: toCreate.length,
2192
+ updated: toUpdate.length,
2193
+ unchanged: 0,
2194
+ skus,
2195
+ batchUsed,
2196
+ batchErrors
2197
+ };
1714
2198
  }
1715
2199
 
1716
2200
  // src/commands/purchases.ts
@@ -1934,7 +2418,7 @@ function parseGrantArg(grantStr) {
1934
2418
  }
1935
2419
 
1936
2420
  // src/commands/testers.ts
1937
- import { readFile as readFile5 } from "fs/promises";
2421
+ import { readFile as readFile6 } from "fs/promises";
1938
2422
  async function listTesters(client, packageName, track) {
1939
2423
  const edit = await client.edits.insert(packageName);
1940
2424
  try {
@@ -1983,7 +2467,7 @@ async function removeTesters(client, packageName, track, groupEmails) {
1983
2467
  }
1984
2468
  }
1985
2469
  async function importTestersFromCsv(client, packageName, track, csvPath) {
1986
- const content = await readFile5(csvPath, "utf-8");
2470
+ const content = await readFile6(csvPath, "utf-8");
1987
2471
  const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
1988
2472
  if (emails.length === 0) {
1989
2473
  throw new GpcError(
@@ -2114,9 +2598,15 @@ async function cancelRecoveryAction(client, packageName, recoveryId) {
2114
2598
  async function deployRecoveryAction(client, packageName, recoveryId) {
2115
2599
  return client.appRecovery.deploy(packageName, recoveryId);
2116
2600
  }
2601
+ async function createRecoveryAction(client, packageName, request) {
2602
+ return client.appRecovery.create(packageName, request);
2603
+ }
2604
+ async function addRecoveryTargeting(client, packageName, actionId, targeting) {
2605
+ return client.appRecovery.addTargeting(packageName, actionId, targeting);
2606
+ }
2117
2607
 
2118
2608
  // src/commands/data-safety.ts
2119
- import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2609
+ import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
2120
2610
  async function getDataSafety(client, packageName) {
2121
2611
  const edit = await client.edits.insert(packageName);
2122
2612
  try {
@@ -2144,16 +2634,21 @@ async function updateDataSafety(client, packageName, data) {
2144
2634
  }
2145
2635
  async function exportDataSafety(client, packageName, outputPath) {
2146
2636
  const dataSafety = await getDataSafety(client, packageName);
2147
- await writeFile2(outputPath, JSON.stringify(dataSafety, null, 2) + "\n", "utf-8");
2637
+ await writeFile3(outputPath, JSON.stringify(dataSafety, null, 2) + "\n", "utf-8");
2148
2638
  return dataSafety;
2149
2639
  }
2150
2640
  async function importDataSafety(client, packageName, filePath) {
2151
- const content = await readFile6(filePath, "utf-8");
2641
+ const content = await readFile7(filePath, "utf-8");
2152
2642
  let data;
2153
2643
  try {
2154
2644
  data = JSON.parse(content);
2155
2645
  } catch {
2156
- throw new Error(`Failed to parse data safety JSON from "${filePath}"`);
2646
+ throw new GpcError(
2647
+ `Failed to parse data safety JSON from "${filePath}"`,
2648
+ "INVALID_JSON",
2649
+ 1,
2650
+ "Ensure the file contains valid JSON matching the data safety schema."
2651
+ );
2157
2652
  }
2158
2653
  return updateDataSafety(client, packageName, data);
2159
2654
  }
@@ -2169,6 +2664,251 @@ async function refundExternalTransaction(client, packageName, transactionId, ref
2169
2664
  return client.externalTransactions.refund(packageName, transactionId, refundData);
2170
2665
  }
2171
2666
 
2667
+ // src/commands/device-tiers.ts
2668
+ async function listDeviceTiers(client, packageName) {
2669
+ if (!packageName) {
2670
+ throw new GpcError(
2671
+ "Package name is required",
2672
+ "MISSING_PACKAGE_NAME",
2673
+ 2,
2674
+ "Provide a package name with --app or set it in config."
2675
+ );
2676
+ }
2677
+ return client.deviceTiers.list(packageName);
2678
+ }
2679
+ async function getDeviceTier(client, packageName, configId) {
2680
+ if (!packageName) {
2681
+ throw new GpcError(
2682
+ "Package name is required",
2683
+ "MISSING_PACKAGE_NAME",
2684
+ 2,
2685
+ "Provide a package name with --app or set it in config."
2686
+ );
2687
+ }
2688
+ if (!configId) {
2689
+ throw new GpcError(
2690
+ "Config ID is required",
2691
+ "MISSING_CONFIG_ID",
2692
+ 2,
2693
+ "Provide a device tier config ID."
2694
+ );
2695
+ }
2696
+ return client.deviceTiers.get(packageName, configId);
2697
+ }
2698
+ async function createDeviceTier(client, packageName, config) {
2699
+ if (!packageName) {
2700
+ throw new GpcError(
2701
+ "Package name is required",
2702
+ "MISSING_PACKAGE_NAME",
2703
+ 2,
2704
+ "Provide a package name with --app or set it in config."
2705
+ );
2706
+ }
2707
+ if (!config || !config.deviceGroups || config.deviceGroups.length === 0) {
2708
+ throw new GpcError(
2709
+ "Device tier config must include at least one device group",
2710
+ "INVALID_DEVICE_TIER_CONFIG",
2711
+ 2,
2712
+ "Provide a valid config with deviceGroups."
2713
+ );
2714
+ }
2715
+ return client.deviceTiers.create(packageName, config);
2716
+ }
2717
+
2718
+ // src/commands/one-time-products.ts
2719
+ async function listOneTimeProducts(client, packageName) {
2720
+ try {
2721
+ return await client.oneTimeProducts.list(packageName);
2722
+ } catch (error) {
2723
+ throw new GpcError(
2724
+ `Failed to list one-time products: ${error instanceof Error ? error.message : String(error)}`,
2725
+ "OTP_LIST_FAILED",
2726
+ 4,
2727
+ "Check your package name and API credentials."
2728
+ );
2729
+ }
2730
+ }
2731
+ async function getOneTimeProduct(client, packageName, productId) {
2732
+ try {
2733
+ return await client.oneTimeProducts.get(packageName, productId);
2734
+ } catch (error) {
2735
+ throw new GpcError(
2736
+ `Failed to get one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2737
+ "OTP_GET_FAILED",
2738
+ 4,
2739
+ "Check that the product ID exists."
2740
+ );
2741
+ }
2742
+ }
2743
+ async function createOneTimeProduct(client, packageName, data) {
2744
+ try {
2745
+ return await client.oneTimeProducts.create(packageName, data);
2746
+ } catch (error) {
2747
+ throw new GpcError(
2748
+ `Failed to create one-time product: ${error instanceof Error ? error.message : String(error)}`,
2749
+ "OTP_CREATE_FAILED",
2750
+ 4,
2751
+ "Verify the product data and ensure the product ID is unique."
2752
+ );
2753
+ }
2754
+ }
2755
+ async function updateOneTimeProduct(client, packageName, productId, data) {
2756
+ try {
2757
+ return await client.oneTimeProducts.update(packageName, productId, data);
2758
+ } catch (error) {
2759
+ throw new GpcError(
2760
+ `Failed to update one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2761
+ "OTP_UPDATE_FAILED",
2762
+ 4,
2763
+ "Check that the product ID exists and the data is valid."
2764
+ );
2765
+ }
2766
+ }
2767
+ async function deleteOneTimeProduct(client, packageName, productId) {
2768
+ try {
2769
+ await client.oneTimeProducts.delete(packageName, productId);
2770
+ } catch (error) {
2771
+ throw new GpcError(
2772
+ `Failed to delete one-time product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2773
+ "OTP_DELETE_FAILED",
2774
+ 4,
2775
+ "Check that the product ID exists and is not active."
2776
+ );
2777
+ }
2778
+ }
2779
+ async function listOneTimeOffers(client, packageName, productId) {
2780
+ try {
2781
+ return await client.oneTimeProducts.listOffers(packageName, productId);
2782
+ } catch (error) {
2783
+ throw new GpcError(
2784
+ `Failed to list offers for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2785
+ "OTP_OFFERS_LIST_FAILED",
2786
+ 4,
2787
+ "Check the product ID and your API credentials."
2788
+ );
2789
+ }
2790
+ }
2791
+ async function getOneTimeOffer(client, packageName, productId, offerId) {
2792
+ try {
2793
+ return await client.oneTimeProducts.getOffer(packageName, productId, offerId);
2794
+ } catch (error) {
2795
+ throw new GpcError(
2796
+ `Failed to get offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2797
+ "OTP_OFFER_GET_FAILED",
2798
+ 4,
2799
+ "Check that the product and offer IDs exist."
2800
+ );
2801
+ }
2802
+ }
2803
+ async function createOneTimeOffer(client, packageName, productId, data) {
2804
+ try {
2805
+ return await client.oneTimeProducts.createOffer(packageName, productId, data);
2806
+ } catch (error) {
2807
+ throw new GpcError(
2808
+ `Failed to create offer for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2809
+ "OTP_OFFER_CREATE_FAILED",
2810
+ 4,
2811
+ "Verify the offer data and ensure the offer ID is unique."
2812
+ );
2813
+ }
2814
+ }
2815
+ async function updateOneTimeOffer(client, packageName, productId, offerId, data) {
2816
+ try {
2817
+ return await client.oneTimeProducts.updateOffer(packageName, productId, offerId, data);
2818
+ } catch (error) {
2819
+ throw new GpcError(
2820
+ `Failed to update offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2821
+ "OTP_OFFER_UPDATE_FAILED",
2822
+ 4,
2823
+ "Check that the product and offer IDs exist and the data is valid."
2824
+ );
2825
+ }
2826
+ }
2827
+ async function deleteOneTimeOffer(client, packageName, productId, offerId) {
2828
+ try {
2829
+ await client.oneTimeProducts.deleteOffer(packageName, productId, offerId);
2830
+ } catch (error) {
2831
+ throw new GpcError(
2832
+ `Failed to delete offer "${offerId}" for product "${productId}": ${error instanceof Error ? error.message : String(error)}`,
2833
+ "OTP_OFFER_DELETE_FAILED",
2834
+ 4,
2835
+ "Check that the product and offer IDs exist."
2836
+ );
2837
+ }
2838
+ }
2839
+
2840
+ // src/utils/spinner.ts
2841
+ import process2 from "process";
2842
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2843
+ var INTERVAL_MS = 80;
2844
+ function createSpinner(message) {
2845
+ const isTTY = process2.stderr.isTTY === true;
2846
+ let frameIndex = 0;
2847
+ let timer;
2848
+ let currentMessage = message;
2849
+ let started = false;
2850
+ function clearLine() {
2851
+ if (isTTY) {
2852
+ process2.stderr.write("\r\x1B[K");
2853
+ }
2854
+ }
2855
+ function renderFrame() {
2856
+ const frame = FRAMES[frameIndex % FRAMES.length];
2857
+ process2.stderr.write(`\r\x1B[K${frame} ${currentMessage}`);
2858
+ frameIndex++;
2859
+ }
2860
+ return {
2861
+ start() {
2862
+ if (started) return;
2863
+ started = true;
2864
+ if (!isTTY) {
2865
+ process2.stderr.write(`${currentMessage}
2866
+ `);
2867
+ return;
2868
+ }
2869
+ renderFrame();
2870
+ timer = setInterval(renderFrame, INTERVAL_MS);
2871
+ },
2872
+ stop(msg) {
2873
+ if (timer) {
2874
+ clearInterval(timer);
2875
+ timer = void 0;
2876
+ }
2877
+ const text = msg ?? currentMessage;
2878
+ if (isTTY) {
2879
+ clearLine();
2880
+ process2.stderr.write(`\u2714 ${text}
2881
+ `);
2882
+ } else if (!started) {
2883
+ process2.stderr.write(`${text}
2884
+ `);
2885
+ }
2886
+ started = false;
2887
+ },
2888
+ fail(msg) {
2889
+ if (timer) {
2890
+ clearInterval(timer);
2891
+ timer = void 0;
2892
+ }
2893
+ const text = msg ?? currentMessage;
2894
+ if (isTTY) {
2895
+ clearLine();
2896
+ process2.stderr.write(`\u2718 ${text}
2897
+ `);
2898
+ } else if (!started) {
2899
+ process2.stderr.write(`${text}
2900
+ `);
2901
+ }
2902
+ started = false;
2903
+ },
2904
+ update(msg) {
2905
+ currentMessage = msg;
2906
+ if (!isTTY || !started) return;
2907
+ renderFrame();
2908
+ }
2909
+ };
2910
+ }
2911
+
2172
2912
  // src/utils/safe-path.ts
2173
2913
  import { resolve, normalize } from "path";
2174
2914
  function safePath(userPath) {
@@ -2229,16 +2969,16 @@ function sortResults(items, sortSpec) {
2229
2969
  }
2230
2970
 
2231
2971
  // src/commands/plugin-scaffold.ts
2232
- import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
2233
- import { join as join4 } from "path";
2972
+ import { mkdir as mkdir3, writeFile as writeFile4 } from "fs/promises";
2973
+ import { join as join5 } from "path";
2234
2974
  async function scaffoldPlugin(options) {
2235
2975
  const { name, dir, description = `GPC plugin: ${name}` } = options;
2236
2976
  const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
2237
2977
  const shortName = pluginName.replace(/^gpc-plugin-/, "");
2238
- const srcDir = join4(dir, "src");
2239
- const testDir = join4(dir, "tests");
2240
- await mkdir2(srcDir, { recursive: true });
2241
- await mkdir2(testDir, { recursive: true });
2978
+ const srcDir = join5(dir, "src");
2979
+ const testDir = join5(dir, "tests");
2980
+ await mkdir3(srcDir, { recursive: true });
2981
+ await mkdir3(testDir, { recursive: true });
2242
2982
  const files = [];
2243
2983
  const pkg = {
2244
2984
  name: pluginName,
@@ -2272,7 +3012,7 @@ async function scaffoldPlugin(options) {
2272
3012
  vitest: "^3.0.0"
2273
3013
  }
2274
3014
  };
2275
- await writeFile3(join4(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
3015
+ await writeFile4(join5(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
2276
3016
  files.push("package.json");
2277
3017
  const tsconfig = {
2278
3018
  compilerOptions: {
@@ -2288,7 +3028,7 @@ async function scaffoldPlugin(options) {
2288
3028
  },
2289
3029
  include: ["src"]
2290
3030
  };
2291
- await writeFile3(join4(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
3031
+ await writeFile4(join5(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
2292
3032
  files.push("tsconfig.json");
2293
3033
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
2294
3034
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -2321,7 +3061,7 @@ export const plugin = definePlugin({
2321
3061
  },
2322
3062
  });
2323
3063
  `;
2324
- await writeFile3(join4(srcDir, "index.ts"), srcContent);
3064
+ await writeFile4(join5(srcDir, "index.ts"), srcContent);
2325
3065
  files.push("src/index.ts");
2326
3066
  const testContent = `import { describe, it, expect, vi } from "vitest";
2327
3067
  import { plugin } from "../src/index";
@@ -2346,14 +3086,14 @@ describe("${pluginName}", () => {
2346
3086
  });
2347
3087
  });
2348
3088
  `;
2349
- await writeFile3(join4(testDir, "plugin.test.ts"), testContent);
3089
+ await writeFile4(join5(testDir, "plugin.test.ts"), testContent);
2350
3090
  files.push("tests/plugin.test.ts");
2351
3091
  return { dir, files };
2352
3092
  }
2353
3093
 
2354
3094
  // src/audit.ts
2355
- import { appendFile, chmod, mkdir as mkdir3 } from "fs/promises";
2356
- import { join as join5 } from "path";
3095
+ import { appendFile, chmod, mkdir as mkdir4 } from "fs/promises";
3096
+ import { join as join6 } from "path";
2357
3097
  var auditDir = null;
2358
3098
  function initAudit(configDir) {
2359
3099
  auditDir = configDir;
@@ -2361,8 +3101,8 @@ function initAudit(configDir) {
2361
3101
  async function writeAuditLog(entry) {
2362
3102
  if (!auditDir) return;
2363
3103
  try {
2364
- await mkdir3(auditDir, { recursive: true, mode: 448 });
2365
- const logPath = join5(auditDir, "audit.log");
3104
+ await mkdir4(auditDir, { recursive: true, mode: 448 });
3105
+ const logPath = join6(auditDir, "audit.log");
2366
3106
  const redactedEntry = redactAuditArgs(entry);
2367
3107
  const line = JSON.stringify(redactedEntry) + "\n";
2368
3108
  await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
@@ -2504,6 +3244,143 @@ async function sendWebhook(config, payload, target) {
2504
3244
  } catch {
2505
3245
  }
2506
3246
  }
3247
+
3248
+ // src/commands/internal-sharing.ts
3249
+ import { extname as extname4 } from "path";
3250
+ async function uploadInternalSharing(client, packageName, filePath, fileType) {
3251
+ const resolvedType = fileType ?? detectFileType(filePath);
3252
+ const validation = await validateUploadFile(filePath);
3253
+ if (!validation.valid) {
3254
+ throw new GpcError(
3255
+ `File validation failed:
3256
+ ${validation.errors.join("\n")}`,
3257
+ "INTERNAL_SHARING_INVALID_FILE",
3258
+ 2,
3259
+ "Check that the file is a valid AAB or APK and is not corrupted."
3260
+ );
3261
+ }
3262
+ let artifact;
3263
+ if (resolvedType === "bundle") {
3264
+ artifact = await client.internalAppSharing.uploadBundle(packageName, filePath);
3265
+ } else {
3266
+ artifact = await client.internalAppSharing.uploadApk(packageName, filePath);
3267
+ }
3268
+ return {
3269
+ downloadUrl: artifact.downloadUrl,
3270
+ sha256: artifact.sha256,
3271
+ certificateFingerprint: artifact.certificateFingerprint,
3272
+ fileType: resolvedType
3273
+ };
3274
+ }
3275
+ function detectFileType(filePath) {
3276
+ const ext = extname4(filePath).toLowerCase();
3277
+ if (ext === ".aab") return "bundle";
3278
+ if (ext === ".apk") return "apk";
3279
+ throw new GpcError(
3280
+ `Cannot detect file type from extension "${ext}". Use --type to specify bundle or apk.`,
3281
+ "INTERNAL_SHARING_UNKNOWN_TYPE",
3282
+ 2,
3283
+ "Use --type bundle for .aab files or --type apk for .apk files."
3284
+ );
3285
+ }
3286
+
3287
+ // src/commands/generated-apks.ts
3288
+ import { writeFile as writeFile5 } from "fs/promises";
3289
+ async function listGeneratedApks(client, packageName, versionCode) {
3290
+ if (!Number.isInteger(versionCode) || versionCode <= 0) {
3291
+ throw new GpcError(
3292
+ `Invalid version code: ${versionCode}`,
3293
+ "GENERATED_APKS_INVALID_VERSION",
3294
+ 2,
3295
+ "Provide a positive integer version code."
3296
+ );
3297
+ }
3298
+ return client.generatedApks.list(packageName, versionCode);
3299
+ }
3300
+ async function downloadGeneratedApk(client, packageName, versionCode, apkId, outputPath) {
3301
+ if (!Number.isInteger(versionCode) || versionCode <= 0) {
3302
+ throw new GpcError(
3303
+ `Invalid version code: ${versionCode}`,
3304
+ "GENERATED_APKS_INVALID_VERSION",
3305
+ 2,
3306
+ "Provide a positive integer version code."
3307
+ );
3308
+ }
3309
+ if (!apkId) {
3310
+ throw new GpcError(
3311
+ "APK ID is required",
3312
+ "GENERATED_APKS_MISSING_ID",
3313
+ 2,
3314
+ "Provide the generated APK ID. Use 'gpc generated-apks list <version-code>' to see available APKs."
3315
+ );
3316
+ }
3317
+ const buffer = await client.generatedApks.download(packageName, versionCode, apkId);
3318
+ const bytes = new Uint8Array(buffer);
3319
+ await writeFile5(outputPath, bytes);
3320
+ return { path: outputPath, sizeBytes: bytes.byteLength };
3321
+ }
3322
+
3323
+ // src/commands/purchase-options.ts
3324
+ async function listPurchaseOptions(client, packageName) {
3325
+ try {
3326
+ return await client.purchaseOptions.list(packageName);
3327
+ } catch (error) {
3328
+ throw new GpcError(
3329
+ `Failed to list purchase options: ${error instanceof Error ? error.message : String(error)}`,
3330
+ "PURCHASE_OPTIONS_LIST_FAILED",
3331
+ 4,
3332
+ "Check your package name and API permissions."
3333
+ );
3334
+ }
3335
+ }
3336
+ async function getPurchaseOption(client, packageName, purchaseOptionId) {
3337
+ try {
3338
+ return await client.purchaseOptions.get(packageName, purchaseOptionId);
3339
+ } catch (error) {
3340
+ throw new GpcError(
3341
+ `Failed to get purchase option "${purchaseOptionId}": ${error instanceof Error ? error.message : String(error)}`,
3342
+ "PURCHASE_OPTION_GET_FAILED",
3343
+ 4,
3344
+ "Check that the purchase option ID exists."
3345
+ );
3346
+ }
3347
+ }
3348
+ async function createPurchaseOption(client, packageName, data) {
3349
+ try {
3350
+ return await client.purchaseOptions.create(packageName, data);
3351
+ } catch (error) {
3352
+ throw new GpcError(
3353
+ `Failed to create purchase option: ${error instanceof Error ? error.message : String(error)}`,
3354
+ "PURCHASE_OPTION_CREATE_FAILED",
3355
+ 4,
3356
+ "Check your purchase option data and API permissions."
3357
+ );
3358
+ }
3359
+ }
3360
+ async function activatePurchaseOption(client, packageName, purchaseOptionId) {
3361
+ try {
3362
+ return await client.purchaseOptions.activate(packageName, purchaseOptionId);
3363
+ } catch (error) {
3364
+ throw new GpcError(
3365
+ `Failed to activate purchase option "${purchaseOptionId}": ${error instanceof Error ? error.message : String(error)}`,
3366
+ "PURCHASE_OPTION_ACTIVATE_FAILED",
3367
+ 4,
3368
+ "Check that the purchase option exists and is in a valid state for activation."
3369
+ );
3370
+ }
3371
+ }
3372
+ async function deactivatePurchaseOption(client, packageName, purchaseOptionId) {
3373
+ try {
3374
+ return await client.purchaseOptions.deactivate(packageName, purchaseOptionId);
3375
+ } catch (error) {
3376
+ throw new GpcError(
3377
+ `Failed to deactivate purchase option "${purchaseOptionId}": ${error instanceof Error ? error.message : String(error)}`,
3378
+ "PURCHASE_OPTION_DEACTIVATE_FAILED",
3379
+ 4,
3380
+ "Check that the purchase option exists and is in a valid state for deactivation."
3381
+ );
3382
+ }
3383
+ }
2507
3384
  export {
2508
3385
  ApiError,
2509
3386
  ConfigError,
@@ -2515,7 +3392,10 @@ export {
2515
3392
  acknowledgeProductPurchase,
2516
3393
  activateBasePlan,
2517
3394
  activateOffer,
3395
+ activatePurchaseOption,
3396
+ addRecoveryTargeting,
2518
3397
  addTesters,
3398
+ batchSyncInAppProducts,
2519
3399
  cancelRecoveryAction,
2520
3400
  cancelSubscriptionPurchase,
2521
3401
  checkThreshold,
@@ -2523,40 +3403,59 @@ export {
2523
3403
  consumeProductPurchase,
2524
3404
  convertRegionPrices,
2525
3405
  createAuditEntry,
3406
+ createDeviceTier,
2526
3407
  createExternalTransaction,
2527
3408
  createInAppProduct,
2528
3409
  createOffer,
3410
+ createOneTimeOffer,
3411
+ createOneTimeProduct,
3412
+ createPurchaseOption,
3413
+ createRecoveryAction,
3414
+ createSpinner,
2529
3415
  createSubscription,
3416
+ createTrack,
2530
3417
  deactivateBasePlan,
2531
3418
  deactivateOffer,
3419
+ deactivatePurchaseOption,
2532
3420
  deferSubscriptionPurchase,
2533
3421
  deleteBasePlan,
2534
3422
  deleteImage,
2535
3423
  deleteInAppProduct,
2536
3424
  deleteListing,
2537
3425
  deleteOffer,
3426
+ deleteOneTimeOffer,
3427
+ deleteOneTimeProduct,
2538
3428
  deleteSubscription,
2539
3429
  deployRecoveryAction,
3430
+ detectFastlane,
2540
3431
  detectOutputFormat,
2541
3432
  diffListings,
2542
3433
  diffListingsCommand,
2543
3434
  discoverPlugins,
3435
+ downloadGeneratedApk,
2544
3436
  downloadReport,
2545
3437
  exportDataSafety,
3438
+ exportImages,
2546
3439
  exportReviews,
2547
3440
  formatCustomPayload,
2548
3441
  formatDiscordPayload,
3442
+ formatJunit,
2549
3443
  formatOutput,
2550
3444
  formatSlackPayload,
3445
+ generateMigrationPlan,
2551
3446
  generateNotesFromGit,
2552
3447
  getAppInfo,
2553
3448
  getCountryAvailability,
2554
3449
  getDataSafety,
3450
+ getDeviceTier,
2555
3451
  getExternalTransaction,
2556
3452
  getInAppProduct,
2557
3453
  getListings,
2558
3454
  getOffer,
3455
+ getOneTimeOffer,
3456
+ getOneTimeProduct,
2559
3457
  getProductPurchase,
3458
+ getPurchaseOption,
2560
3459
  getReleasesStatus,
2561
3460
  getReview,
2562
3461
  getSubscription,
@@ -2579,9 +3478,14 @@ export {
2579
3478
  isValidBcp47,
2580
3479
  isValidReportType,
2581
3480
  isValidStatsDimension,
3481
+ listDeviceTiers,
3482
+ listGeneratedApks,
2582
3483
  listImages,
2583
3484
  listInAppProducts,
2584
3485
  listOffers,
3486
+ listOneTimeOffers,
3487
+ listOneTimeProducts,
3488
+ listPurchaseOptions,
2585
3489
  listRecoveryActions,
2586
3490
  listReports,
2587
3491
  listReviews,
@@ -2591,6 +3495,8 @@ export {
2591
3495
  listUsers,
2592
3496
  listVoidedPurchases,
2593
3497
  migratePrices,
3498
+ parseAppfile,
3499
+ parseFastfile,
2594
3500
  parseGrantArg,
2595
3501
  parseMonth,
2596
3502
  promoteRelease,
@@ -2618,16 +3524,22 @@ export {
2618
3524
  updateInAppProduct,
2619
3525
  updateListing,
2620
3526
  updateOffer,
3527
+ updateOneTimeOffer,
3528
+ updateOneTimeProduct,
2621
3529
  updateRollout,
2622
3530
  updateSubscription,
3531
+ updateTrackConfig,
2623
3532
  updateUser,
3533
+ uploadExternallyHosted,
2624
3534
  uploadImage,
3535
+ uploadInternalSharing,
2625
3536
  uploadRelease,
2626
3537
  validateImage,
2627
3538
  validatePreSubmission,
2628
3539
  validateReleaseNotes,
2629
3540
  validateUploadFile,
2630
3541
  writeAuditLog,
2631
- writeListingsToDir
3542
+ writeListingsToDir,
3543
+ writeMigrationOutput
2632
3544
  };
2633
3545
  //# sourceMappingURL=index.js.map