@gpc-cli/core 0.9.77 → 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/README.md +1 -1
- package/dist/index.d.ts +165 -6
- package/dist/index.js +697 -142
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -443,7 +443,15 @@ function formatJunit(data, commandName = "command") {
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
// src/plugins.ts
|
|
446
|
-
|
|
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
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
485
|
+
stagedBeforeHandlers,
|
|
486
|
+
stagedAfterHandlers,
|
|
487
|
+
stagedErrorHandlers,
|
|
488
|
+
stagedBeforeRequestHandlers,
|
|
489
|
+
stagedAfterResponseHandlers,
|
|
490
|
+
stagedCommands
|
|
468
491
|
);
|
|
469
|
-
const hooks = isTrusted || !
|
|
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
|
-
|
|
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
|
|
626
|
-
return approved?.has(
|
|
657
|
+
function isPluginTrusted(specifier, approvalId, approved) {
|
|
658
|
+
if (isFirstPartyPluginSpecifier(specifier)) return true;
|
|
659
|
+
return approved?.has(approvalId) ?? false;
|
|
627
660
|
}
|
|
628
|
-
async function
|
|
661
|
+
async function discoverPluginEntries(options) {
|
|
629
662
|
const plugins = [];
|
|
630
663
|
const seen = /* @__PURE__ */ new Set();
|
|
631
|
-
const
|
|
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
|
-
|
|
635
|
-
if (
|
|
671
|
+
const resolvedSpecifier = resolvePluginApprovalId(name, cwd);
|
|
672
|
+
if (seen.has(resolvedSpecifier)) continue;
|
|
673
|
+
if (!isPluginTrusted(name, resolvedSpecifier, approved)) continue;
|
|
636
674
|
try {
|
|
637
|
-
const
|
|
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
|
-
|
|
641
|
-
|
|
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 =
|
|
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 =
|
|
1009
|
+
const filePath = join2(langDir, fileName);
|
|
860
1010
|
if (await exists(filePath)) {
|
|
861
|
-
const content = await
|
|
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 =
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
1331
|
-
import { join as
|
|
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 =
|
|
1501
|
+
const fastlaneDir = join3(cwd, "fastlane");
|
|
1352
1502
|
const hasFastlaneDir = await fileExists(fastlaneDir);
|
|
1353
|
-
const fastfilePath = hasFastlaneDir ?
|
|
1354
|
-
const appfilePath = hasFastlaneDir ?
|
|
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(
|
|
1358
|
-
const metadataDir = hasFastlaneDir ?
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3552
|
-
import { join as
|
|
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
|
|
3791
|
+
const content = await readFile5(join5(dir, file), "utf-8");
|
|
3642
3792
|
try {
|
|
3643
3793
|
localProducts.push(JSON.parse(content));
|
|
3644
3794
|
} catch {
|
|
@@ -3843,6 +3993,255 @@ async function convertRegionPrices(client, packageName, currencyCode, amount) {
|
|
|
3843
3993
|
});
|
|
3844
3994
|
}
|
|
3845
3995
|
|
|
3996
|
+
// src/reports/errors.ts
|
|
3997
|
+
function reportAccessDeniedError(bucket) {
|
|
3998
|
+
return new GpcError(
|
|
3999
|
+
`Access denied reading the Play reports bucket "${bucket}".`,
|
|
4000
|
+
"REPORT_ACCESS_DENIED",
|
|
4001
|
+
4,
|
|
4002
|
+
"Grant the service account access: Play Console -> Users & permissions -> the service account -> Account permissions -> enable 'View app information and download bulk reports (read-only)', then wait a few minutes for it to propagate."
|
|
4003
|
+
);
|
|
4004
|
+
}
|
|
4005
|
+
function reportAuthRejectedError(bucket) {
|
|
4006
|
+
return new GpcError(
|
|
4007
|
+
`Authentication was rejected reading the Play reports bucket "${bucket}".`,
|
|
4008
|
+
"REPORT_AUTH_REJECTED",
|
|
4009
|
+
3,
|
|
4010
|
+
"The access token was not accepted. Check the service account key is valid (gpc auth status) and that your system clock is accurate, then retry."
|
|
4011
|
+
);
|
|
4012
|
+
}
|
|
4013
|
+
function reportBucketNotFoundError(bucket) {
|
|
4014
|
+
return new GpcError(
|
|
4015
|
+
`Play reports bucket "${bucket}" was not found.`,
|
|
4016
|
+
"REPORT_BUCKET_NOT_FOUND",
|
|
4017
|
+
4,
|
|
4018
|
+
"Confirm the bucket name. Copy the exact Cloud Storage URI from Play Console -> Download reports, then set it with --bucket or the 'reports.bucket' config key. The default is pubsite_prod_<developerId>."
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
4021
|
+
function reportObjectNotFoundError(opts) {
|
|
4022
|
+
const { reportType, month, dimension, available } = opts;
|
|
4023
|
+
const dimPart = dimension ? ` dimension "${dimension}"` : "";
|
|
4024
|
+
const availPart = available && available.length ? ` Available: ${available.join(", ")}.` : " No matching report objects were found for that month.";
|
|
4025
|
+
return new GpcError(
|
|
4026
|
+
`No "${reportType}" report found for ${month}${dimPart}.`,
|
|
4027
|
+
"REPORT_OBJECT_NOT_FOUND",
|
|
4028
|
+
4,
|
|
4029
|
+
`Check the month is complete and the report type/dimension exist.${availPart} Reports for the current month may not be published until it ends.`
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
// src/reports/gcs.ts
|
|
4034
|
+
var GCS_API_BASE = "https://storage.googleapis.com/storage/v1/b";
|
|
4035
|
+
var LIST_TIMEOUT_MS = 6e4;
|
|
4036
|
+
var DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
4037
|
+
function networkError(bucket, cause) {
|
|
4038
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
4039
|
+
return new GpcError(
|
|
4040
|
+
`Network error reading the Play reports bucket "${bucket}": ${detail}`,
|
|
4041
|
+
"NETWORK_ERROR",
|
|
4042
|
+
5,
|
|
4043
|
+
"Check your internet connection and retry. Google Cloud Storage may also be briefly unavailable."
|
|
4044
|
+
);
|
|
4045
|
+
}
|
|
4046
|
+
function maxRetries() {
|
|
4047
|
+
const parsed = Number(process.env["GPC_MAX_RETRIES"]);
|
|
4048
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 10 ? parsed : 3;
|
|
4049
|
+
}
|
|
4050
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
4051
|
+
async function gcsFetch(auth, bucket, url, timeoutMs) {
|
|
4052
|
+
const retries = maxRetries();
|
|
4053
|
+
let lastError;
|
|
4054
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
4055
|
+
if (attempt > 0) {
|
|
4056
|
+
await new Promise((r) => setTimeout(r, Math.min(1e3 * 2 ** (attempt - 1), 8e3)));
|
|
4057
|
+
}
|
|
4058
|
+
const token = await auth.getAccessToken();
|
|
4059
|
+
let res;
|
|
4060
|
+
try {
|
|
4061
|
+
res = await fetch(url, {
|
|
4062
|
+
headers: { authorization: `Bearer ${token}` },
|
|
4063
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4064
|
+
});
|
|
4065
|
+
} catch (err) {
|
|
4066
|
+
lastError = err;
|
|
4067
|
+
continue;
|
|
4068
|
+
}
|
|
4069
|
+
if (RETRYABLE_STATUSES.has(res.status) && attempt < retries) {
|
|
4070
|
+
continue;
|
|
4071
|
+
}
|
|
4072
|
+
if (res.status === 401) {
|
|
4073
|
+
throw reportAuthRejectedError(bucket);
|
|
4074
|
+
}
|
|
4075
|
+
if (res.status === 403) {
|
|
4076
|
+
throw reportAccessDeniedError(bucket);
|
|
4077
|
+
}
|
|
4078
|
+
return res;
|
|
4079
|
+
}
|
|
4080
|
+
throw networkError(bucket, lastError ?? new Error("retries exhausted"));
|
|
4081
|
+
}
|
|
4082
|
+
async function listReportObjects(auth, bucket, options) {
|
|
4083
|
+
const params = new URLSearchParams({ prefix: options.prefix });
|
|
4084
|
+
if (options.maxResults) params.set("maxResults", String(options.maxResults));
|
|
4085
|
+
if (options.pageToken) params.set("pageToken", options.pageToken);
|
|
4086
|
+
const url = `${GCS_API_BASE}/${encodeURIComponent(bucket)}/o?${params.toString()}`;
|
|
4087
|
+
const res = await gcsFetch(auth, bucket, url, LIST_TIMEOUT_MS);
|
|
4088
|
+
if (res.status === 404) {
|
|
4089
|
+
throw reportBucketNotFoundError(bucket);
|
|
4090
|
+
}
|
|
4091
|
+
if (!res.ok) {
|
|
4092
|
+
throw new GpcError(
|
|
4093
|
+
`Google Cloud Storage returned HTTP ${res.status} listing the reports bucket "${bucket}".`,
|
|
4094
|
+
"REPORT_LIST_FAILED",
|
|
4095
|
+
4,
|
|
4096
|
+
"Retry in a moment. If the error persists, verify the bucket name and the service account's report access."
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
let body;
|
|
4100
|
+
try {
|
|
4101
|
+
body = await res.json();
|
|
4102
|
+
} catch (err) {
|
|
4103
|
+
throw networkError(bucket, err);
|
|
4104
|
+
}
|
|
4105
|
+
const objects = (body.items ?? []).filter((item) => typeof item.name === "string").map((item) => ({
|
|
4106
|
+
name: item.name,
|
|
4107
|
+
size: Number(item.size ?? 0),
|
|
4108
|
+
updated: item.updated
|
|
4109
|
+
}));
|
|
4110
|
+
return { objects, nextPageToken: body.nextPageToken };
|
|
4111
|
+
}
|
|
4112
|
+
async function downloadReportObject(auth, bucket, objectName) {
|
|
4113
|
+
const url = `${GCS_API_BASE}/${encodeURIComponent(bucket)}/o/${encodeURIComponent(objectName)}?alt=media`;
|
|
4114
|
+
const res = await gcsFetch(auth, bucket, url, DOWNLOAD_TIMEOUT_MS);
|
|
4115
|
+
if (res.status === 404) {
|
|
4116
|
+
throw new GpcError(
|
|
4117
|
+
`Report object "${objectName}" was not found in bucket "${bucket}".`,
|
|
4118
|
+
"REPORT_OBJECT_NOT_FOUND",
|
|
4119
|
+
4,
|
|
4120
|
+
"List available reports first: gpc reports list <report-type> --month <YYYY-MM>"
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
if (!res.ok) {
|
|
4124
|
+
throw new GpcError(
|
|
4125
|
+
`Google Cloud Storage returned HTTP ${res.status} downloading "${objectName}".`,
|
|
4126
|
+
"REPORT_DOWNLOAD_FAILED",
|
|
4127
|
+
4,
|
|
4128
|
+
"Retry in a moment. If the error persists, list the reports to confirm the object still exists."
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
try {
|
|
4132
|
+
return Buffer.from(await res.arrayBuffer());
|
|
4133
|
+
} catch (err) {
|
|
4134
|
+
throw networkError(bucket, err);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
// src/reports/decode.ts
|
|
4139
|
+
import { gunzipSync } from "zlib";
|
|
4140
|
+
import { fromBuffer } from "yauzl";
|
|
4141
|
+
var GZIP_MAGIC = [31, 139];
|
|
4142
|
+
var ZIP_MAGIC = [80, 75];
|
|
4143
|
+
function isGzip(buf) {
|
|
4144
|
+
return buf.length >= 2 && buf[0] === GZIP_MAGIC[0] && buf[1] === GZIP_MAGIC[1];
|
|
4145
|
+
}
|
|
4146
|
+
function isZip(buf) {
|
|
4147
|
+
return buf.length >= 2 && buf[0] === ZIP_MAGIC[0] && buf[1] === ZIP_MAGIC[1];
|
|
4148
|
+
}
|
|
4149
|
+
var MAX_INFLATED_BYTES = 512 * 1024 * 1024;
|
|
4150
|
+
function gunzipIfNeeded(buf) {
|
|
4151
|
+
return isGzip(buf) ? gunzipSync(buf, { maxOutputLength: MAX_INFLATED_BYTES }) : buf;
|
|
4152
|
+
}
|
|
4153
|
+
function decodeCsvText(buf) {
|
|
4154
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) {
|
|
4155
|
+
return buf.subarray(2).toString("utf16le");
|
|
4156
|
+
}
|
|
4157
|
+
if (buf.length >= 2 && buf[0] === 254 && buf[1] === 255) {
|
|
4158
|
+
const swapped = Buffer.from(buf.subarray(2));
|
|
4159
|
+
swapped.swap16();
|
|
4160
|
+
return swapped.toString("utf16le");
|
|
4161
|
+
}
|
|
4162
|
+
if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) {
|
|
4163
|
+
return buf.subarray(3).toString("utf8");
|
|
4164
|
+
}
|
|
4165
|
+
return buf.toString("utf8");
|
|
4166
|
+
}
|
|
4167
|
+
function decodeStatsCsv(buf) {
|
|
4168
|
+
return decodeCsvText(gunzipIfNeeded(buf));
|
|
4169
|
+
}
|
|
4170
|
+
var ZIP_READ_TIMEOUT_MS = 3e4;
|
|
4171
|
+
function extractCsvEntriesFromZip(buffer) {
|
|
4172
|
+
return new Promise((resolve2, reject) => {
|
|
4173
|
+
let settled = false;
|
|
4174
|
+
const timer = setTimeout(() => {
|
|
4175
|
+
settleReject(new Error("Timed out reading report archive"));
|
|
4176
|
+
}, ZIP_READ_TIMEOUT_MS);
|
|
4177
|
+
timer.unref?.();
|
|
4178
|
+
function settleResolve(v) {
|
|
4179
|
+
if (settled) return;
|
|
4180
|
+
settled = true;
|
|
4181
|
+
clearTimeout(timer);
|
|
4182
|
+
resolve2(v);
|
|
4183
|
+
}
|
|
4184
|
+
function settleReject(e) {
|
|
4185
|
+
if (settled) return;
|
|
4186
|
+
settled = true;
|
|
4187
|
+
clearTimeout(timer);
|
|
4188
|
+
reject(e);
|
|
4189
|
+
}
|
|
4190
|
+
try {
|
|
4191
|
+
fromBuffer(buffer, { lazyEntries: true }, (err, openedZip) => {
|
|
4192
|
+
if (err || !openedZip) {
|
|
4193
|
+
settleReject(err ?? new Error("Failed to open report archive"));
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
const zipfile = openedZip;
|
|
4197
|
+
const entries = [];
|
|
4198
|
+
let pending = 0;
|
|
4199
|
+
let entriesDone = false;
|
|
4200
|
+
function fail(e) {
|
|
4201
|
+
try {
|
|
4202
|
+
zipfile.close();
|
|
4203
|
+
} catch {
|
|
4204
|
+
}
|
|
4205
|
+
settleReject(e);
|
|
4206
|
+
}
|
|
4207
|
+
function maybeResolve() {
|
|
4208
|
+
if (!settled && entriesDone && pending === 0) settleResolve(entries);
|
|
4209
|
+
}
|
|
4210
|
+
zipfile.on("entry", (entry) => {
|
|
4211
|
+
if (/\/$/.test(entry.fileName) || !/\.csv$/i.test(entry.fileName)) {
|
|
4212
|
+
zipfile.readEntry();
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
pending++;
|
|
4216
|
+
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
4217
|
+
if (streamErr || !stream) {
|
|
4218
|
+
fail(streamErr ?? new Error(`Failed to read ${entry.fileName}`));
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
const chunks = [];
|
|
4222
|
+
stream.on("data", (c) => chunks.push(c));
|
|
4223
|
+
stream.on("error", fail);
|
|
4224
|
+
stream.on("end", () => {
|
|
4225
|
+
entries.push({ name: entry.fileName, text: decodeStatsCsv(Buffer.concat(chunks)) });
|
|
4226
|
+
pending--;
|
|
4227
|
+
zipfile.readEntry();
|
|
4228
|
+
maybeResolve();
|
|
4229
|
+
});
|
|
4230
|
+
});
|
|
4231
|
+
});
|
|
4232
|
+
zipfile.on("end", () => {
|
|
4233
|
+
entriesDone = true;
|
|
4234
|
+
maybeResolve();
|
|
4235
|
+
});
|
|
4236
|
+
zipfile.on("error", fail);
|
|
4237
|
+
zipfile.readEntry();
|
|
4238
|
+
});
|
|
4239
|
+
} catch (e) {
|
|
4240
|
+
settleReject(e instanceof Error ? e : new Error(String(e)));
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
|
|
3846
4245
|
// src/commands/reports.ts
|
|
3847
4246
|
var FINANCIAL_REPORT_TYPES = /* @__PURE__ */ new Set([
|
|
3848
4247
|
"earnings",
|
|
@@ -3865,6 +4264,7 @@ var VALID_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
|
3865
4264
|
"device",
|
|
3866
4265
|
"app_version",
|
|
3867
4266
|
"carrier",
|
|
4267
|
+
"traffic_source",
|
|
3868
4268
|
"overview"
|
|
3869
4269
|
]);
|
|
3870
4270
|
function isFinancialReportType(type) {
|
|
@@ -3901,22 +4301,131 @@ function parseMonth(monthStr) {
|
|
|
3901
4301
|
}
|
|
3902
4302
|
return { year, month };
|
|
3903
4303
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
`Report listing for "${reportType}" is not available through the Google Play Developer API.`,
|
|
3907
|
-
"REPORT_NOT_SUPPORTED",
|
|
3908
|
-
1,
|
|
3909
|
-
isFinancialReportType(reportType) ? "Financial reports are delivered via Google Cloud Storage. Access them from Play Console \u2192 Download reports \u2192 Financial." : "Stats reports are delivered via Google Cloud Storage. For real-time metrics, use 'gpc vitals' commands."
|
|
3910
|
-
);
|
|
4304
|
+
function monthToken(parsed) {
|
|
4305
|
+
return `${parsed.year}${String(parsed.month).padStart(2, "0")}`;
|
|
3911
4306
|
}
|
|
3912
|
-
|
|
4307
|
+
function nameHasMonth(name, token) {
|
|
4308
|
+
return new RegExp(`(?<![0-9])${token}(?![0-9])`).test(name);
|
|
4309
|
+
}
|
|
4310
|
+
function resolveReportsBucket(config) {
|
|
4311
|
+
const explicit = config.reports?.bucket?.trim();
|
|
4312
|
+
if (explicit) {
|
|
4313
|
+
const reduced = explicit.replace(/^gs:\/\//, "").replace(/\/.*$/, "");
|
|
4314
|
+
if (!/^[a-z0-9][a-z0-9._-]{1,221}$/.test(reduced)) {
|
|
4315
|
+
throw new GpcError(
|
|
4316
|
+
`Invalid reports bucket name "${reduced}".`,
|
|
4317
|
+
"REPORT_BUCKET_INVALID",
|
|
4318
|
+
2,
|
|
4319
|
+
"Copy the exact Cloud Storage URI from Play Console -> Download reports. The bucket name looks like pubsite_prod_<developerId>."
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
return reduced;
|
|
4323
|
+
}
|
|
4324
|
+
if (config.developerId) {
|
|
4325
|
+
return `pubsite_prod_${config.developerId}`;
|
|
4326
|
+
}
|
|
3913
4327
|
throw new GpcError(
|
|
3914
|
-
|
|
3915
|
-
"
|
|
3916
|
-
|
|
3917
|
-
|
|
4328
|
+
"Cannot determine the Play reports bucket: no bucket configured and no developer id set.",
|
|
4329
|
+
"REPORT_BUCKET_UNKNOWN",
|
|
4330
|
+
2,
|
|
4331
|
+
"Set your developer id (gpc config set developerId <id> or GPC_DEVELOPER_ID) so the default pubsite_prod_<developerId> bucket can be derived, or pass the bucket directly with --bucket / GPC_REPORTS_BUCKET / the reports.bucket config key. The exact URI is shown in Play Console -> Download reports."
|
|
3918
4332
|
);
|
|
3919
4333
|
}
|
|
4334
|
+
function reportPrefix(reportType, packageName) {
|
|
4335
|
+
switch (reportType) {
|
|
4336
|
+
case "earnings":
|
|
4337
|
+
return "earnings/";
|
|
4338
|
+
case "sales":
|
|
4339
|
+
case "estimated_sales":
|
|
4340
|
+
return "sales/";
|
|
4341
|
+
case "play_balance":
|
|
4342
|
+
return "play_balance/";
|
|
4343
|
+
case "reviews":
|
|
4344
|
+
return packageName ? `reviews/reviews_${packageName}_` : "reviews/";
|
|
4345
|
+
case "subscriptions":
|
|
4346
|
+
return packageName ? `financial-stats/subscriptions/subscriptions_${packageName}_` : "financial-stats/subscriptions/";
|
|
4347
|
+
default:
|
|
4348
|
+
return packageName ? `stats/${reportType}/${reportType}_${packageName}_` : `stats/${reportType}/`;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
async function listReports(auth, bucket, reportType, options = {}) {
|
|
4352
|
+
const packageName = isStatsReportType(reportType) ? options.packageName : void 0;
|
|
4353
|
+
const basePrefix = reportPrefix(reportType, packageName);
|
|
4354
|
+
const serverSideMonth = Boolean(packageName && options.month);
|
|
4355
|
+
const prefix = serverSideMonth ? `${basePrefix}${monthToken(options.month)}` : basePrefix;
|
|
4356
|
+
const { objects, nextPageToken } = await listReportObjects(auth, bucket, {
|
|
4357
|
+
prefix,
|
|
4358
|
+
maxResults: options.maxResults,
|
|
4359
|
+
pageToken: options.pageToken
|
|
4360
|
+
});
|
|
4361
|
+
const reports = options.month && !serverSideMonth ? objects.filter((o) => nameHasMonth(o.name, monthToken(options.month))) : objects;
|
|
4362
|
+
return { reports, nextPageToken };
|
|
4363
|
+
}
|
|
4364
|
+
async function downloadStatsReport(auth, bucket, packageName, reportType, month, dimension) {
|
|
4365
|
+
const token = monthToken(month);
|
|
4366
|
+
const prefix = `${reportPrefix(reportType, packageName)}${token}`;
|
|
4367
|
+
const { objects } = await listReportObjects(auth, bucket, { prefix });
|
|
4368
|
+
const wanted = reportType === "reviews" ? objects.find((o) => o.name.endsWith(`${token}.csv`)) : objects.find((o) => o.name.endsWith(`${token}_${dimension}.csv`));
|
|
4369
|
+
if (!wanted) {
|
|
4370
|
+
const available = objects.map((o) => new RegExp(`(?<![0-9])${token}_([a-z_]+)\\.csv$`).exec(o.name)?.[1]).filter((d) => Boolean(d));
|
|
4371
|
+
throw reportObjectNotFoundError({
|
|
4372
|
+
reportType,
|
|
4373
|
+
month: `${month.year}-${String(month.month).padStart(2, "0")}`,
|
|
4374
|
+
dimension: reportType === "reviews" ? void 0 : dimension,
|
|
4375
|
+
available: [...new Set(available)]
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
const bytes = await downloadReportObject(auth, bucket, wanted.name);
|
|
4379
|
+
try {
|
|
4380
|
+
return { objectName: wanted.name, csv: decodeStatsCsv(bytes) };
|
|
4381
|
+
} catch (err) {
|
|
4382
|
+
throw new GpcError(
|
|
4383
|
+
`Could not decode the report "${wanted.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
4384
|
+
"REPORT_DECODE_FAILED",
|
|
4385
|
+
4,
|
|
4386
|
+
"Re-download in case the transfer was corrupted. If the error persists, the object may be malformed; download it from Play Console -> Download reports to compare."
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
async function downloadFinancialReport(auth, bucket, reportType, month, options = {}) {
|
|
4391
|
+
const token = monthToken(month);
|
|
4392
|
+
const { objects } = await listReportObjects(auth, bucket, {
|
|
4393
|
+
prefix: reportPrefix(reportType)
|
|
4394
|
+
});
|
|
4395
|
+
const matches = objects.filter((o) => nameHasMonth(o.name, token));
|
|
4396
|
+
if (matches.length === 0) {
|
|
4397
|
+
const availableMonths = [
|
|
4398
|
+
...new Set(
|
|
4399
|
+
objects.map((o) => /(?<![0-9])(\d{4})(\d{2})(?![0-9])/.exec(o.name)).filter((m) => m !== null).map((m) => `${m[1]}-${m[2]}`)
|
|
4400
|
+
)
|
|
4401
|
+
].slice(-6);
|
|
4402
|
+
throw reportObjectNotFoundError({
|
|
4403
|
+
reportType,
|
|
4404
|
+
month: `${month.year}-${String(month.month).padStart(2, "0")}`,
|
|
4405
|
+
available: availableMonths
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
const chosen = matches[matches.length - 1];
|
|
4409
|
+
const bytes = await downloadReportObject(auth, bucket, chosen.name);
|
|
4410
|
+
if (isZip(bytes)) {
|
|
4411
|
+
if (options.extractEntries === false) {
|
|
4412
|
+
return { objectName: chosen.name, kind: "zip", entries: [], raw: bytes };
|
|
4413
|
+
}
|
|
4414
|
+
let entries;
|
|
4415
|
+
try {
|
|
4416
|
+
entries = await extractCsvEntriesFromZip(bytes);
|
|
4417
|
+
} catch (err) {
|
|
4418
|
+
throw new GpcError(
|
|
4419
|
+
`Could not read the report archive "${chosen.name}": ${err instanceof Error ? err.message : String(err)}`,
|
|
4420
|
+
"REPORT_ARCHIVE_UNREADABLE",
|
|
4421
|
+
4,
|
|
4422
|
+
"Save the raw archive instead (--output-file report.zip) and open it locally, or re-download in case the transfer was corrupted."
|
|
4423
|
+
);
|
|
4424
|
+
}
|
|
4425
|
+
return { objectName: chosen.name, kind: "zip", entries, raw: bytes };
|
|
4426
|
+
}
|
|
4427
|
+
return { objectName: chosen.name, kind: "csv", text: decodeStatsCsv(bytes) };
|
|
4428
|
+
}
|
|
3920
4429
|
|
|
3921
4430
|
// src/commands/users.ts
|
|
3922
4431
|
import { paginateAll as paginateAll5 } from "@gpc-cli/api";
|
|
@@ -4017,7 +4526,7 @@ async function deleteGrant(client, developerId, email, packageName) {
|
|
|
4017
4526
|
}
|
|
4018
4527
|
|
|
4019
4528
|
// src/commands/testers.ts
|
|
4020
|
-
import { readFile as
|
|
4529
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
4021
4530
|
async function listTesters(client, packageName, track) {
|
|
4022
4531
|
const edit = await client.edits.insert(packageName);
|
|
4023
4532
|
try {
|
|
@@ -4064,7 +4573,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
|
|
|
4064
4573
|
}
|
|
4065
4574
|
}
|
|
4066
4575
|
async function importTestersFromCsv(client, packageName, track, csvPath, commitOptions) {
|
|
4067
|
-
const content = await
|
|
4576
|
+
const content = await readFile6(csvPath, "utf-8");
|
|
4068
4577
|
const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
|
|
4069
4578
|
if (emails.length === 0) {
|
|
4070
4579
|
throw new GpcError(
|
|
@@ -4207,7 +4716,7 @@ async function addRecoveryTargeting(client, packageName, actionId, targeting) {
|
|
|
4207
4716
|
}
|
|
4208
4717
|
|
|
4209
4718
|
// src/commands/data-safety.ts
|
|
4210
|
-
import { readFile as
|
|
4719
|
+
import { readFile as readFile7, stat as stat4 } from "fs/promises";
|
|
4211
4720
|
var MAX_CSV_BYTES = 1 * 1024 * 1024;
|
|
4212
4721
|
async function updateDataSafety(client, packageName, data) {
|
|
4213
4722
|
return client.dataSafety.update(packageName, data);
|
|
@@ -4240,7 +4749,7 @@ async function importDataSafety(client, packageName, filePath) {
|
|
|
4240
4749
|
"The CSV should be a few KB. Check that you are passing the correct file."
|
|
4241
4750
|
);
|
|
4242
4751
|
}
|
|
4243
|
-
const csvContent = await
|
|
4752
|
+
const csvContent = await readFile7(filePath, "utf-8");
|
|
4244
4753
|
return updateDataSafety(client, packageName, { safetyLabels: csvContent });
|
|
4245
4754
|
}
|
|
4246
4755
|
|
|
@@ -4540,16 +5049,16 @@ function createSpinner(message) {
|
|
|
4540
5049
|
}
|
|
4541
5050
|
|
|
4542
5051
|
// src/utils/train-state.ts
|
|
4543
|
-
import { mkdir as mkdir4, readFile as
|
|
4544
|
-
import { dirname, join as
|
|
5052
|
+
import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
5053
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
4545
5054
|
import { getCacheDir as getCacheDir2 } from "@gpc-cli/config";
|
|
4546
5055
|
function stateFile(packageName) {
|
|
4547
|
-
return
|
|
5056
|
+
return join6(getCacheDir2(), `train-${packageName}.json`);
|
|
4548
5057
|
}
|
|
4549
5058
|
async function readTrainState(packageName) {
|
|
4550
5059
|
const path = stateFile(packageName);
|
|
4551
5060
|
try {
|
|
4552
|
-
const raw = await
|
|
5061
|
+
const raw = await readFile8(path, "utf-8");
|
|
4553
5062
|
return JSON.parse(raw);
|
|
4554
5063
|
} catch {
|
|
4555
5064
|
return null;
|
|
@@ -4557,7 +5066,7 @@ async function readTrainState(packageName) {
|
|
|
4557
5066
|
}
|
|
4558
5067
|
async function writeTrainState(packageName, state) {
|
|
4559
5068
|
const path = stateFile(packageName);
|
|
4560
|
-
const dir =
|
|
5069
|
+
const dir = dirname2(path);
|
|
4561
5070
|
await mkdir4(dir, { recursive: true });
|
|
4562
5071
|
await writeFile4(path, JSON.stringify(state, null, 2), "utf-8");
|
|
4563
5072
|
}
|
|
@@ -4703,8 +5212,8 @@ async function executeStage(apiClient, packageName, state, stageIndex) {
|
|
|
4703
5212
|
}
|
|
4704
5213
|
|
|
4705
5214
|
// src/commands/games.ts
|
|
4706
|
-
import { mkdir as mkdir5, readdir as readdir4, readFile as
|
|
4707
|
-
import { extname as extname2, join as
|
|
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";
|
|
4708
5217
|
async function listLeaderboards(client, packageName) {
|
|
4709
5218
|
const result = await client.leaderboards.list(packageName);
|
|
4710
5219
|
return result.items ?? [];
|
|
@@ -4848,7 +5357,7 @@ async function readConfigDir(dir) {
|
|
|
4848
5357
|
const entries = (await readdir4(dir)).filter((f) => f.toLowerCase().endsWith(".json"));
|
|
4849
5358
|
const out = [];
|
|
4850
5359
|
for (const file of entries) {
|
|
4851
|
-
const raw = await
|
|
5360
|
+
const raw = await readFile9(join7(dir, file), "utf-8");
|
|
4852
5361
|
let parsed;
|
|
4853
5362
|
try {
|
|
4854
5363
|
parsed = JSON.parse(raw);
|
|
@@ -4901,7 +5410,7 @@ async function pullAchievementConfigs(client, applicationId, dir) {
|
|
|
4901
5410
|
for (const config of result.items ?? []) {
|
|
4902
5411
|
const id = config.id ?? "unknown";
|
|
4903
5412
|
await writeFile5(
|
|
4904
|
-
|
|
5413
|
+
join7(dir, safeConfigFilename(id)),
|
|
4905
5414
|
JSON.stringify(config, null, 2) + "\n",
|
|
4906
5415
|
"utf-8"
|
|
4907
5416
|
);
|
|
@@ -4940,7 +5449,7 @@ async function pullLeaderboardConfigs(client, applicationId, dir) {
|
|
|
4940
5449
|
for (const config of result.items ?? []) {
|
|
4941
5450
|
const id = config.id ?? "unknown";
|
|
4942
5451
|
await writeFile5(
|
|
4943
|
-
|
|
5452
|
+
join7(dir, safeConfigFilename(id)),
|
|
4944
5453
|
JSON.stringify(config, null, 2) + "\n",
|
|
4945
5454
|
"utf-8"
|
|
4946
5455
|
);
|
|
@@ -4964,8 +5473,8 @@ async function publishEnterpriseApp(client, params) {
|
|
|
4964
5473
|
}
|
|
4965
5474
|
|
|
4966
5475
|
// src/audit.ts
|
|
4967
|
-
import { appendFile, chmod, mkdir as mkdir6, readFile as
|
|
4968
|
-
import { join as
|
|
5476
|
+
import { appendFile, chmod, mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
5477
|
+
import { join as join8 } from "path";
|
|
4969
5478
|
var auditDir = null;
|
|
4970
5479
|
function initAudit(configDir) {
|
|
4971
5480
|
auditDir = configDir;
|
|
@@ -4974,7 +5483,7 @@ async function writeAuditLog(entry) {
|
|
|
4974
5483
|
if (!auditDir) return;
|
|
4975
5484
|
try {
|
|
4976
5485
|
await mkdir6(auditDir, { recursive: true, mode: 448 });
|
|
4977
|
-
const logPath =
|
|
5486
|
+
const logPath = join8(auditDir, "audit.log");
|
|
4978
5487
|
const redactedEntry = redactAuditArgs(entry);
|
|
4979
5488
|
const line = JSON.stringify(redactedEntry) + "\n";
|
|
4980
5489
|
await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
|
|
@@ -5029,10 +5538,10 @@ function createAuditEntry(command, args, app) {
|
|
|
5029
5538
|
}
|
|
5030
5539
|
async function listAuditEvents(options) {
|
|
5031
5540
|
if (!auditDir) return [];
|
|
5032
|
-
const logPath =
|
|
5541
|
+
const logPath = join8(auditDir, "audit.log");
|
|
5033
5542
|
let content;
|
|
5034
5543
|
try {
|
|
5035
|
-
content = await
|
|
5544
|
+
content = await readFile10(logPath, "utf-8");
|
|
5036
5545
|
} catch {
|
|
5037
5546
|
return [];
|
|
5038
5547
|
}
|
|
@@ -5067,10 +5576,10 @@ async function searchAuditEvents(query) {
|
|
|
5067
5576
|
}
|
|
5068
5577
|
async function clearAuditLog(options) {
|
|
5069
5578
|
if (!auditDir) return { deleted: 0, remaining: 0 };
|
|
5070
|
-
const logPath =
|
|
5579
|
+
const logPath = join8(auditDir, "audit.log");
|
|
5071
5580
|
let content;
|
|
5072
5581
|
try {
|
|
5073
|
-
content = await
|
|
5582
|
+
content = await readFile10(logPath, "utf-8");
|
|
5074
5583
|
} catch {
|
|
5075
5584
|
return { deleted: 0, remaining: 0 };
|
|
5076
5585
|
}
|
|
@@ -5159,7 +5668,7 @@ function safePathWithin(userPath, baseDir) {
|
|
|
5159
5668
|
|
|
5160
5669
|
// src/commands/init.ts
|
|
5161
5670
|
import { mkdir as mkdir7, writeFile as writeFile7, stat as stat5 } from "fs/promises";
|
|
5162
|
-
import { join as
|
|
5671
|
+
import { join as join9 } from "path";
|
|
5163
5672
|
async function exists2(path) {
|
|
5164
5673
|
try {
|
|
5165
5674
|
await stat5(path);
|
|
@@ -5194,7 +5703,7 @@ async function initProject(options) {
|
|
|
5194
5703
|
null,
|
|
5195
5704
|
2
|
|
5196
5705
|
);
|
|
5197
|
-
await safeWrite(
|
|
5706
|
+
await safeWrite(join9(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
|
|
5198
5707
|
const preflightrc = JSON.stringify(
|
|
5199
5708
|
{
|
|
5200
5709
|
failOn: "error",
|
|
@@ -5208,27 +5717,27 @@ async function initProject(options) {
|
|
|
5208
5717
|
2
|
|
5209
5718
|
);
|
|
5210
5719
|
await safeWrite(
|
|
5211
|
-
|
|
5720
|
+
join9(dir, ".preflightrc.json"),
|
|
5212
5721
|
preflightrc + "\n",
|
|
5213
5722
|
created,
|
|
5214
5723
|
skipped,
|
|
5215
5724
|
skipExisting
|
|
5216
5725
|
);
|
|
5217
|
-
const metaDir =
|
|
5218
|
-
await safeWrite(
|
|
5219
|
-
await safeWrite(
|
|
5220
|
-
await safeWrite(
|
|
5221
|
-
await safeWrite(
|
|
5222
|
-
const ssDir =
|
|
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");
|
|
5223
5732
|
await mkdir7(ssDir, { recursive: true });
|
|
5224
|
-
await safeWrite(
|
|
5733
|
+
await safeWrite(join9(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
|
|
5225
5734
|
if (ci === "github") {
|
|
5226
5735
|
const workflow = githubActionsTemplate(pkg);
|
|
5227
|
-
const workflowDir =
|
|
5228
|
-
await safeWrite(
|
|
5736
|
+
const workflowDir = join9(dir, ".github", "workflows");
|
|
5737
|
+
await safeWrite(join9(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
|
|
5229
5738
|
} else if (ci === "gitlab") {
|
|
5230
5739
|
const pipeline = gitlabCiTemplate(pkg);
|
|
5231
|
-
await safeWrite(
|
|
5740
|
+
await safeWrite(join9(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
|
|
5232
5741
|
}
|
|
5233
5742
|
return { created, skipped };
|
|
5234
5743
|
}
|
|
@@ -5324,13 +5833,13 @@ var DEFAULT_PREFLIGHT_CONFIG = {
|
|
|
5324
5833
|
};
|
|
5325
5834
|
|
|
5326
5835
|
// src/preflight/config.ts
|
|
5327
|
-
import { readFile as
|
|
5836
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5328
5837
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "error", "warning", "info"]);
|
|
5329
5838
|
async function loadPreflightConfig(configPath) {
|
|
5330
5839
|
const path = configPath || ".preflightrc.json";
|
|
5331
5840
|
let raw;
|
|
5332
5841
|
try {
|
|
5333
|
-
raw = await
|
|
5842
|
+
raw = await readFile11(path, "utf-8");
|
|
5334
5843
|
} catch {
|
|
5335
5844
|
return { ...DEFAULT_PREFLIGHT_CONFIG };
|
|
5336
5845
|
}
|
|
@@ -5375,8 +5884,8 @@ async function loadPreflightConfig(configPath) {
|
|
|
5375
5884
|
}
|
|
5376
5885
|
|
|
5377
5886
|
// src/preflight/aab-reader.ts
|
|
5378
|
-
import { readFile as
|
|
5379
|
-
import { fromBuffer } from "yauzl";
|
|
5887
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
5888
|
+
import { fromBuffer as fromBuffer2 } from "yauzl";
|
|
5380
5889
|
|
|
5381
5890
|
// src/preflight/manifest-parser.ts
|
|
5382
5891
|
import protobuf from "protobufjs";
|
|
@@ -5573,7 +6082,7 @@ async function readAab(aabPath) {
|
|
|
5573
6082
|
const manifestPath = detectManifestPath(aabPath);
|
|
5574
6083
|
let buffer;
|
|
5575
6084
|
try {
|
|
5576
|
-
buffer = await
|
|
6085
|
+
buffer = await readFile12(aabPath);
|
|
5577
6086
|
} catch (err) {
|
|
5578
6087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5579
6088
|
throw new Error(`Could not read ${aabPath}: ${msg}`, { cause: err });
|
|
@@ -5637,7 +6146,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
5637
6146
|
reject(error);
|
|
5638
6147
|
}
|
|
5639
6148
|
try {
|
|
5640
|
-
|
|
6149
|
+
fromBuffer2(buffer, { lazyEntries: true }, (err, zipfile) => {
|
|
5641
6150
|
if (err || !zipfile) {
|
|
5642
6151
|
settleReject(err ?? new Error("Failed to open AAB file"));
|
|
5643
6152
|
return;
|
|
@@ -6297,8 +6806,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
|
|
|
6297
6806
|
}
|
|
6298
6807
|
|
|
6299
6808
|
// src/preflight/scanners/metadata-scanner.ts
|
|
6300
|
-
import { readdir as readdir5, stat as stat6, readFile as
|
|
6301
|
-
import { join as
|
|
6809
|
+
import { readdir as readdir5, stat as stat6, readFile as readFile13 } from "fs/promises";
|
|
6810
|
+
import { join as join10 } from "path";
|
|
6302
6811
|
var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
6303
6812
|
var FILE_MAP2 = {
|
|
6304
6813
|
"title.txt": "title",
|
|
@@ -6349,14 +6858,14 @@ var metadataScanner = {
|
|
|
6349
6858
|
return findings;
|
|
6350
6859
|
}
|
|
6351
6860
|
for (const lang of locales) {
|
|
6352
|
-
const langDir =
|
|
6861
|
+
const langDir = join10(dir, lang);
|
|
6353
6862
|
const langStat = await stat6(langDir).catch(() => null);
|
|
6354
6863
|
if (!langStat?.isDirectory()) continue;
|
|
6355
6864
|
const fields = {};
|
|
6356
6865
|
for (const [fileName, field] of Object.entries(FILE_MAP2)) {
|
|
6357
|
-
const filePath =
|
|
6866
|
+
const filePath = join10(langDir, fileName);
|
|
6358
6867
|
try {
|
|
6359
|
-
const content = await
|
|
6868
|
+
const content = await readFile13(filePath, "utf-8");
|
|
6360
6869
|
fields[field] = content.trimEnd();
|
|
6361
6870
|
} catch {
|
|
6362
6871
|
}
|
|
@@ -6395,7 +6904,7 @@ var metadataScanner = {
|
|
|
6395
6904
|
let totalScreenshots = 0;
|
|
6396
6905
|
let phoneScreenshots = 0;
|
|
6397
6906
|
for (const ssDir of SCREENSHOT_DIRS) {
|
|
6398
|
-
const ssPath =
|
|
6907
|
+
const ssPath = join10(langDir, "images", ssDir);
|
|
6399
6908
|
try {
|
|
6400
6909
|
const ssEntries = await readdir5(ssPath);
|
|
6401
6910
|
const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
|
|
@@ -6427,9 +6936,9 @@ var metadataScanner = {
|
|
|
6427
6936
|
}
|
|
6428
6937
|
}
|
|
6429
6938
|
const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
|
|
6430
|
-
const privacyPath =
|
|
6939
|
+
const privacyPath = join10(dir, defaultLang, "privacy_policy_url.txt");
|
|
6431
6940
|
try {
|
|
6432
|
-
const url = await
|
|
6941
|
+
const url = await readFile13(privacyPath, "utf-8");
|
|
6433
6942
|
if (!url.trim()) throw new Error("empty");
|
|
6434
6943
|
} catch {
|
|
6435
6944
|
findings.push({
|
|
@@ -6454,11 +6963,11 @@ var metadataScanner = {
|
|
|
6454
6963
|
};
|
|
6455
6964
|
|
|
6456
6965
|
// src/preflight/scanners/secrets-scanner.ts
|
|
6457
|
-
import { readFile as
|
|
6966
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
6458
6967
|
|
|
6459
6968
|
// src/preflight/scan-files.ts
|
|
6460
6969
|
import { readdir as readdir6, stat as stat7 } from "fs/promises";
|
|
6461
|
-
import { join as
|
|
6970
|
+
import { join as join11, extname as extname3 } from "path";
|
|
6462
6971
|
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
6463
6972
|
".git",
|
|
6464
6973
|
"node_modules",
|
|
@@ -6481,7 +6990,7 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
|
|
|
6481
6990
|
}
|
|
6482
6991
|
for (const entry of entries) {
|
|
6483
6992
|
if (skipDirs.has(entry)) continue;
|
|
6484
|
-
const fullPath =
|
|
6993
|
+
const fullPath = join11(dir, entry);
|
|
6485
6994
|
const s = await stat7(fullPath).catch(() => null);
|
|
6486
6995
|
if (!s) continue;
|
|
6487
6996
|
if (s.isDirectory()) {
|
|
@@ -6574,7 +7083,7 @@ var secretsScanner = {
|
|
|
6574
7083
|
for (const filePath of files) {
|
|
6575
7084
|
let content;
|
|
6576
7085
|
try {
|
|
6577
|
-
content = await
|
|
7086
|
+
content = await readFile14(filePath, "utf-8");
|
|
6578
7087
|
} catch {
|
|
6579
7088
|
continue;
|
|
6580
7089
|
}
|
|
@@ -6602,7 +7111,7 @@ var secretsScanner = {
|
|
|
6602
7111
|
};
|
|
6603
7112
|
|
|
6604
7113
|
// src/preflight/scanners/billing-scanner.ts
|
|
6605
|
-
import { readFile as
|
|
7114
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
6606
7115
|
var BILLING_PATTERNS = [
|
|
6607
7116
|
{
|
|
6608
7117
|
ruleId: "billing-stripe-sdk",
|
|
@@ -6663,7 +7172,7 @@ var billingScanner = {
|
|
|
6663
7172
|
for (const filePath of files) {
|
|
6664
7173
|
let content;
|
|
6665
7174
|
try {
|
|
6666
|
-
content = await
|
|
7175
|
+
content = await readFile15(filePath, "utf-8");
|
|
6667
7176
|
} catch {
|
|
6668
7177
|
continue;
|
|
6669
7178
|
}
|
|
@@ -6689,7 +7198,7 @@ var billingScanner = {
|
|
|
6689
7198
|
};
|
|
6690
7199
|
|
|
6691
7200
|
// src/preflight/scanners/privacy-scanner.ts
|
|
6692
|
-
import { readFile as
|
|
7201
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
6693
7202
|
var TRACKING_SDKS = [
|
|
6694
7203
|
{
|
|
6695
7204
|
name: "Facebook SDK",
|
|
@@ -6729,7 +7238,7 @@ var privacyScanner = {
|
|
|
6729
7238
|
for (const filePath of files) {
|
|
6730
7239
|
let content;
|
|
6731
7240
|
try {
|
|
6732
|
-
content = await
|
|
7241
|
+
content = await readFile16(filePath, "utf-8");
|
|
6733
7242
|
} catch {
|
|
6734
7243
|
continue;
|
|
6735
7244
|
}
|
|
@@ -6879,6 +7388,19 @@ var policyScanner = {
|
|
|
6879
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.",
|
|
6880
7389
|
policyUrl: "https://developer.android.com/developer-verification"
|
|
6881
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
|
+
}
|
|
6882
7404
|
if (perms.has("android.permission.SYSTEM_ALERT_WINDOW")) {
|
|
6883
7405
|
findings.push({
|
|
6884
7406
|
scanner: "policy",
|
|
@@ -7091,13 +7613,13 @@ async function runPreflight(options) {
|
|
|
7091
7613
|
|
|
7092
7614
|
// src/commands/plugin-scaffold.ts
|
|
7093
7615
|
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
7094
|
-
import { join as
|
|
7616
|
+
import { join as join12 } from "path";
|
|
7095
7617
|
async function scaffoldPlugin(options) {
|
|
7096
7618
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
7097
7619
|
const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
|
|
7098
7620
|
const shortName = pluginName.replace(/^gpc-plugin-/, "");
|
|
7099
|
-
const srcDir =
|
|
7100
|
-
const testDir =
|
|
7621
|
+
const srcDir = join12(dir, "src");
|
|
7622
|
+
const testDir = join12(dir, "tests");
|
|
7101
7623
|
await mkdir8(srcDir, { recursive: true });
|
|
7102
7624
|
await mkdir8(testDir, { recursive: true });
|
|
7103
7625
|
const files = [];
|
|
@@ -7115,6 +7637,9 @@ async function scaffoldPlugin(options) {
|
|
|
7115
7637
|
}
|
|
7116
7638
|
},
|
|
7117
7639
|
files: ["dist"],
|
|
7640
|
+
gpc: {
|
|
7641
|
+
permissions: ["hooks:beforeCommand", "hooks:afterCommand"]
|
|
7642
|
+
},
|
|
7118
7643
|
scripts: {
|
|
7119
7644
|
build: "tsup src/index.ts --format esm --dts",
|
|
7120
7645
|
dev: "tsup src/index.ts --format esm --dts --watch",
|
|
@@ -7133,7 +7658,7 @@ async function scaffoldPlugin(options) {
|
|
|
7133
7658
|
vitest: "^3.0.0"
|
|
7134
7659
|
}
|
|
7135
7660
|
};
|
|
7136
|
-
await writeFile8(
|
|
7661
|
+
await writeFile8(join12(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
7137
7662
|
files.push("package.json");
|
|
7138
7663
|
const tsconfig = {
|
|
7139
7664
|
compilerOptions: {
|
|
@@ -7149,7 +7674,7 @@ async function scaffoldPlugin(options) {
|
|
|
7149
7674
|
},
|
|
7150
7675
|
include: ["src"]
|
|
7151
7676
|
};
|
|
7152
|
-
await writeFile8(
|
|
7677
|
+
await writeFile8(join12(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
7153
7678
|
files.push("tsconfig.json");
|
|
7154
7679
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
7155
7680
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -7169,7 +7694,8 @@ export const plugin = definePlugin({
|
|
|
7169
7694
|
// Example: send notifications, update dashboards, etc.
|
|
7170
7695
|
});
|
|
7171
7696
|
|
|
7172
|
-
//
|
|
7697
|
+
// To add custom commands, first add "commands:register" to
|
|
7698
|
+
// package.json#gpc.permissions, then uncomment this block:
|
|
7173
7699
|
// hooks.registerCommands((registry) => {
|
|
7174
7700
|
// registry.add({
|
|
7175
7701
|
// name: "${shortName}",
|
|
@@ -7182,7 +7708,7 @@ export const plugin = definePlugin({
|
|
|
7182
7708
|
},
|
|
7183
7709
|
});
|
|
7184
7710
|
`;
|
|
7185
|
-
await writeFile8(
|
|
7711
|
+
await writeFile8(join12(srcDir, "index.ts"), srcContent);
|
|
7186
7712
|
files.push("src/index.ts");
|
|
7187
7713
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
7188
7714
|
import { plugin } from "../src/index";
|
|
@@ -7207,7 +7733,7 @@ describe("${pluginName}", () => {
|
|
|
7207
7733
|
});
|
|
7208
7734
|
});
|
|
7209
7735
|
`;
|
|
7210
|
-
await writeFile8(
|
|
7736
|
+
await writeFile8(join12(testDir, "plugin.test.ts"), testContent);
|
|
7211
7737
|
files.push("tests/plugin.test.ts");
|
|
7212
7738
|
return { dir, files };
|
|
7213
7739
|
}
|
|
@@ -7398,7 +7924,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
|
|
|
7398
7924
|
}
|
|
7399
7925
|
|
|
7400
7926
|
// src/commands/bundle-analysis.ts
|
|
7401
|
-
import { readFile as
|
|
7927
|
+
import { readFile as readFile17, stat as stat8 } from "fs/promises";
|
|
7402
7928
|
var EOCD_SIGNATURE = 101010256;
|
|
7403
7929
|
var CD_SIGNATURE = 33639248;
|
|
7404
7930
|
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
@@ -7478,7 +8004,7 @@ async function analyzeBundle(filePath) {
|
|
|
7478
8004
|
if (!fileInfo || !fileInfo.isFile()) {
|
|
7479
8005
|
throw new Error(`File not found: ${filePath}`);
|
|
7480
8006
|
}
|
|
7481
|
-
const buf = await
|
|
8007
|
+
const buf = await readFile17(filePath);
|
|
7482
8008
|
const cdEntries = parseCentralDirectory(buf);
|
|
7483
8009
|
const fileType = detectFileType2(filePath);
|
|
7484
8010
|
const isAab = fileType === "aab";
|
|
@@ -7564,7 +8090,7 @@ function topFiles(analysis, n = 20) {
|
|
|
7564
8090
|
async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
|
|
7565
8091
|
let config;
|
|
7566
8092
|
try {
|
|
7567
|
-
const raw = await
|
|
8093
|
+
const raw = await readFile17(configPath, "utf-8");
|
|
7568
8094
|
config = JSON.parse(raw);
|
|
7569
8095
|
} catch {
|
|
7570
8096
|
return { passed: true, violations: [] };
|
|
@@ -8926,7 +9452,7 @@ async function sha256File(filePath) {
|
|
|
8926
9452
|
|
|
8927
9453
|
// src/commands/image-sync.ts
|
|
8928
9454
|
import { readdir as readdir7, stat as stat10 } from "fs/promises";
|
|
8929
|
-
import { join as
|
|
9455
|
+
import { join as join13, extname as extname5 } from "path";
|
|
8930
9456
|
import { PlayApiError } from "@gpc-cli/api";
|
|
8931
9457
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
8932
9458
|
var ALL_IMAGE_TYPES2 = [
|
|
@@ -8981,7 +9507,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8981
9507
|
try {
|
|
8982
9508
|
for (const language of languages) {
|
|
8983
9509
|
for (const imageType of imageTypes) {
|
|
8984
|
-
const localDir =
|
|
9510
|
+
const localDir = join13(dir, language, imageType);
|
|
8985
9511
|
const localFiles = await scanLocalImages(localDir);
|
|
8986
9512
|
let remoteImages;
|
|
8987
9513
|
try {
|
|
@@ -8996,12 +9522,12 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
8996
9522
|
const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
|
|
8997
9523
|
const localHashes = /* @__PURE__ */ new Map();
|
|
8998
9524
|
for (const file of localFiles) {
|
|
8999
|
-
const hash = await sha256File(
|
|
9525
|
+
const hash = await sha256File(join13(localDir, file));
|
|
9000
9526
|
localHashes.set(file, hash);
|
|
9001
9527
|
}
|
|
9002
9528
|
const uploadLocal = async (file, reason) => {
|
|
9003
9529
|
if (!options?.dryRun) {
|
|
9004
|
-
const filePath =
|
|
9530
|
+
const filePath = join13(localDir, file);
|
|
9005
9531
|
const check = await validateImage(filePath, imageType);
|
|
9006
9532
|
if (!check.valid) {
|
|
9007
9533
|
throw new GpcError(
|
|
@@ -9035,7 +9561,13 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
9035
9561
|
if (inOrder) {
|
|
9036
9562
|
for (const file of localFiles) {
|
|
9037
9563
|
skipped++;
|
|
9038
|
-
details.push({
|
|
9564
|
+
details.push({
|
|
9565
|
+
language,
|
|
9566
|
+
imageType,
|
|
9567
|
+
file,
|
|
9568
|
+
action: "skip",
|
|
9569
|
+
reason: "already in order"
|
|
9570
|
+
});
|
|
9039
9571
|
}
|
|
9040
9572
|
continue;
|
|
9041
9573
|
}
|
|
@@ -9045,7 +9577,13 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
9045
9577
|
}
|
|
9046
9578
|
for (const img of remoteImages) {
|
|
9047
9579
|
deleted++;
|
|
9048
|
-
details.push({
|
|
9580
|
+
details.push({
|
|
9581
|
+
language,
|
|
9582
|
+
imageType,
|
|
9583
|
+
file: img.id,
|
|
9584
|
+
action: "delete",
|
|
9585
|
+
reason: "reordering"
|
|
9586
|
+
});
|
|
9049
9587
|
}
|
|
9050
9588
|
}
|
|
9051
9589
|
for (const file of localFiles) {
|
|
@@ -9444,7 +9982,9 @@ export {
|
|
|
9444
9982
|
createTranslator,
|
|
9445
9983
|
deactivateBasePlan,
|
|
9446
9984
|
deactivateOffer,
|
|
9985
|
+
decodeCsvText,
|
|
9447
9986
|
decodeNotification,
|
|
9987
|
+
decodeStatsCsv,
|
|
9448
9988
|
defaultGitRunner,
|
|
9449
9989
|
deferSubscriptionPurchase,
|
|
9450
9990
|
deferSubscriptionV2,
|
|
@@ -9470,12 +10010,16 @@ export {
|
|
|
9470
10010
|
diffOneTimeProduct,
|
|
9471
10011
|
diffReleases,
|
|
9472
10012
|
diffSubscription,
|
|
10013
|
+
discoverPluginEntries,
|
|
9473
10014
|
discoverPlugins,
|
|
9474
10015
|
dispatchWebhook,
|
|
10016
|
+
downloadFinancialReport,
|
|
9475
10017
|
downloadGeneratedApk,
|
|
9476
|
-
|
|
10018
|
+
downloadReportObject,
|
|
10019
|
+
downloadStatsReport,
|
|
9477
10020
|
exportImages,
|
|
9478
10021
|
exportReviews,
|
|
10022
|
+
extractCsvEntriesFromZip,
|
|
9479
10023
|
fetchAggregateCost,
|
|
9480
10024
|
fetchChangelog,
|
|
9481
10025
|
fetchReleaseNotes,
|
|
@@ -9534,6 +10078,7 @@ export {
|
|
|
9534
10078
|
getVitalsRendering,
|
|
9535
10079
|
getVitalsStartup,
|
|
9536
10080
|
gradeFromPercent,
|
|
10081
|
+
gunzipIfNeeded,
|
|
9537
10082
|
handleBreach,
|
|
9538
10083
|
importDataSafety,
|
|
9539
10084
|
importTestersFromCsv,
|
|
@@ -9541,11 +10086,13 @@ export {
|
|
|
9541
10086
|
initProject,
|
|
9542
10087
|
inviteUser,
|
|
9543
10088
|
isFinancialReportType,
|
|
10089
|
+
isGzip,
|
|
9544
10090
|
isStatsReportType,
|
|
9545
10091
|
isValidBcp47,
|
|
9546
10092
|
isValidReportType,
|
|
9547
10093
|
isValidStatsDimension,
|
|
9548
10094
|
isVersionedNotesDir,
|
|
10095
|
+
isZip,
|
|
9549
10096
|
lintListing,
|
|
9550
10097
|
lintListings,
|
|
9551
10098
|
lintLocalListings,
|
|
@@ -9564,6 +10111,7 @@ export {
|
|
|
9564
10111
|
listOneTimeOffers,
|
|
9565
10112
|
listOneTimeProducts,
|
|
9566
10113
|
listRecoveryActions,
|
|
10114
|
+
listReportObjects,
|
|
9567
10115
|
listReports,
|
|
9568
10116
|
listReviews,
|
|
9569
10117
|
listSubscriptions,
|
|
@@ -9575,6 +10123,7 @@ export {
|
|
|
9575
10123
|
loadStatusCache,
|
|
9576
10124
|
maybePaginate,
|
|
9577
10125
|
migratePrices,
|
|
10126
|
+
monthToken,
|
|
9578
10127
|
moreResultsFooter,
|
|
9579
10128
|
normalizeFingerprint,
|
|
9580
10129
|
parseAppfile,
|
|
@@ -9616,8 +10165,13 @@ export {
|
|
|
9616
10165
|
renderPlayStorePrompt,
|
|
9617
10166
|
renderPrompt,
|
|
9618
10167
|
replyToReview,
|
|
10168
|
+
reportAccessDeniedError,
|
|
10169
|
+
reportBucketNotFoundError,
|
|
10170
|
+
reportObjectNotFoundError,
|
|
10171
|
+
reportPrefix,
|
|
9619
10172
|
resolveAiConfig,
|
|
9620
10173
|
resolveLocales,
|
|
10174
|
+
resolveReportsBucket,
|
|
9621
10175
|
retryOnUploadNotComplete,
|
|
9622
10176
|
revokeSubscriptionPurchase,
|
|
9623
10177
|
runPreflight,
|
|
@@ -9667,6 +10221,7 @@ export {
|
|
|
9667
10221
|
validateImage,
|
|
9668
10222
|
validateLanguageCode,
|
|
9669
10223
|
validatePackageName,
|
|
10224
|
+
validatePluginForApproval,
|
|
9670
10225
|
validatePreSubmission,
|
|
9671
10226
|
validateReleaseNotes,
|
|
9672
10227
|
validateSku,
|