@gpc-cli/core 0.9.78 → 0.10.0

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
@@ -443,7 +443,15 @@ function formatJunit(data, commandName = "command") {
443
443
  }
444
444
 
445
445
  // src/plugins.ts
446
- var FIRST_PARTY_PLUGINS = /* @__PURE__ */ new Set(["@gpc-cli/plugin-ci", "@gpc-cli/plugin-sdk"]);
446
+ import { readFile } from "fs/promises";
447
+ import { dirname, join, parse } from "path";
448
+ import { fileURLToPath } from "url";
449
+ import { resolvePluginApprovalId } from "@gpc-cli/config";
450
+ var FIRST_PARTY_PLUGINS = /* @__PURE__ */ new Set(["@gpc-cli/plugin-ci"]);
451
+ var discoveredPluginManifests = /* @__PURE__ */ new WeakMap();
452
+ function isFirstPartyPluginSpecifier(specifier) {
453
+ return FIRST_PARTY_PLUGINS.has(specifier);
454
+ }
447
455
  var PluginManager = class {
448
456
  plugins = [];
449
457
  beforeHandlers = [];
@@ -454,19 +462,34 @@ var PluginManager = class {
454
462
  registeredCommands = [];
455
463
  /** Load and register a plugin */
456
464
  async load(plugin, manifest) {
457
- const isTrusted = manifest?.trusted ?? FIRST_PARTY_PLUGINS.has(plugin.name);
458
- if (!isTrusted && manifest?.permissions) {
459
- validatePermissions(manifest.permissions);
460
- }
465
+ const effectiveManifest = manifest ?? discoveredPluginManifests.get(plugin);
466
+ const isTrusted = effectiveManifest ? effectiveManifest.trusted === true : FIRST_PARTY_PLUGINS.has(plugin.name);
467
+ if (!isTrusted && effectiveManifest) {
468
+ if (!effectiveManifest.permissions) {
469
+ throw new GpcError(
470
+ `Third-party plugin "${effectiveManifest.name}" must declare permissions in package.json under gpc.permissions`,
471
+ "PLUGIN_PERMISSIONS_REQUIRED",
472
+ 10,
473
+ "Declare only the lifecycle hooks the plugin needs, then reinstall or relink it."
474
+ );
475
+ }
476
+ validatePermissions(effectiveManifest.permissions);
477
+ }
478
+ const stagedBeforeHandlers = [];
479
+ const stagedAfterHandlers = [];
480
+ const stagedErrorHandlers = [];
481
+ const stagedBeforeRequestHandlers = [];
482
+ const stagedAfterResponseHandlers = [];
483
+ const stagedCommands = [];
461
484
  const allHooks = createHooks(
462
- this.beforeHandlers,
463
- this.afterHandlers,
464
- this.errorHandlers,
465
- this.beforeRequestHandlers,
466
- this.afterResponseHandlers,
467
- this.registeredCommands
485
+ stagedBeforeHandlers,
486
+ stagedAfterHandlers,
487
+ stagedErrorHandlers,
488
+ stagedBeforeRequestHandlers,
489
+ stagedAfterResponseHandlers,
490
+ stagedCommands
468
491
  );
469
- const hooks = isTrusted || !manifest?.permissions ? allHooks : createRestrictedHooks(allHooks, new Set(manifest.permissions));
492
+ const hooks = isTrusted || !effectiveManifest ? allHooks : createRestrictedHooks(allHooks, new Set(effectiveManifest.permissions));
470
493
  try {
471
494
  await plugin.register(hooks);
472
495
  } catch (err) {
@@ -474,9 +497,15 @@ var PluginManager = class {
474
497
  console.warn(`[gpc] Plugin "${plugin.name}" failed to register: ${msg}`);
475
498
  return;
476
499
  }
500
+ this.beforeHandlers.push(...stagedBeforeHandlers);
501
+ this.afterHandlers.push(...stagedAfterHandlers);
502
+ this.errorHandlers.push(...stagedErrorHandlers);
503
+ this.beforeRequestHandlers.push(...stagedBeforeRequestHandlers);
504
+ this.afterResponseHandlers.push(...stagedAfterResponseHandlers);
505
+ this.registeredCommands.push(...stagedCommands);
477
506
  this.plugins.push({
478
- name: plugin.name,
479
- version: plugin.version,
507
+ name: effectiveManifest?.name ?? plugin.name,
508
+ version: effectiveManifest?.version ?? plugin.version,
480
509
  trusted: isTrusted
481
510
  });
482
511
  }
@@ -489,7 +518,10 @@ var PluginManager = class {
489
518
  /** Run all afterCommand handlers */
490
519
  async runAfterCommand(event, result) {
491
520
  for (const handler of this.afterHandlers) {
492
- await handler(event, result);
521
+ try {
522
+ await handler(event, result);
523
+ } catch {
524
+ }
493
525
  }
494
526
  }
495
527
  /** Run all onError handlers */
@@ -577,6 +609,7 @@ var HOOK_PERMISSIONS = {
577
609
  afterResponse: "hooks:afterResponse",
578
610
  registerCommands: "commands:register"
579
611
  };
612
+ var LEGACY_PLUGIN_PERMISSIONS = Object.values(HOOK_PERMISSIONS);
580
613
  function createRestrictedHooks(inner, permissions) {
581
614
  function gate(key) {
582
615
  const requiredPerm = HOOK_PERMISSIONS[key];
@@ -621,31 +654,148 @@ function validatePermissions(permissions) {
621
654
  }
622
655
  }
623
656
  }
624
- function isPluginTrusted(specifier, approved) {
625
- if (specifier.startsWith("@gpc-cli/")) return true;
626
- return approved?.has(specifier) ?? false;
657
+ function isPluginTrusted(specifier, approvalId, approved) {
658
+ if (isFirstPartyPluginSpecifier(specifier)) return true;
659
+ return approved?.has(approvalId) ?? false;
627
660
  }
628
- async function discoverPlugins(options) {
661
+ async function discoverPluginEntries(options) {
629
662
  const plugins = [];
630
663
  const seen = /* @__PURE__ */ new Set();
631
- const approved = options?.approvedPlugins ? new Set(options.approvedPlugins) : void 0;
664
+ const cwd = options?.cwd;
665
+ const approved = options?.approvedPlugins ? new Set(options.approvedPlugins.map((name) => resolvePluginApprovalId(name, cwd))) : void 0;
666
+ const legacyApproved = new Set(
667
+ (options?.legacyApprovedPlugins ?? []).map((name) => resolvePluginApprovalId(name, cwd))
668
+ );
632
669
  if (options?.configPlugins) {
633
670
  for (const name of options.configPlugins) {
634
- if (seen.has(name)) continue;
635
- if (!isPluginTrusted(name, approved)) continue;
671
+ const resolvedSpecifier = resolvePluginApprovalId(name, cwd);
672
+ if (seen.has(resolvedSpecifier)) continue;
673
+ if (!isPluginTrusted(name, resolvedSpecifier, approved)) continue;
636
674
  try {
637
- const mod = await import(name);
675
+ const manifestInfo = await readPluginManifest(resolvedSpecifier, options?.resolveSpecifier);
676
+ const claimsFirstParty = isFirstPartyPluginSpecifier(name);
677
+ const trusted = claimsFirstParty && manifestInfo.packageFound && manifestInfo.manifest.name === name;
678
+ if (claimsFirstParty && !trusted) {
679
+ throw new GpcError(
680
+ `Configured first-party plugin "${name}" resolved to package "${manifestInfo.manifest.name}"`,
681
+ "PLUGIN_IDENTITY_MISMATCH",
682
+ 10,
683
+ "Remove npm aliases or replacements for first-party GPC plugin packages."
684
+ );
685
+ }
686
+ if (!trusted && manifestInfo.legacyPermissions && !legacyApproved.has(resolvedSpecifier)) {
687
+ throw new GpcError(
688
+ `Third-party plugin "${manifestInfo.manifest.name}" must declare permissions in package.json under gpc.permissions`,
689
+ "PLUGIN_PERMISSIONS_REQUIRED",
690
+ 10,
691
+ "Ask the plugin author to publish an explicit permission list before approving it."
692
+ );
693
+ }
694
+ if (!trusted) validatePermissions(manifestInfo.manifest.permissions ?? []);
695
+ const mod = await import(resolvedSpecifier);
638
696
  const plugin = resolvePlugin(mod);
639
697
  if (plugin) {
640
- plugins.push(plugin);
641
- seen.add(name);
698
+ const manifest = trusted ? {
699
+ name: manifestInfo.manifest.name,
700
+ version: manifestInfo.manifest.version,
701
+ trusted: true
702
+ } : manifestInfo.manifest;
703
+ const entry = {
704
+ plugin,
705
+ manifest,
706
+ legacyPermissions: manifestInfo?.legacyPermissions ?? false
707
+ };
708
+ discoveredPluginManifests.set(plugin, entry.manifest);
709
+ plugins.push(entry);
710
+ seen.add(resolvedSpecifier);
711
+ }
712
+ } catch (error) {
713
+ if (error instanceof GpcError && error.code.startsWith("PLUGIN_")) {
714
+ console.warn(`[gpc] Skipping plugin "${name}": ${error.message}`);
642
715
  }
643
- } catch {
644
716
  }
645
717
  }
646
718
  }
647
719
  return plugins;
648
720
  }
721
+ async function discoverPlugins(options) {
722
+ const compatibilityOptions = options?.legacyApprovedPlugins === void 0 ? { ...options, legacyApprovedPlugins: options?.approvedPlugins } : options;
723
+ const entries = await discoverPluginEntries(compatibilityOptions);
724
+ return entries.map(({ plugin }) => plugin);
725
+ }
726
+ async function validatePluginForApproval(specifier, cwd) {
727
+ const resolvedSpecifier = resolvePluginApprovalId(specifier, cwd);
728
+ const manifestInfo = await readPluginManifest(resolvedSpecifier);
729
+ if (manifestInfo.legacyPermissions) {
730
+ throw new GpcError(
731
+ `Third-party plugin "${manifestInfo.manifest.name}" must declare permissions in package.json under gpc.permissions`,
732
+ "PLUGIN_PERMISSIONS_REQUIRED",
733
+ 10,
734
+ "Ask the plugin author to publish an explicit permission list before approving it."
735
+ );
736
+ }
737
+ validatePermissions(manifestInfo.manifest.permissions ?? []);
738
+ return { specifier: resolvedSpecifier, manifest: manifestInfo.manifest };
739
+ }
740
+ async function readPluginManifest(specifier, resolveSpecifier) {
741
+ const { packageJson, adjacent } = await findPackageJson(specifier, resolveSpecifier);
742
+ const gpc = packageJson?.["gpc"] && typeof packageJson["gpc"] === "object" ? packageJson["gpc"] : void 0;
743
+ const permissionDeclaration = gpc?.["permissions"];
744
+ const declaresPluginMetadata = permissionDeclaration !== void 0;
745
+ const identifyByPath = specifier.startsWith("file:") && !declaresPluginMetadata && !adjacent;
746
+ const packageName = !identifyByPath && typeof packageJson?.["name"] === "string" ? packageJson["name"] : specifier;
747
+ const packageVersion = !identifyByPath && typeof packageJson?.["version"] === "string" ? packageJson["version"] : "unknown";
748
+ if (permissionDeclaration !== void 0 && !Array.isArray(permissionDeclaration)) {
749
+ throw new GpcError(
750
+ `Plugin "${packageName}" must declare gpc.permissions as an array`,
751
+ "PLUGIN_INVALID_PERMISSION",
752
+ 10,
753
+ "Declare permissions as a JSON array of supported permission names."
754
+ );
755
+ }
756
+ const permissions = permissionDeclaration;
757
+ return {
758
+ manifest: {
759
+ name: packageName,
760
+ version: packageVersion,
761
+ permissions: permissions ?? [...LEGACY_PLUGIN_PERMISSIONS],
762
+ trusted: false
763
+ },
764
+ legacyPermissions: permissions === void 0,
765
+ packageFound: packageJson !== void 0
766
+ };
767
+ }
768
+ async function findPackageJson(specifier, resolveSpecifier = import.meta.resolve) {
769
+ let resolved;
770
+ try {
771
+ resolved = resolveSpecifier(specifier);
772
+ } catch {
773
+ return { adjacent: false };
774
+ }
775
+ if (!resolved.startsWith("file:")) return { adjacent: false };
776
+ const moduleDirectory = dirname(fileURLToPath(resolved));
777
+ let directory = moduleDirectory;
778
+ const root = parse(directory).root;
779
+ while (directory !== root) {
780
+ let contents;
781
+ try {
782
+ contents = await readFile(join(directory, "package.json"), "utf8");
783
+ } catch (error) {
784
+ if (error.code !== "ENOENT") return { adjacent: false };
785
+ directory = dirname(directory);
786
+ continue;
787
+ }
788
+ try {
789
+ return {
790
+ packageJson: JSON.parse(contents),
791
+ adjacent: directory === moduleDirectory
792
+ };
793
+ } catch {
794
+ return { adjacent: false };
795
+ }
796
+ }
797
+ return { adjacent: false };
798
+ }
649
799
  function resolvePlugin(mod) {
650
800
  if (!mod || typeof mod !== "object") return void 0;
651
801
  const m = mod;
@@ -820,8 +970,8 @@ async function validateImage(filePath, imageType) {
820
970
  }
821
971
 
822
972
  // src/utils/fastlane.ts
823
- import { readFile, writeFile, mkdir, readdir, stat as stat2 } from "fs/promises";
824
- import { join } from "path";
973
+ import { readFile as readFile2, writeFile, mkdir, readdir, stat as stat2 } from "fs/promises";
974
+ import { join as join2 } from "path";
825
975
  var FILE_MAP = {
826
976
  "title.txt": "title",
827
977
  "short_description.txt": "shortDescription",
@@ -846,7 +996,7 @@ async function readListingsFromDir(dir) {
846
996
  const SAFE_LANG2 = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
847
997
  for (const lang of entries) {
848
998
  if (!SAFE_LANG2.test(lang)) continue;
849
- const langDir = join(dir, lang);
999
+ const langDir = join2(dir, lang);
850
1000
  const langStat = await stat2(langDir);
851
1001
  if (!langStat.isDirectory()) continue;
852
1002
  const listing = {
@@ -856,9 +1006,9 @@ async function readListingsFromDir(dir) {
856
1006
  fullDescription: ""
857
1007
  };
858
1008
  for (const [fileName, field] of Object.entries(FILE_MAP)) {
859
- const filePath = join(langDir, fileName);
1009
+ const filePath = join2(langDir, fileName);
860
1010
  if (await exists(filePath)) {
861
- const content = await readFile(filePath, "utf-8");
1011
+ const content = await readFile2(filePath, "utf-8");
862
1012
  listing[field] = content.trimEnd();
863
1013
  }
864
1014
  }
@@ -868,12 +1018,12 @@ async function readListingsFromDir(dir) {
868
1018
  }
869
1019
  async function writeListingsToDir(dir, listings) {
870
1020
  for (const listing of listings) {
871
- const langDir = join(dir, listing.language);
1021
+ const langDir = join2(dir, listing.language);
872
1022
  await mkdir(langDir, { recursive: true });
873
1023
  for (const [field, fileName] of Object.entries(FIELD_TO_FILE)) {
874
1024
  const value = listing[field];
875
1025
  if (value !== void 0 && value !== "") {
876
- await writeFile(join(langDir, fileName), value + "\n", "utf-8");
1026
+ await writeFile(join2(langDir, fileName), value + "\n", "utf-8");
877
1027
  }
878
1028
  }
879
1029
  }
@@ -1248,7 +1398,7 @@ var ALL_IMAGE_TYPES = [
1248
1398
  ];
1249
1399
  async function exportImages(client, packageName, dir, options) {
1250
1400
  const { mkdir: mkdir9, writeFile: writeFile10 } = await import("fs/promises");
1251
- const { join: join13 } = await import("path");
1401
+ const { join: join14 } = await import("path");
1252
1402
  const edit = await client.edits.insert(packageName);
1253
1403
  try {
1254
1404
  let languages;
@@ -1279,7 +1429,7 @@ async function exportImages(client, packageName, dir, options) {
1279
1429
  const batch = tasks.slice(i, i + concurrency);
1280
1430
  const results = await Promise.all(
1281
1431
  batch.map(async (task) => {
1282
- const dirPath = join13(dir, task.language, task.imageType);
1432
+ const dirPath = join14(dir, task.language, task.imageType);
1283
1433
  await mkdir9(dirPath, { recursive: true });
1284
1434
  const response = await fetch(task.url);
1285
1435
  if (!response.ok) {
@@ -1291,7 +1441,7 @@ async function exportImages(client, packageName, dir, options) {
1291
1441
  );
1292
1442
  }
1293
1443
  const buffer = Buffer.from(await response.arrayBuffer());
1294
- const filePath = join13(dirPath, `${task.index}.png`);
1444
+ const filePath = join14(dirPath, `${task.index}.png`);
1295
1445
  await writeFile10(filePath, buffer);
1296
1446
  return buffer.length;
1297
1447
  })
@@ -1327,8 +1477,8 @@ async function updateAppDetails(client, packageName, details, commitOptions) {
1327
1477
  }
1328
1478
 
1329
1479
  // src/commands/migrate.ts
1330
- import { readdir as readdir2, readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
1331
- import { join as join2 } from "path";
1480
+ import { readdir as readdir2, readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2, access } from "fs/promises";
1481
+ import { join as join3 } from "path";
1332
1482
  var COMPLEX_RUBY_RE = /\b(begin|rescue|ensure|if |unless |case |while |until |for )\b/;
1333
1483
  async function fileExists(path) {
1334
1484
  try {
@@ -1348,14 +1498,14 @@ async function detectFastlane(cwd) {
1348
1498
  metadataLanguages: [],
1349
1499
  parseWarnings: []
1350
1500
  };
1351
- const fastlaneDir = join2(cwd, "fastlane");
1501
+ const fastlaneDir = join3(cwd, "fastlane");
1352
1502
  const hasFastlaneDir = await fileExists(fastlaneDir);
1353
- const fastfilePath = hasFastlaneDir ? join2(fastlaneDir, "Fastfile") : join2(cwd, "Fastfile");
1354
- const appfilePath = hasFastlaneDir ? join2(fastlaneDir, "Appfile") : join2(cwd, "Appfile");
1503
+ const fastfilePath = hasFastlaneDir ? join3(fastlaneDir, "Fastfile") : join3(cwd, "Fastfile");
1504
+ const appfilePath = hasFastlaneDir ? join3(fastlaneDir, "Appfile") : join3(cwd, "Appfile");
1355
1505
  result.hasFastfile = await fileExists(fastfilePath);
1356
1506
  result.hasAppfile = await fileExists(appfilePath);
1357
- result.hasGemfile = await fileExists(join2(cwd, "Gemfile"));
1358
- const metadataDir = hasFastlaneDir ? join2(fastlaneDir, "metadata", "android") : join2(cwd, "metadata", "android");
1507
+ result.hasGemfile = await fileExists(join3(cwd, "Gemfile"));
1508
+ const metadataDir = hasFastlaneDir ? join3(fastlaneDir, "metadata", "android") : join3(cwd, "metadata", "android");
1359
1509
  result.hasMetadata = await fileExists(metadataDir);
1360
1510
  if (result.hasMetadata) {
1361
1511
  try {
@@ -1367,7 +1517,7 @@ async function detectFastlane(cwd) {
1367
1517
  }
1368
1518
  if (result.hasFastfile) {
1369
1519
  try {
1370
- const content = await readFile2(fastfilePath, "utf-8");
1520
+ const content = await readFile3(fastfilePath, "utf-8");
1371
1521
  result.lanes = parseFastfile(content);
1372
1522
  if (COMPLEX_RUBY_RE.test(content)) {
1373
1523
  result.parseWarnings.push(
@@ -1380,7 +1530,7 @@ async function detectFastlane(cwd) {
1380
1530
  }
1381
1531
  if (result.hasAppfile) {
1382
1532
  try {
1383
- const content = await readFile2(appfilePath, "utf-8");
1533
+ const content = await readFile3(appfilePath, "utf-8");
1384
1534
  const parsed = parseAppfile(content);
1385
1535
  result.packageName = parsed.packageName;
1386
1536
  result.jsonKeyPath = parsed.jsonKeyPath;
@@ -1500,11 +1650,11 @@ async function writeMigrationOutput(result, dir) {
1500
1650
  await mkdir2(dir, { recursive: true });
1501
1651
  const files = [];
1502
1652
  if (Object.keys(result.config).length > 0) {
1503
- const configPath = join2(dir, ".gpcrc.json");
1653
+ const configPath = join3(dir, ".gpcrc.json");
1504
1654
  await writeFile2(configPath, JSON.stringify(result.config, null, 2) + "\n", "utf-8");
1505
1655
  files.push(configPath);
1506
1656
  }
1507
- const migrationPath = join2(dir, "MIGRATION.md");
1657
+ const migrationPath = join3(dir, "MIGRATION.md");
1508
1658
  const lines = [
1509
1659
  "# Fastlane to GPC Migration",
1510
1660
  "",
@@ -2796,17 +2946,17 @@ function watchVitalsWithAutoHalt(reporting, packageName, options) {
2796
2946
  }
2797
2947
 
2798
2948
  // src/commands/status.ts
2799
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
2949
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
2800
2950
  import { execFile } from "child_process";
2801
- import { join as join3 } from "path";
2951
+ import { join as join4 } from "path";
2802
2952
  import { getCacheDir } from "@gpc-cli/config";
2803
2953
  var DEFAULT_TTL_SECONDS = 3600;
2804
2954
  function cacheFilePath(packageName) {
2805
- return join3(getCacheDir(), `status-${packageName}.json`);
2955
+ return join4(getCacheDir(), `status-${packageName}.json`);
2806
2956
  }
2807
2957
  async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
2808
2958
  try {
2809
- const raw = await readFile3(cacheFilePath(packageName), "utf-8");
2959
+ const raw = await readFile4(cacheFilePath(packageName), "utf-8");
2810
2960
  const entry = JSON.parse(raw);
2811
2961
  const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
2812
2962
  if (age > (entry.ttl ?? ttlSeconds)) return null;
@@ -3265,13 +3415,13 @@ async function runWatchLoop(opts) {
3265
3415
  }
3266
3416
  }
3267
3417
  function breachStateFilePath(packageName) {
3268
- return join3(getCacheDir(), `breach-state-${packageName}.json`);
3418
+ return join4(getCacheDir(), `breach-state-${packageName}.json`);
3269
3419
  }
3270
3420
  async function trackBreachState(packageName, isBreaching) {
3271
3421
  const filePath = breachStateFilePath(packageName);
3272
3422
  let prevBreaching = false;
3273
3423
  try {
3274
- const raw = await readFile3(filePath, "utf-8");
3424
+ const raw = await readFile4(filePath, "utf-8");
3275
3425
  prevBreaching = JSON.parse(raw).breaching;
3276
3426
  } catch {
3277
3427
  }
@@ -3548,8 +3698,8 @@ async function runWatch(client, reporting, config, callbacks) {
3548
3698
  }
3549
3699
 
3550
3700
  // src/commands/iap.ts
3551
- import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
3552
- import { join as join4 } from "path";
3701
+ import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
3702
+ import { join as join5 } from "path";
3553
3703
  import { paginateAll as paginateAll3 } from "@gpc-cli/api";
3554
3704
  async function listInAppProducts(client, packageName, options) {
3555
3705
  if (options?.limit || options?.nextPage) {
@@ -3638,7 +3788,7 @@ async function readProductsFromDir(dir) {
3638
3788
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
3639
3789
  const localProducts = [];
3640
3790
  for (const file of jsonFiles) {
3641
- const content = await readFile4(join4(dir, file), "utf-8");
3791
+ const content = await readFile5(join5(dir, file), "utf-8");
3642
3792
  try {
3643
3793
  localProducts.push(JSON.parse(content));
3644
3794
  } catch {
@@ -4376,7 +4526,7 @@ async function deleteGrant(client, developerId, email, packageName) {
4376
4526
  }
4377
4527
 
4378
4528
  // src/commands/testers.ts
4379
- import { readFile as readFile5 } from "fs/promises";
4529
+ import { readFile as readFile6 } from "fs/promises";
4380
4530
  async function listTesters(client, packageName, track) {
4381
4531
  const edit = await client.edits.insert(packageName);
4382
4532
  try {
@@ -4423,7 +4573,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
4423
4573
  }
4424
4574
  }
4425
4575
  async function importTestersFromCsv(client, packageName, track, csvPath, commitOptions) {
4426
- const content = await readFile5(csvPath, "utf-8");
4576
+ const content = await readFile6(csvPath, "utf-8");
4427
4577
  const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
4428
4578
  if (emails.length === 0) {
4429
4579
  throw new GpcError(
@@ -4566,7 +4716,7 @@ async function addRecoveryTargeting(client, packageName, actionId, targeting) {
4566
4716
  }
4567
4717
 
4568
4718
  // src/commands/data-safety.ts
4569
- import { readFile as readFile6, stat as stat4 } from "fs/promises";
4719
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
4570
4720
  var MAX_CSV_BYTES = 1 * 1024 * 1024;
4571
4721
  async function updateDataSafety(client, packageName, data) {
4572
4722
  return client.dataSafety.update(packageName, data);
@@ -4599,7 +4749,7 @@ async function importDataSafety(client, packageName, filePath) {
4599
4749
  "The CSV should be a few KB. Check that you are passing the correct file."
4600
4750
  );
4601
4751
  }
4602
- const csvContent = await readFile6(filePath, "utf-8");
4752
+ const csvContent = await readFile7(filePath, "utf-8");
4603
4753
  return updateDataSafety(client, packageName, { safetyLabels: csvContent });
4604
4754
  }
4605
4755
 
@@ -4899,16 +5049,16 @@ function createSpinner(message) {
4899
5049
  }
4900
5050
 
4901
5051
  // src/utils/train-state.ts
4902
- import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
4903
- import { dirname, join as join5 } from "path";
5052
+ import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
5053
+ import { dirname as dirname2, join as join6 } from "path";
4904
5054
  import { getCacheDir as getCacheDir2 } from "@gpc-cli/config";
4905
5055
  function stateFile(packageName) {
4906
- return join5(getCacheDir2(), `train-${packageName}.json`);
5056
+ return join6(getCacheDir2(), `train-${packageName}.json`);
4907
5057
  }
4908
5058
  async function readTrainState(packageName) {
4909
5059
  const path = stateFile(packageName);
4910
5060
  try {
4911
- const raw = await readFile7(path, "utf-8");
5061
+ const raw = await readFile8(path, "utf-8");
4912
5062
  return JSON.parse(raw);
4913
5063
  } catch {
4914
5064
  return null;
@@ -4916,7 +5066,7 @@ async function readTrainState(packageName) {
4916
5066
  }
4917
5067
  async function writeTrainState(packageName, state) {
4918
5068
  const path = stateFile(packageName);
4919
- const dir = dirname(path);
5069
+ const dir = dirname2(path);
4920
5070
  await mkdir4(dir, { recursive: true });
4921
5071
  await writeFile4(path, JSON.stringify(state, null, 2), "utf-8");
4922
5072
  }
@@ -5062,8 +5212,8 @@ async function executeStage(apiClient, packageName, state, stageIndex) {
5062
5212
  }
5063
5213
 
5064
5214
  // src/commands/games.ts
5065
- import { mkdir as mkdir5, readdir as readdir4, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
5066
- import { extname as extname2, join as join6 } from "path";
5215
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
5216
+ import { extname as extname2, join as join7 } from "path";
5067
5217
  async function listLeaderboards(client, packageName) {
5068
5218
  const result = await client.leaderboards.list(packageName);
5069
5219
  return result.items ?? [];
@@ -5207,7 +5357,7 @@ async function readConfigDir(dir) {
5207
5357
  const entries = (await readdir4(dir)).filter((f) => f.toLowerCase().endsWith(".json"));
5208
5358
  const out = [];
5209
5359
  for (const file of entries) {
5210
- const raw = await readFile8(join6(dir, file), "utf-8");
5360
+ const raw = await readFile9(join7(dir, file), "utf-8");
5211
5361
  let parsed;
5212
5362
  try {
5213
5363
  parsed = JSON.parse(raw);
@@ -5260,7 +5410,7 @@ async function pullAchievementConfigs(client, applicationId, dir) {
5260
5410
  for (const config of result.items ?? []) {
5261
5411
  const id = config.id ?? "unknown";
5262
5412
  await writeFile5(
5263
- join6(dir, safeConfigFilename(id)),
5413
+ join7(dir, safeConfigFilename(id)),
5264
5414
  JSON.stringify(config, null, 2) + "\n",
5265
5415
  "utf-8"
5266
5416
  );
@@ -5299,7 +5449,7 @@ async function pullLeaderboardConfigs(client, applicationId, dir) {
5299
5449
  for (const config of result.items ?? []) {
5300
5450
  const id = config.id ?? "unknown";
5301
5451
  await writeFile5(
5302
- join6(dir, safeConfigFilename(id)),
5452
+ join7(dir, safeConfigFilename(id)),
5303
5453
  JSON.stringify(config, null, 2) + "\n",
5304
5454
  "utf-8"
5305
5455
  );
@@ -5323,8 +5473,8 @@ async function publishEnterpriseApp(client, params) {
5323
5473
  }
5324
5474
 
5325
5475
  // src/audit.ts
5326
- import { appendFile, chmod, mkdir as mkdir6, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
5327
- import { join as join7 } from "path";
5476
+ import { appendFile, chmod, mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
5477
+ import { join as join8 } from "path";
5328
5478
  var auditDir = null;
5329
5479
  function initAudit(configDir) {
5330
5480
  auditDir = configDir;
@@ -5333,7 +5483,7 @@ async function writeAuditLog(entry) {
5333
5483
  if (!auditDir) return;
5334
5484
  try {
5335
5485
  await mkdir6(auditDir, { recursive: true, mode: 448 });
5336
- const logPath = join7(auditDir, "audit.log");
5486
+ const logPath = join8(auditDir, "audit.log");
5337
5487
  const redactedEntry = redactAuditArgs(entry);
5338
5488
  const line = JSON.stringify(redactedEntry) + "\n";
5339
5489
  await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
@@ -5388,10 +5538,10 @@ function createAuditEntry(command, args, app) {
5388
5538
  }
5389
5539
  async function listAuditEvents(options) {
5390
5540
  if (!auditDir) return [];
5391
- const logPath = join7(auditDir, "audit.log");
5541
+ const logPath = join8(auditDir, "audit.log");
5392
5542
  let content;
5393
5543
  try {
5394
- content = await readFile9(logPath, "utf-8");
5544
+ content = await readFile10(logPath, "utf-8");
5395
5545
  } catch {
5396
5546
  return [];
5397
5547
  }
@@ -5426,10 +5576,10 @@ async function searchAuditEvents(query) {
5426
5576
  }
5427
5577
  async function clearAuditLog(options) {
5428
5578
  if (!auditDir) return { deleted: 0, remaining: 0 };
5429
- const logPath = join7(auditDir, "audit.log");
5579
+ const logPath = join8(auditDir, "audit.log");
5430
5580
  let content;
5431
5581
  try {
5432
- content = await readFile9(logPath, "utf-8");
5582
+ content = await readFile10(logPath, "utf-8");
5433
5583
  } catch {
5434
5584
  return { deleted: 0, remaining: 0 };
5435
5585
  }
@@ -5518,7 +5668,7 @@ function safePathWithin(userPath, baseDir) {
5518
5668
 
5519
5669
  // src/commands/init.ts
5520
5670
  import { mkdir as mkdir7, writeFile as writeFile7, stat as stat5 } from "fs/promises";
5521
- import { join as join8 } from "path";
5671
+ import { join as join9 } from "path";
5522
5672
  async function exists2(path) {
5523
5673
  try {
5524
5674
  await stat5(path);
@@ -5553,7 +5703,7 @@ async function initProject(options) {
5553
5703
  null,
5554
5704
  2
5555
5705
  );
5556
- await safeWrite(join8(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
5706
+ await safeWrite(join9(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
5557
5707
  const preflightrc = JSON.stringify(
5558
5708
  {
5559
5709
  failOn: "error",
@@ -5567,27 +5717,27 @@ async function initProject(options) {
5567
5717
  2
5568
5718
  );
5569
5719
  await safeWrite(
5570
- join8(dir, ".preflightrc.json"),
5720
+ join9(dir, ".preflightrc.json"),
5571
5721
  preflightrc + "\n",
5572
5722
  created,
5573
5723
  skipped,
5574
5724
  skipExisting
5575
5725
  );
5576
- const metaDir = join8(dir, "metadata", "android", "en-US");
5577
- await safeWrite(join8(metaDir, "title.txt"), "", created, skipped, skipExisting);
5578
- await safeWrite(join8(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
5579
- await safeWrite(join8(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
5580
- await safeWrite(join8(metaDir, "video.txt"), "", created, skipped, skipExisting);
5581
- const ssDir = join8(metaDir, "images", "phoneScreenshots");
5726
+ const metaDir = join9(dir, "metadata", "android", "en-US");
5727
+ await safeWrite(join9(metaDir, "title.txt"), "", created, skipped, skipExisting);
5728
+ await safeWrite(join9(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
5729
+ await safeWrite(join9(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
5730
+ await safeWrite(join9(metaDir, "video.txt"), "", created, skipped, skipExisting);
5731
+ const ssDir = join9(metaDir, "images", "phoneScreenshots");
5582
5732
  await mkdir7(ssDir, { recursive: true });
5583
- await safeWrite(join8(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
5733
+ await safeWrite(join9(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
5584
5734
  if (ci === "github") {
5585
5735
  const workflow = githubActionsTemplate(pkg);
5586
- const workflowDir = join8(dir, ".github", "workflows");
5587
- await safeWrite(join8(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
5736
+ const workflowDir = join9(dir, ".github", "workflows");
5737
+ await safeWrite(join9(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
5588
5738
  } else if (ci === "gitlab") {
5589
5739
  const pipeline = gitlabCiTemplate(pkg);
5590
- await safeWrite(join8(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
5740
+ await safeWrite(join9(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
5591
5741
  }
5592
5742
  return { created, skipped };
5593
5743
  }
@@ -5683,13 +5833,13 @@ var DEFAULT_PREFLIGHT_CONFIG = {
5683
5833
  };
5684
5834
 
5685
5835
  // src/preflight/config.ts
5686
- import { readFile as readFile10 } from "fs/promises";
5836
+ import { readFile as readFile11 } from "fs/promises";
5687
5837
  var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "error", "warning", "info"]);
5688
5838
  async function loadPreflightConfig(configPath) {
5689
5839
  const path = configPath || ".preflightrc.json";
5690
5840
  let raw;
5691
5841
  try {
5692
- raw = await readFile10(path, "utf-8");
5842
+ raw = await readFile11(path, "utf-8");
5693
5843
  } catch {
5694
5844
  return { ...DEFAULT_PREFLIGHT_CONFIG };
5695
5845
  }
@@ -5734,7 +5884,7 @@ async function loadPreflightConfig(configPath) {
5734
5884
  }
5735
5885
 
5736
5886
  // src/preflight/aab-reader.ts
5737
- import { readFile as readFile11 } from "fs/promises";
5887
+ import { readFile as readFile12 } from "fs/promises";
5738
5888
  import { fromBuffer as fromBuffer2 } from "yauzl";
5739
5889
 
5740
5890
  // src/preflight/manifest-parser.ts
@@ -5932,7 +6082,7 @@ async function readAab(aabPath) {
5932
6082
  const manifestPath = detectManifestPath(aabPath);
5933
6083
  let buffer;
5934
6084
  try {
5935
- buffer = await readFile11(aabPath);
6085
+ buffer = await readFile12(aabPath);
5936
6086
  } catch (err) {
5937
6087
  const msg = err instanceof Error ? err.message : String(err);
5938
6088
  throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
@@ -6656,8 +6806,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
6656
6806
  }
6657
6807
 
6658
6808
  // src/preflight/scanners/metadata-scanner.ts
6659
- import { readdir as readdir5, stat as stat6, readFile as readFile12 } from "fs/promises";
6660
- import { join as join9 } from "path";
6809
+ import { readdir as readdir5, stat as stat6, readFile as readFile13 } from "fs/promises";
6810
+ import { join as join10 } from "path";
6661
6811
  var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
6662
6812
  var FILE_MAP2 = {
6663
6813
  "title.txt": "title",
@@ -6708,14 +6858,14 @@ var metadataScanner = {
6708
6858
  return findings;
6709
6859
  }
6710
6860
  for (const lang of locales) {
6711
- const langDir = join9(dir, lang);
6861
+ const langDir = join10(dir, lang);
6712
6862
  const langStat = await stat6(langDir).catch(() => null);
6713
6863
  if (!langStat?.isDirectory()) continue;
6714
6864
  const fields = {};
6715
6865
  for (const [fileName, field] of Object.entries(FILE_MAP2)) {
6716
- const filePath = join9(langDir, fileName);
6866
+ const filePath = join10(langDir, fileName);
6717
6867
  try {
6718
- const content = await readFile12(filePath, "utf-8");
6868
+ const content = await readFile13(filePath, "utf-8");
6719
6869
  fields[field] = content.trimEnd();
6720
6870
  } catch {
6721
6871
  }
@@ -6754,7 +6904,7 @@ var metadataScanner = {
6754
6904
  let totalScreenshots = 0;
6755
6905
  let phoneScreenshots = 0;
6756
6906
  for (const ssDir of SCREENSHOT_DIRS) {
6757
- const ssPath = join9(langDir, "images", ssDir);
6907
+ const ssPath = join10(langDir, "images", ssDir);
6758
6908
  try {
6759
6909
  const ssEntries = await readdir5(ssPath);
6760
6910
  const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
@@ -6786,9 +6936,9 @@ var metadataScanner = {
6786
6936
  }
6787
6937
  }
6788
6938
  const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
6789
- const privacyPath = join9(dir, defaultLang, "privacy_policy_url.txt");
6939
+ const privacyPath = join10(dir, defaultLang, "privacy_policy_url.txt");
6790
6940
  try {
6791
- const url = await readFile12(privacyPath, "utf-8");
6941
+ const url = await readFile13(privacyPath, "utf-8");
6792
6942
  if (!url.trim()) throw new Error("empty");
6793
6943
  } catch {
6794
6944
  findings.push({
@@ -6813,11 +6963,11 @@ var metadataScanner = {
6813
6963
  };
6814
6964
 
6815
6965
  // src/preflight/scanners/secrets-scanner.ts
6816
- import { readFile as readFile13 } from "fs/promises";
6966
+ import { readFile as readFile14 } from "fs/promises";
6817
6967
 
6818
6968
  // src/preflight/scan-files.ts
6819
6969
  import { readdir as readdir6, stat as stat7 } from "fs/promises";
6820
- import { join as join10, extname as extname3 } from "path";
6970
+ import { join as join11, extname as extname3 } from "path";
6821
6971
  var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
6822
6972
  ".git",
6823
6973
  "node_modules",
@@ -6840,7 +6990,7 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
6840
6990
  }
6841
6991
  for (const entry of entries) {
6842
6992
  if (skipDirs.has(entry)) continue;
6843
- const fullPath = join10(dir, entry);
6993
+ const fullPath = join11(dir, entry);
6844
6994
  const s = await stat7(fullPath).catch(() => null);
6845
6995
  if (!s) continue;
6846
6996
  if (s.isDirectory()) {
@@ -6933,7 +7083,7 @@ var secretsScanner = {
6933
7083
  for (const filePath of files) {
6934
7084
  let content;
6935
7085
  try {
6936
- content = await readFile13(filePath, "utf-8");
7086
+ content = await readFile14(filePath, "utf-8");
6937
7087
  } catch {
6938
7088
  continue;
6939
7089
  }
@@ -6961,7 +7111,7 @@ var secretsScanner = {
6961
7111
  };
6962
7112
 
6963
7113
  // src/preflight/scanners/billing-scanner.ts
6964
- import { readFile as readFile14 } from "fs/promises";
7114
+ import { readFile as readFile15 } from "fs/promises";
6965
7115
  var BILLING_PATTERNS = [
6966
7116
  {
6967
7117
  ruleId: "billing-stripe-sdk",
@@ -7022,7 +7172,7 @@ var billingScanner = {
7022
7172
  for (const filePath of files) {
7023
7173
  let content;
7024
7174
  try {
7025
- content = await readFile14(filePath, "utf-8");
7175
+ content = await readFile15(filePath, "utf-8");
7026
7176
  } catch {
7027
7177
  continue;
7028
7178
  }
@@ -7048,7 +7198,7 @@ var billingScanner = {
7048
7198
  };
7049
7199
 
7050
7200
  // src/preflight/scanners/privacy-scanner.ts
7051
- import { readFile as readFile15 } from "fs/promises";
7201
+ import { readFile as readFile16 } from "fs/promises";
7052
7202
  var TRACKING_SDKS = [
7053
7203
  {
7054
7204
  name: "Facebook SDK",
@@ -7088,7 +7238,7 @@ var privacyScanner = {
7088
7238
  for (const filePath of files) {
7089
7239
  let content;
7090
7240
  try {
7091
- content = await readFile15(filePath, "utf-8");
7241
+ content = await readFile16(filePath, "utf-8");
7092
7242
  } catch {
7093
7243
  continue;
7094
7244
  }
@@ -7238,6 +7388,19 @@ var policyScanner = {
7238
7388
  suggestion: "Run `gpc verify checklist` to review readiness, including registration of every app on your account. This is a distribution-market and account-registration reminder, not an AAB/APK defect.",
7239
7389
  policyUrl: "https://developer.android.com/developer-verification"
7240
7390
  });
7391
+ const foregroundServicePerms = manifest.permissions.filter((p) => p.startsWith("android.permission.FOREGROUND_SERVICE")).sort();
7392
+ if (foregroundServicePerms.length > 0) {
7393
+ const shortNames = foregroundServicePerms.map((p) => p.replace("android.permission.", ""));
7394
+ findings.push({
7395
+ scanner: "policy",
7396
+ ruleId: "policy-app-content-declaration",
7397
+ severity: "info",
7398
+ title: "Foreground service permissions require an App content declaration",
7399
+ message: `This app requests ${shortNames.join(", ")}. Google Play requires a completed "Foreground service permissions" declaration under App content before it will accept a release. The declaration is stored in Play Console, so it cannot be verified from the AAB.`,
7400
+ suggestion: "Complete Play Console \u2192 your app \u2192 Policy \u2192 App content \u2192 Foreground service permissions before releasing. If it is incomplete, the Play API rejects the upload with a 403 whose wording resembles a service account permission error; GPC reports that as API_DECLARATION_REQUIRED.",
7401
+ policyUrl: "https://support.google.com/googleplay/android-developer/answer/13392821"
7402
+ });
7403
+ }
7241
7404
  if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
7242
7405
  findings.push({
7243
7406
  scanner: "policy",
@@ -7450,13 +7613,13 @@ async function runPreflight(options) {
7450
7613
 
7451
7614
  // src/commands/plugin-scaffold.ts
7452
7615
  import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
7453
- import { join as join11 } from "path";
7616
+ import { join as join12 } from "path";
7454
7617
  async function scaffoldPlugin(options) {
7455
7618
  const { name, dir, description = `GPC plugin: ${name}` } = options;
7456
7619
  const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
7457
7620
  const shortName = pluginName.replace(/^gpc-plugin-/, "");
7458
- const srcDir = join11(dir, "src");
7459
- const testDir = join11(dir, "tests");
7621
+ const srcDir = join12(dir, "src");
7622
+ const testDir = join12(dir, "tests");
7460
7623
  await mkdir8(srcDir, { recursive: true });
7461
7624
  await mkdir8(testDir, { recursive: true });
7462
7625
  const files = [];
@@ -7474,6 +7637,9 @@ async function scaffoldPlugin(options) {
7474
7637
  }
7475
7638
  },
7476
7639
  files: ["dist"],
7640
+ gpc: {
7641
+ permissions: ["hooks:beforeCommand", "hooks:afterCommand"]
7642
+ },
7477
7643
  scripts: {
7478
7644
  build: "tsup src/index.ts --format esm --dts",
7479
7645
  dev: "tsup src/index.ts --format esm --dts --watch",
@@ -7492,7 +7658,7 @@ async function scaffoldPlugin(options) {
7492
7658
  vitest: "^3.0.0"
7493
7659
  }
7494
7660
  };
7495
- await writeFile8(join11(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
7661
+ await writeFile8(join12(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
7496
7662
  files.push("package.json");
7497
7663
  const tsconfig = {
7498
7664
  compilerOptions: {
@@ -7508,7 +7674,7 @@ async function scaffoldPlugin(options) {
7508
7674
  },
7509
7675
  include: ["src"]
7510
7676
  };
7511
- await writeFile8(join11(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
7677
+ await writeFile8(join12(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
7512
7678
  files.push("tsconfig.json");
7513
7679
  const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
7514
7680
  import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
@@ -7528,7 +7694,8 @@ export const plugin = definePlugin({
7528
7694
  // Example: send notifications, update dashboards, etc.
7529
7695
  });
7530
7696
 
7531
- // Uncomment to add custom commands:
7697
+ // To add custom commands, first add "commands:register" to
7698
+ // package.json#gpc.permissions, then uncomment this block:
7532
7699
  // hooks.registerCommands((registry) => {
7533
7700
  // registry.add({
7534
7701
  // name: "${shortName}",
@@ -7541,7 +7708,7 @@ export const plugin = definePlugin({
7541
7708
  },
7542
7709
  });
7543
7710
  `;
7544
- await writeFile8(join11(srcDir, "index.ts"), srcContent);
7711
+ await writeFile8(join12(srcDir, "index.ts"), srcContent);
7545
7712
  files.push("src/index.ts");
7546
7713
  const testContent = `import { describe, it, expect, vi } from "vitest";
7547
7714
  import { plugin } from "../src/index";
@@ -7566,7 +7733,7 @@ describe("${pluginName}", () => {
7566
7733
  });
7567
7734
  });
7568
7735
  `;
7569
- await writeFile8(join11(testDir, "plugin.test.ts"), testContent);
7736
+ await writeFile8(join12(testDir, "plugin.test.ts"), testContent);
7570
7737
  files.push("tests/plugin.test.ts");
7571
7738
  return { dir, files };
7572
7739
  }
@@ -7757,7 +7924,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
7757
7924
  }
7758
7925
 
7759
7926
  // src/commands/bundle-analysis.ts
7760
- import { readFile as readFile16, stat as stat8 } from "fs/promises";
7927
+ import { readFile as readFile17, stat as stat8 } from "fs/promises";
7761
7928
  var EOCD_SIGNATURE = 101010256;
7762
7929
  var CD_SIGNATURE = 33639248;
7763
7930
  var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
@@ -7837,7 +8004,7 @@ async function analyzeBundle(filePath) {
7837
8004
  if (!fileInfo || !fileInfo.isFile()) {
7838
8005
  throw new Error(`File not found: ${filePath}`);
7839
8006
  }
7840
- const buf = await readFile16(filePath);
8007
+ const buf = await readFile17(filePath);
7841
8008
  const cdEntries = parseCentralDirectory(buf);
7842
8009
  const fileType = detectFileType2(filePath);
7843
8010
  const isAab = fileType === "aab";
@@ -7923,7 +8090,7 @@ function topFiles(analysis, n = 20) {
7923
8090
  async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
7924
8091
  let config;
7925
8092
  try {
7926
- const raw = await readFile16(configPath, "utf-8");
8093
+ const raw = await readFile17(configPath, "utf-8");
7927
8094
  config = JSON.parse(raw);
7928
8095
  } catch {
7929
8096
  return { passed: true, violations: [] };
@@ -9285,7 +9452,7 @@ async function sha256File(filePath) {
9285
9452
 
9286
9453
  // src/commands/image-sync.ts
9287
9454
  import { readdir as readdir7, stat as stat10 } from "fs/promises";
9288
- import { join as join12, extname as extname5 } from "path";
9455
+ import { join as join13, extname as extname5 } from "path";
9289
9456
  import { PlayApiError } from "@gpc-cli/api";
9290
9457
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
9291
9458
  var ALL_IMAGE_TYPES2 = [
@@ -9340,7 +9507,7 @@ async function syncImages(client, packageName, dir, options) {
9340
9507
  try {
9341
9508
  for (const language of languages) {
9342
9509
  for (const imageType of imageTypes) {
9343
- const localDir = join12(dir, language, imageType);
9510
+ const localDir = join13(dir, language, imageType);
9344
9511
  const localFiles = await scanLocalImages(localDir);
9345
9512
  let remoteImages;
9346
9513
  try {
@@ -9355,12 +9522,12 @@ async function syncImages(client, packageName, dir, options) {
9355
9522
  const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
9356
9523
  const localHashes = /* @__PURE__ */ new Map();
9357
9524
  for (const file of localFiles) {
9358
- const hash = await sha256File(join12(localDir, file));
9525
+ const hash = await sha256File(join13(localDir, file));
9359
9526
  localHashes.set(file, hash);
9360
9527
  }
9361
9528
  const uploadLocal = async (file, reason) => {
9362
9529
  if (!options?.dryRun) {
9363
- const filePath = join12(localDir, file);
9530
+ const filePath = join13(localDir, file);
9364
9531
  const check = await validateImage(filePath, imageType);
9365
9532
  if (!check.valid) {
9366
9533
  throw new GpcError(
@@ -9394,7 +9561,13 @@ async function syncImages(client, packageName, dir, options) {
9394
9561
  if (inOrder) {
9395
9562
  for (const file of localFiles) {
9396
9563
  skipped++;
9397
- details.push({ language, imageType, file, action: "skip", reason: "already in order" });
9564
+ details.push({
9565
+ language,
9566
+ imageType,
9567
+ file,
9568
+ action: "skip",
9569
+ reason: "already in order"
9570
+ });
9398
9571
  }
9399
9572
  continue;
9400
9573
  }
@@ -9404,7 +9577,13 @@ async function syncImages(client, packageName, dir, options) {
9404
9577
  }
9405
9578
  for (const img of remoteImages) {
9406
9579
  deleted++;
9407
- details.push({ language, imageType, file: img.id, action: "delete", reason: "reordering" });
9580
+ details.push({
9581
+ language,
9582
+ imageType,
9583
+ file: img.id,
9584
+ action: "delete",
9585
+ reason: "reordering"
9586
+ });
9408
9587
  }
9409
9588
  }
9410
9589
  for (const file of localFiles) {
@@ -9831,6 +10010,7 @@ export {
9831
10010
  diffOneTimeProduct,
9832
10011
  diffReleases,
9833
10012
  diffSubscription,
10013
+ discoverPluginEntries,
9834
10014
  discoverPlugins,
9835
10015
  dispatchWebhook,
9836
10016
  downloadFinancialReport,
@@ -10041,6 +10221,7 @@ export {
10041
10221
  validateImage,
10042
10222
  validateLanguageCode,
10043
10223
  validatePackageName,
10224
+ validatePluginForApproval,
10044
10225
  validatePreSubmission,
10045
10226
  validateReleaseNotes,
10046
10227
  validateSku,