@gpc-cli/core 0.9.58 → 0.9.60
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/{chunk-QIYAOW6R.js → chunk-3AUJEAXP.js} +189 -41
- package/dist/chunk-3AUJEAXP.js.map +1 -0
- package/dist/index.d.ts +43 -2
- package/dist/index.js +338 -167
- package/dist/index.js.map +1 -1
- package/dist/{releases-I5MYFNCV.js → releases-S54GLWH3.js} +2 -2
- package/package.json +2 -2
- package/dist/chunk-QIYAOW6R.js.map +0 -1
- /package/dist/{releases-I5MYFNCV.js.map → releases-S54GLWH3.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -4,19 +4,25 @@ import {
|
|
|
4
4
|
GpcError,
|
|
5
5
|
NetworkError,
|
|
6
6
|
applyReleaseNotes,
|
|
7
|
+
commitWithRescue,
|
|
7
8
|
createTrack,
|
|
8
9
|
diffReleases,
|
|
9
10
|
fetchReleaseNotes,
|
|
10
11
|
getReleasesStatus,
|
|
12
|
+
isVersionedNotesDir,
|
|
11
13
|
listTracks,
|
|
12
14
|
promoteRelease,
|
|
15
|
+
readReleaseNotesForVersion,
|
|
16
|
+
readReleaseNotesFromDir,
|
|
13
17
|
updateRollout,
|
|
14
18
|
updateTrackConfig,
|
|
15
19
|
uploadExternallyHosted,
|
|
16
20
|
uploadRelease,
|
|
21
|
+
validateAndCommit,
|
|
22
|
+
validateReleaseNotes,
|
|
17
23
|
validateUploadFile,
|
|
18
24
|
waitForBundleProcessing
|
|
19
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-3AUJEAXP.js";
|
|
20
26
|
|
|
21
27
|
// src/output.ts
|
|
22
28
|
import process2 from "process";
|
|
@@ -915,10 +921,7 @@ async function updateListing(client, packageName, language, data, commitOptions)
|
|
|
915
921
|
const edit = await client.edits.insert(packageName);
|
|
916
922
|
try {
|
|
917
923
|
const listing = await client.listings.patch(packageName, edit.id, language, data);
|
|
918
|
-
|
|
919
|
-
await client.edits.validate(packageName, edit.id);
|
|
920
|
-
}
|
|
921
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
924
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
922
925
|
return listing;
|
|
923
926
|
} catch (error) {
|
|
924
927
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
@@ -931,10 +934,7 @@ async function deleteListing(client, packageName, language, commitOptions) {
|
|
|
931
934
|
const edit = await client.edits.insert(packageName);
|
|
932
935
|
try {
|
|
933
936
|
await client.listings.delete(packageName, edit.id, language);
|
|
934
|
-
|
|
935
|
-
await client.edits.validate(packageName, edit.id);
|
|
936
|
-
}
|
|
937
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
937
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
938
938
|
} catch (error) {
|
|
939
939
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
940
940
|
});
|
|
@@ -1057,10 +1057,7 @@ ${details}`,
|
|
|
1057
1057
|
const { language, ...data } = listing;
|
|
1058
1058
|
await client.listings.update(packageName, edit.id, language, data);
|
|
1059
1059
|
}
|
|
1060
|
-
|
|
1061
|
-
await client.edits.validate(packageName, edit.id);
|
|
1062
|
-
}
|
|
1063
|
-
await client.edits.commit(packageName, edit.id, options?.commitOptions);
|
|
1060
|
+
await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
|
|
1064
1061
|
return {
|
|
1065
1062
|
updated: localListings.length,
|
|
1066
1063
|
languages: localListings.map((l) => l.language)
|
|
@@ -1101,10 +1098,7 @@ async function uploadImage(client, packageName, language, imageType, filePath, c
|
|
|
1101
1098
|
const edit = await client.edits.insert(packageName);
|
|
1102
1099
|
try {
|
|
1103
1100
|
const image = await client.images.upload(packageName, edit.id, language, imageType, filePath);
|
|
1104
|
-
|
|
1105
|
-
await client.edits.validate(packageName, edit.id);
|
|
1106
|
-
}
|
|
1107
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
1101
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
1108
1102
|
return image;
|
|
1109
1103
|
} catch (error) {
|
|
1110
1104
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
@@ -1117,10 +1111,7 @@ async function deleteImage(client, packageName, language, imageType, imageId, co
|
|
|
1117
1111
|
const edit = await client.edits.insert(packageName);
|
|
1118
1112
|
try {
|
|
1119
1113
|
await client.images.delete(packageName, edit.id, language, imageType, imageId);
|
|
1120
|
-
|
|
1121
|
-
await client.edits.validate(packageName, edit.id);
|
|
1122
|
-
}
|
|
1123
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
1114
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
1124
1115
|
} catch (error) {
|
|
1125
1116
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
1126
1117
|
});
|
|
@@ -1233,10 +1224,7 @@ async function updateAppDetails(client, packageName, details, commitOptions) {
|
|
|
1233
1224
|
const edit = await client.edits.insert(packageName);
|
|
1234
1225
|
try {
|
|
1235
1226
|
const result = await client.details.patch(packageName, edit.id, details);
|
|
1236
|
-
|
|
1237
|
-
await client.edits.validate(packageName, edit.id);
|
|
1238
|
-
}
|
|
1239
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
1227
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
1240
1228
|
return result;
|
|
1241
1229
|
} catch (error) {
|
|
1242
1230
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
@@ -1529,55 +1517,8 @@ function validateSku(sku) {
|
|
|
1529
1517
|
}
|
|
1530
1518
|
}
|
|
1531
1519
|
|
|
1532
|
-
// src/utils/release-notes.ts
|
|
1533
|
-
import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
1534
|
-
import { extname as extname2, basename, join as join3 } from "path";
|
|
1535
|
-
var MAX_NOTES_LENGTH = 500;
|
|
1536
|
-
async function readReleaseNotesFromDir(dir) {
|
|
1537
|
-
let entries;
|
|
1538
|
-
try {
|
|
1539
|
-
entries = await readdir3(dir);
|
|
1540
|
-
} catch {
|
|
1541
|
-
throw new GpcError(
|
|
1542
|
-
`Release notes directory not found: ${dir}`,
|
|
1543
|
-
"RELEASE_NOTES_DIR_NOT_FOUND",
|
|
1544
|
-
1,
|
|
1545
|
-
`Create the directory and add .txt files named by language code (e.g., en-US.txt). Path: ${dir}`
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
const notes = [];
|
|
1549
|
-
for (const entry of entries) {
|
|
1550
|
-
if (extname2(entry) !== ".txt") continue;
|
|
1551
|
-
const language = basename(entry, ".txt");
|
|
1552
|
-
const filePath = join3(dir, entry);
|
|
1553
|
-
const stats = await stat3(filePath);
|
|
1554
|
-
if (!stats.isFile()) continue;
|
|
1555
|
-
const text = (await readFile3(filePath, "utf-8")).trim();
|
|
1556
|
-
if (text.length === 0) continue;
|
|
1557
|
-
notes.push({ language, text });
|
|
1558
|
-
}
|
|
1559
|
-
return notes;
|
|
1560
|
-
}
|
|
1561
|
-
function validateReleaseNotes(notes) {
|
|
1562
|
-
const errors = [];
|
|
1563
|
-
const warnings = [];
|
|
1564
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1565
|
-
for (const note of notes) {
|
|
1566
|
-
if (seen.has(note.language)) {
|
|
1567
|
-
errors.push(`Duplicate language code: ${note.language}`);
|
|
1568
|
-
}
|
|
1569
|
-
seen.add(note.language);
|
|
1570
|
-
if (note.text.length > MAX_NOTES_LENGTH) {
|
|
1571
|
-
warnings.push(
|
|
1572
|
-
`Release notes for "${note.language}" are ${note.text.length} chars (max ${MAX_NOTES_LENGTH}) \u2014 Google Play will reject notes exceeding this limit`
|
|
1573
|
-
);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
1520
|
// src/commands/validate.ts
|
|
1580
|
-
import { stat as
|
|
1521
|
+
import { stat as stat3 } from "fs/promises";
|
|
1581
1522
|
var STANDARD_TRACKS = /* @__PURE__ */ new Set([
|
|
1582
1523
|
"internal",
|
|
1583
1524
|
"qa",
|
|
@@ -1621,7 +1562,7 @@ async function validatePreSubmission(options) {
|
|
|
1621
1562
|
}
|
|
1622
1563
|
if (options.mappingFile) {
|
|
1623
1564
|
try {
|
|
1624
|
-
const stats = await
|
|
1565
|
+
const stats = await stat3(options.mappingFile);
|
|
1625
1566
|
checks.push({
|
|
1626
1567
|
name: "mapping",
|
|
1627
1568
|
passed: stats.isFile(),
|
|
@@ -2420,12 +2361,28 @@ var METRIC_SET_METRICS = {
|
|
|
2420
2361
|
],
|
|
2421
2362
|
errorCountMetricSet: ["errorReportCount", "distinctUsers"]
|
|
2422
2363
|
};
|
|
2423
|
-
function
|
|
2364
|
+
async function getFreshnessEndDate(reporting, packageName, metricSet, aggregation = "DAILY") {
|
|
2365
|
+
try {
|
|
2366
|
+
const info = await reporting.getMetricSetFreshness(packageName, metricSet);
|
|
2367
|
+
const match = info.freshnessInfo?.freshnesses?.find(
|
|
2368
|
+
(f) => f.aggregationPeriod === aggregation
|
|
2369
|
+
);
|
|
2370
|
+
if (match) {
|
|
2371
|
+
return new Date(
|
|
2372
|
+
Date.UTC(match.latestEndTime.year, match.latestEndTime.month - 1, match.latestEndTime.day)
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
} catch {
|
|
2376
|
+
}
|
|
2377
|
+
return void 0;
|
|
2378
|
+
}
|
|
2379
|
+
function buildQuery(metricSet, options, freshnessEnd) {
|
|
2424
2380
|
const metrics = METRIC_SET_METRICS[metricSet] ?? ["errorReportCount", "distinctUsers"];
|
|
2425
2381
|
const days = options?.days ?? 30;
|
|
2426
2382
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2427
|
-
const
|
|
2428
|
-
const
|
|
2383
|
+
const yesterday = new Date(Date.now() - DAY_MS);
|
|
2384
|
+
const end = freshnessEnd && freshnessEnd < yesterday ? freshnessEnd : yesterday;
|
|
2385
|
+
const start = new Date(end.getTime() - days * DAY_MS);
|
|
2429
2386
|
const query = {
|
|
2430
2387
|
metrics,
|
|
2431
2388
|
timelineSpec: {
|
|
@@ -2448,7 +2405,13 @@ function buildQuery(metricSet, options) {
|
|
|
2448
2405
|
return query;
|
|
2449
2406
|
}
|
|
2450
2407
|
async function queryMetric(reporting, packageName, metricSet, options) {
|
|
2451
|
-
const
|
|
2408
|
+
const freshnessEnd = await getFreshnessEndDate(
|
|
2409
|
+
reporting,
|
|
2410
|
+
packageName,
|
|
2411
|
+
metricSet,
|
|
2412
|
+
options?.aggregation
|
|
2413
|
+
);
|
|
2414
|
+
const query = buildQuery(metricSet, options, freshnessEnd);
|
|
2452
2415
|
return reporting.queryMetricSet(packageName, metricSet, query);
|
|
2453
2416
|
}
|
|
2454
2417
|
async function getVitalsOverview(reporting, packageName) {
|
|
@@ -2460,8 +2423,15 @@ async function getVitalsOverview(reporting, packageName) {
|
|
|
2460
2423
|
["excessiveWakeupRateMetricSet", "excessiveWakeupRate"],
|
|
2461
2424
|
["stuckBackgroundWakelockRateMetricSet", "stuckWakelockRate"]
|
|
2462
2425
|
];
|
|
2426
|
+
const freshnessResults = await Promise.allSettled(
|
|
2427
|
+
metricSets.map(([metric]) => getFreshnessEndDate(reporting, packageName, metric))
|
|
2428
|
+
);
|
|
2463
2429
|
const results = await Promise.allSettled(
|
|
2464
|
-
metricSets.map(([metric]) =>
|
|
2430
|
+
metricSets.map(([metric], i) => {
|
|
2431
|
+
const fr = freshnessResults[i];
|
|
2432
|
+
const freshnessEnd = fr?.status === "fulfilled" ? fr.value : void 0;
|
|
2433
|
+
return reporting.queryMetricSet(packageName, metric, buildQuery(metric, void 0, freshnessEnd));
|
|
2434
|
+
})
|
|
2465
2435
|
);
|
|
2466
2436
|
const overview = {};
|
|
2467
2437
|
for (let i = 0; i < metricSets.length; i++) {
|
|
@@ -2514,7 +2484,9 @@ async function searchVitalsErrors(reporting, packageName, options) {
|
|
|
2514
2484
|
async function compareVitalsTrend(reporting, packageName, metricSet, days = 7) {
|
|
2515
2485
|
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2516
2486
|
const nowMs = Date.now();
|
|
2517
|
-
const
|
|
2487
|
+
const freshnessEnd = await getFreshnessEndDate(reporting, packageName, metricSet);
|
|
2488
|
+
const fallback = nowMs - 2 * DAY_MS;
|
|
2489
|
+
const baseMs = freshnessEnd ? Math.min(freshnessEnd.getTime(), fallback) : fallback;
|
|
2518
2490
|
const currentEnd = new Date(baseMs);
|
|
2519
2491
|
const currentStart = new Date(baseMs - days * DAY_MS);
|
|
2520
2492
|
const previousEnd = new Date(baseMs - days * DAY_MS - DAY_MS);
|
|
@@ -2660,17 +2632,17 @@ function watchVitalsWithAutoHalt(reporting, packageName, options) {
|
|
|
2660
2632
|
}
|
|
2661
2633
|
|
|
2662
2634
|
// src/commands/status.ts
|
|
2663
|
-
import { mkdir as mkdir3, readFile as
|
|
2635
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
2664
2636
|
import { execFile } from "child_process";
|
|
2665
|
-
import { join as
|
|
2637
|
+
import { join as join3 } from "path";
|
|
2666
2638
|
import { getCacheDir } from "@gpc-cli/config";
|
|
2667
2639
|
var DEFAULT_TTL_SECONDS = 3600;
|
|
2668
2640
|
function cacheFilePath(packageName) {
|
|
2669
|
-
return
|
|
2641
|
+
return join3(getCacheDir(), `status-${packageName}.json`);
|
|
2670
2642
|
}
|
|
2671
2643
|
async function loadStatusCache(packageName, ttlSeconds = DEFAULT_TTL_SECONDS) {
|
|
2672
2644
|
try {
|
|
2673
|
-
const raw = await
|
|
2645
|
+
const raw = await readFile3(cacheFilePath(packageName), "utf-8");
|
|
2674
2646
|
const entry = JSON.parse(raw);
|
|
2675
2647
|
const age = (Date.now() - new Date(entry.fetchedAt).getTime()) / 1e3;
|
|
2676
2648
|
if (age > (entry.ttl ?? ttlSeconds)) return null;
|
|
@@ -3079,13 +3051,13 @@ async function runWatchLoop(opts) {
|
|
|
3079
3051
|
}
|
|
3080
3052
|
}
|
|
3081
3053
|
function breachStateFilePath(packageName) {
|
|
3082
|
-
return
|
|
3054
|
+
return join3(getCacheDir(), `breach-state-${packageName}.json`);
|
|
3083
3055
|
}
|
|
3084
3056
|
async function trackBreachState(packageName, isBreaching) {
|
|
3085
3057
|
const filePath = breachStateFilePath(packageName);
|
|
3086
3058
|
let prevBreaching = false;
|
|
3087
3059
|
try {
|
|
3088
|
-
const raw = await
|
|
3060
|
+
const raw = await readFile3(filePath, "utf-8");
|
|
3089
3061
|
prevBreaching = JSON.parse(raw).breaching;
|
|
3090
3062
|
} catch {
|
|
3091
3063
|
}
|
|
@@ -3238,7 +3210,7 @@ async function handleBreach(event, config, client) {
|
|
|
3238
3210
|
}
|
|
3239
3211
|
case "halt": {
|
|
3240
3212
|
try {
|
|
3241
|
-
const { updateRollout: updateRollout2 } = await import("./releases-
|
|
3213
|
+
const { updateRollout: updateRollout2 } = await import("./releases-S54GLWH3.js");
|
|
3242
3214
|
await updateRollout2(client, config.packageName, config.track, "halt");
|
|
3243
3215
|
halted = true;
|
|
3244
3216
|
} catch {
|
|
@@ -3354,8 +3326,8 @@ async function runWatch(client, reporting, config, callbacks) {
|
|
|
3354
3326
|
}
|
|
3355
3327
|
|
|
3356
3328
|
// src/commands/iap.ts
|
|
3357
|
-
import { readdir as
|
|
3358
|
-
import { join as
|
|
3329
|
+
import { readdir as readdir3, readFile as readFile4 } from "fs/promises";
|
|
3330
|
+
import { join as join4 } from "path";
|
|
3359
3331
|
import { paginateAll as paginateAll3 } from "@gpc-cli/api";
|
|
3360
3332
|
async function listInAppProducts(client, packageName, options) {
|
|
3361
3333
|
if (options?.limit || options?.nextPage) {
|
|
@@ -3440,11 +3412,11 @@ async function batchUpdateProducts(client, packageName, products) {
|
|
|
3440
3412
|
return results;
|
|
3441
3413
|
}
|
|
3442
3414
|
async function readProductsFromDir(dir) {
|
|
3443
|
-
const files = await
|
|
3415
|
+
const files = await readdir3(dir);
|
|
3444
3416
|
const jsonFiles = files.filter((f) => f.endsWith(".json"));
|
|
3445
3417
|
const localProducts = [];
|
|
3446
3418
|
for (const file of jsonFiles) {
|
|
3447
|
-
const content = await
|
|
3419
|
+
const content = await readFile4(join4(dir, file), "utf-8");
|
|
3448
3420
|
try {
|
|
3449
3421
|
localProducts.push(JSON.parse(content));
|
|
3450
3422
|
} catch {
|
|
@@ -3801,7 +3773,7 @@ async function deleteGrant(client, developerId, email, packageName) {
|
|
|
3801
3773
|
}
|
|
3802
3774
|
|
|
3803
3775
|
// src/commands/testers.ts
|
|
3804
|
-
import { readFile as
|
|
3776
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
3805
3777
|
async function listTesters(client, packageName, track) {
|
|
3806
3778
|
const edit = await client.edits.insert(packageName);
|
|
3807
3779
|
try {
|
|
@@ -3822,10 +3794,7 @@ async function addTesters(client, packageName, track, groupEmails, commitOptions
|
|
|
3822
3794
|
const updated = await client.testers.update(packageName, edit.id, track, {
|
|
3823
3795
|
googleGroups: [...existing]
|
|
3824
3796
|
});
|
|
3825
|
-
|
|
3826
|
-
await client.edits.validate(packageName, edit.id);
|
|
3827
|
-
}
|
|
3828
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
3797
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
3829
3798
|
return updated;
|
|
3830
3799
|
} catch (error) {
|
|
3831
3800
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
@@ -3842,10 +3811,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
|
|
|
3842
3811
|
const updated = await client.testers.update(packageName, edit.id, track, {
|
|
3843
3812
|
googleGroups: filtered
|
|
3844
3813
|
});
|
|
3845
|
-
|
|
3846
|
-
await client.edits.validate(packageName, edit.id);
|
|
3847
|
-
}
|
|
3848
|
-
await client.edits.commit(packageName, edit.id, commitOptions);
|
|
3814
|
+
await validateAndCommit(client, packageName, edit.id, commitOptions);
|
|
3849
3815
|
return updated;
|
|
3850
3816
|
} catch (error) {
|
|
3851
3817
|
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
@@ -3854,7 +3820,7 @@ async function removeTesters(client, packageName, track, groupEmails, commitOpti
|
|
|
3854
3820
|
}
|
|
3855
3821
|
}
|
|
3856
3822
|
async function importTestersFromCsv(client, packageName, track, csvPath, commitOptions) {
|
|
3857
|
-
const content = await
|
|
3823
|
+
const content = await readFile5(csvPath, "utf-8");
|
|
3858
3824
|
const emails = content.split(/[,\n\r]+/).map((e) => e.trim()).filter((e) => e.length > 0 && e.includes("@"));
|
|
3859
3825
|
if (emails.length === 0) {
|
|
3860
3826
|
throw new GpcError(
|
|
@@ -3997,12 +3963,12 @@ async function addRecoveryTargeting(client, packageName, actionId, targeting) {
|
|
|
3997
3963
|
}
|
|
3998
3964
|
|
|
3999
3965
|
// src/commands/data-safety.ts
|
|
4000
|
-
import { readFile as
|
|
3966
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
4001
3967
|
async function updateDataSafety(client, packageName, data) {
|
|
4002
3968
|
return client.dataSafety.update(packageName, data);
|
|
4003
3969
|
}
|
|
4004
3970
|
async function importDataSafety(client, packageName, filePath) {
|
|
4005
|
-
const content = await
|
|
3971
|
+
const content = await readFile6(filePath, "utf-8");
|
|
4006
3972
|
let data;
|
|
4007
3973
|
try {
|
|
4008
3974
|
data = JSON.parse(content);
|
|
@@ -4306,16 +4272,16 @@ function createSpinner(message) {
|
|
|
4306
4272
|
}
|
|
4307
4273
|
|
|
4308
4274
|
// src/utils/train-state.ts
|
|
4309
|
-
import { mkdir as mkdir4, readFile as
|
|
4310
|
-
import { dirname, join as
|
|
4275
|
+
import { mkdir as mkdir4, readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
4276
|
+
import { dirname, join as join5 } from "path";
|
|
4311
4277
|
import { getCacheDir as getCacheDir2 } from "@gpc-cli/config";
|
|
4312
4278
|
function stateFile(packageName) {
|
|
4313
|
-
return
|
|
4279
|
+
return join5(getCacheDir2(), `train-${packageName}.json`);
|
|
4314
4280
|
}
|
|
4315
4281
|
async function readTrainState(packageName) {
|
|
4316
4282
|
const path = stateFile(packageName);
|
|
4317
4283
|
try {
|
|
4318
|
-
const raw = await
|
|
4284
|
+
const raw = await readFile7(path, "utf-8");
|
|
4319
4285
|
return JSON.parse(raw);
|
|
4320
4286
|
} catch {
|
|
4321
4287
|
return null;
|
|
@@ -4495,8 +4461,8 @@ async function publishEnterpriseApp(client, params) {
|
|
|
4495
4461
|
}
|
|
4496
4462
|
|
|
4497
4463
|
// src/audit.ts
|
|
4498
|
-
import { appendFile, chmod, mkdir as mkdir5, readFile as
|
|
4499
|
-
import { join as
|
|
4464
|
+
import { appendFile, chmod, mkdir as mkdir5, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
|
|
4465
|
+
import { join as join6 } from "path";
|
|
4500
4466
|
var auditDir = null;
|
|
4501
4467
|
function initAudit(configDir) {
|
|
4502
4468
|
auditDir = configDir;
|
|
@@ -4505,7 +4471,7 @@ async function writeAuditLog(entry) {
|
|
|
4505
4471
|
if (!auditDir) return;
|
|
4506
4472
|
try {
|
|
4507
4473
|
await mkdir5(auditDir, { recursive: true, mode: 448 });
|
|
4508
|
-
const logPath =
|
|
4474
|
+
const logPath = join6(auditDir, "audit.log");
|
|
4509
4475
|
const redactedEntry = redactAuditArgs(entry);
|
|
4510
4476
|
const line = JSON.stringify(redactedEntry) + "\n";
|
|
4511
4477
|
await appendFile(logPath, line, { encoding: "utf-8", mode: 384 });
|
|
@@ -4560,10 +4526,10 @@ function createAuditEntry(command, args, app) {
|
|
|
4560
4526
|
}
|
|
4561
4527
|
async function listAuditEvents(options) {
|
|
4562
4528
|
if (!auditDir) return [];
|
|
4563
|
-
const logPath =
|
|
4529
|
+
const logPath = join6(auditDir, "audit.log");
|
|
4564
4530
|
let content;
|
|
4565
4531
|
try {
|
|
4566
|
-
content = await
|
|
4532
|
+
content = await readFile8(logPath, "utf-8");
|
|
4567
4533
|
} catch {
|
|
4568
4534
|
return [];
|
|
4569
4535
|
}
|
|
@@ -4598,10 +4564,10 @@ async function searchAuditEvents(query) {
|
|
|
4598
4564
|
}
|
|
4599
4565
|
async function clearAuditLog(options) {
|
|
4600
4566
|
if (!auditDir) return { deleted: 0, remaining: 0 };
|
|
4601
|
-
const logPath =
|
|
4567
|
+
const logPath = join6(auditDir, "audit.log");
|
|
4602
4568
|
let content;
|
|
4603
4569
|
try {
|
|
4604
|
-
content = await
|
|
4570
|
+
content = await readFile8(logPath, "utf-8");
|
|
4605
4571
|
} catch {
|
|
4606
4572
|
return { deleted: 0, remaining: 0 };
|
|
4607
4573
|
}
|
|
@@ -4689,11 +4655,11 @@ function safePathWithin(userPath, baseDir) {
|
|
|
4689
4655
|
}
|
|
4690
4656
|
|
|
4691
4657
|
// src/commands/init.ts
|
|
4692
|
-
import { mkdir as mkdir6, writeFile as writeFile6, stat as
|
|
4693
|
-
import { join as
|
|
4658
|
+
import { mkdir as mkdir6, writeFile as writeFile6, stat as stat4 } from "fs/promises";
|
|
4659
|
+
import { join as join7 } from "path";
|
|
4694
4660
|
async function exists2(path) {
|
|
4695
4661
|
try {
|
|
4696
|
-
await
|
|
4662
|
+
await stat4(path);
|
|
4697
4663
|
return true;
|
|
4698
4664
|
} catch {
|
|
4699
4665
|
return false;
|
|
@@ -4725,7 +4691,7 @@ async function initProject(options) {
|
|
|
4725
4691
|
null,
|
|
4726
4692
|
2
|
|
4727
4693
|
);
|
|
4728
|
-
await safeWrite(
|
|
4694
|
+
await safeWrite(join7(dir, ".gpcrc.json"), gpcrc + "\n", created, skipped, skipExisting);
|
|
4729
4695
|
const preflightrc = JSON.stringify(
|
|
4730
4696
|
{
|
|
4731
4697
|
failOn: "error",
|
|
@@ -4739,27 +4705,27 @@ async function initProject(options) {
|
|
|
4739
4705
|
2
|
|
4740
4706
|
);
|
|
4741
4707
|
await safeWrite(
|
|
4742
|
-
|
|
4708
|
+
join7(dir, ".preflightrc.json"),
|
|
4743
4709
|
preflightrc + "\n",
|
|
4744
4710
|
created,
|
|
4745
4711
|
skipped,
|
|
4746
4712
|
skipExisting
|
|
4747
4713
|
);
|
|
4748
|
-
const metaDir =
|
|
4749
|
-
await safeWrite(
|
|
4750
|
-
await safeWrite(
|
|
4751
|
-
await safeWrite(
|
|
4752
|
-
await safeWrite(
|
|
4753
|
-
const ssDir =
|
|
4714
|
+
const metaDir = join7(dir, "metadata", "android", "en-US");
|
|
4715
|
+
await safeWrite(join7(metaDir, "title.txt"), "", created, skipped, skipExisting);
|
|
4716
|
+
await safeWrite(join7(metaDir, "short_description.txt"), "", created, skipped, skipExisting);
|
|
4717
|
+
await safeWrite(join7(metaDir, "full_description.txt"), "", created, skipped, skipExisting);
|
|
4718
|
+
await safeWrite(join7(metaDir, "video.txt"), "", created, skipped, skipExisting);
|
|
4719
|
+
const ssDir = join7(metaDir, "images", "phoneScreenshots");
|
|
4754
4720
|
await mkdir6(ssDir, { recursive: true });
|
|
4755
|
-
await safeWrite(
|
|
4721
|
+
await safeWrite(join7(ssDir, ".gitkeep"), "", created, skipped, skipExisting);
|
|
4756
4722
|
if (ci === "github") {
|
|
4757
4723
|
const workflow = githubActionsTemplate(pkg);
|
|
4758
|
-
const workflowDir =
|
|
4759
|
-
await safeWrite(
|
|
4724
|
+
const workflowDir = join7(dir, ".github", "workflows");
|
|
4725
|
+
await safeWrite(join7(workflowDir, "gpc-release.yml"), workflow, created, skipped, skipExisting);
|
|
4760
4726
|
} else if (ci === "gitlab") {
|
|
4761
4727
|
const pipeline = gitlabCiTemplate(pkg);
|
|
4762
|
-
await safeWrite(
|
|
4728
|
+
await safeWrite(join7(dir, ".gitlab-ci-gpc.yml"), pipeline, created, skipped, skipExisting);
|
|
4763
4729
|
}
|
|
4764
4730
|
return { created, skipped };
|
|
4765
4731
|
}
|
|
@@ -4854,13 +4820,13 @@ var DEFAULT_PREFLIGHT_CONFIG = {
|
|
|
4854
4820
|
};
|
|
4855
4821
|
|
|
4856
4822
|
// src/preflight/config.ts
|
|
4857
|
-
import { readFile as
|
|
4823
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
4858
4824
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "error", "warning", "info"]);
|
|
4859
4825
|
async function loadPreflightConfig(configPath) {
|
|
4860
4826
|
const path = configPath || ".preflightrc.json";
|
|
4861
4827
|
let raw;
|
|
4862
4828
|
try {
|
|
4863
|
-
raw = await
|
|
4829
|
+
raw = await readFile9(path, "utf-8");
|
|
4864
4830
|
} catch {
|
|
4865
4831
|
return { ...DEFAULT_PREFLIGHT_CONFIG };
|
|
4866
4832
|
}
|
|
@@ -5777,8 +5743,8 @@ ${fileList}` + (hasPageSizeCompat ? "\nandroid:pageSizeCompat is set, so the app
|
|
|
5777
5743
|
}
|
|
5778
5744
|
|
|
5779
5745
|
// src/preflight/scanners/metadata-scanner.ts
|
|
5780
|
-
import { readdir as
|
|
5781
|
-
import { join as
|
|
5746
|
+
import { readdir as readdir4, stat as stat5, readFile as readFile10 } from "fs/promises";
|
|
5747
|
+
import { join as join8 } from "path";
|
|
5782
5748
|
var SAFE_LANG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
|
|
5783
5749
|
var FILE_MAP2 = {
|
|
5784
5750
|
"title.txt": "title",
|
|
@@ -5804,7 +5770,7 @@ var metadataScanner = {
|
|
|
5804
5770
|
const findings = [];
|
|
5805
5771
|
let entries;
|
|
5806
5772
|
try {
|
|
5807
|
-
entries = await
|
|
5773
|
+
entries = await readdir4(dir);
|
|
5808
5774
|
} catch {
|
|
5809
5775
|
findings.push({
|
|
5810
5776
|
scanner: "metadata",
|
|
@@ -5829,14 +5795,14 @@ var metadataScanner = {
|
|
|
5829
5795
|
return findings;
|
|
5830
5796
|
}
|
|
5831
5797
|
for (const lang of locales) {
|
|
5832
|
-
const langDir =
|
|
5833
|
-
const langStat = await
|
|
5798
|
+
const langDir = join8(dir, lang);
|
|
5799
|
+
const langStat = await stat5(langDir).catch(() => null);
|
|
5834
5800
|
if (!langStat?.isDirectory()) continue;
|
|
5835
5801
|
const fields = {};
|
|
5836
5802
|
for (const [fileName, field] of Object.entries(FILE_MAP2)) {
|
|
5837
|
-
const filePath =
|
|
5803
|
+
const filePath = join8(langDir, fileName);
|
|
5838
5804
|
try {
|
|
5839
|
-
const content = await
|
|
5805
|
+
const content = await readFile10(filePath, "utf-8");
|
|
5840
5806
|
fields[field] = content.trimEnd();
|
|
5841
5807
|
} catch {
|
|
5842
5808
|
}
|
|
@@ -5875,9 +5841,9 @@ var metadataScanner = {
|
|
|
5875
5841
|
let totalScreenshots = 0;
|
|
5876
5842
|
let phoneScreenshots = 0;
|
|
5877
5843
|
for (const ssDir of SCREENSHOT_DIRS) {
|
|
5878
|
-
const ssPath =
|
|
5844
|
+
const ssPath = join8(langDir, "images", ssDir);
|
|
5879
5845
|
try {
|
|
5880
|
-
const ssEntries = await
|
|
5846
|
+
const ssEntries = await readdir4(ssPath);
|
|
5881
5847
|
const imageFiles = ssEntries.filter((f) => /\.(png|jpe?g|webp)$/i.test(f));
|
|
5882
5848
|
totalScreenshots += imageFiles.length;
|
|
5883
5849
|
if (ssDir === "phoneScreenshots") {
|
|
@@ -5907,9 +5873,9 @@ var metadataScanner = {
|
|
|
5907
5873
|
}
|
|
5908
5874
|
}
|
|
5909
5875
|
const defaultLang = locales.includes("en-US") ? "en-US" : locales[0];
|
|
5910
|
-
const privacyPath =
|
|
5876
|
+
const privacyPath = join8(dir, defaultLang, "privacy_policy_url.txt");
|
|
5911
5877
|
try {
|
|
5912
|
-
const url = await
|
|
5878
|
+
const url = await readFile10(privacyPath, "utf-8");
|
|
5913
5879
|
if (!url.trim()) throw new Error("empty");
|
|
5914
5880
|
} catch {
|
|
5915
5881
|
findings.push({
|
|
@@ -5934,11 +5900,11 @@ var metadataScanner = {
|
|
|
5934
5900
|
};
|
|
5935
5901
|
|
|
5936
5902
|
// src/preflight/scanners/secrets-scanner.ts
|
|
5937
|
-
import { readFile as
|
|
5903
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
5938
5904
|
|
|
5939
5905
|
// src/preflight/scan-files.ts
|
|
5940
|
-
import { readdir as
|
|
5941
|
-
import { join as
|
|
5906
|
+
import { readdir as readdir5, stat as stat6 } from "fs/promises";
|
|
5907
|
+
import { join as join9, extname as extname2 } from "path";
|
|
5942
5908
|
var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5943
5909
|
".git",
|
|
5944
5910
|
"node_modules",
|
|
@@ -5955,20 +5921,20 @@ async function collectSourceFiles(dir, extensions, skipDirs = DEFAULT_SKIP_DIRS,
|
|
|
5955
5921
|
const files = [];
|
|
5956
5922
|
let entries;
|
|
5957
5923
|
try {
|
|
5958
|
-
entries = await
|
|
5924
|
+
entries = await readdir5(dir);
|
|
5959
5925
|
} catch {
|
|
5960
5926
|
return files;
|
|
5961
5927
|
}
|
|
5962
5928
|
for (const entry of entries) {
|
|
5963
5929
|
if (skipDirs.has(entry)) continue;
|
|
5964
|
-
const fullPath =
|
|
5965
|
-
const s = await
|
|
5930
|
+
const fullPath = join9(dir, entry);
|
|
5931
|
+
const s = await stat6(fullPath).catch(() => null);
|
|
5966
5932
|
if (!s) continue;
|
|
5967
5933
|
if (s.isDirectory()) {
|
|
5968
5934
|
const sub = await collectSourceFiles(fullPath, extensions, skipDirs, maxDepth - 1);
|
|
5969
5935
|
files.push(...sub);
|
|
5970
5936
|
} else if (s.isFile()) {
|
|
5971
|
-
const ext =
|
|
5937
|
+
const ext = extname2(entry).toLowerCase();
|
|
5972
5938
|
if (extensions.has(ext) || entry.endsWith(".gradle.kts")) {
|
|
5973
5939
|
files.push(fullPath);
|
|
5974
5940
|
}
|
|
@@ -6054,7 +6020,7 @@ var secretsScanner = {
|
|
|
6054
6020
|
for (const filePath of files) {
|
|
6055
6021
|
let content;
|
|
6056
6022
|
try {
|
|
6057
|
-
content = await
|
|
6023
|
+
content = await readFile11(filePath, "utf-8");
|
|
6058
6024
|
} catch {
|
|
6059
6025
|
continue;
|
|
6060
6026
|
}
|
|
@@ -6082,7 +6048,7 @@ var secretsScanner = {
|
|
|
6082
6048
|
};
|
|
6083
6049
|
|
|
6084
6050
|
// src/preflight/scanners/billing-scanner.ts
|
|
6085
|
-
import { readFile as
|
|
6051
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
6086
6052
|
var BILLING_PATTERNS = [
|
|
6087
6053
|
{
|
|
6088
6054
|
ruleId: "billing-stripe-sdk",
|
|
@@ -6143,7 +6109,7 @@ var billingScanner = {
|
|
|
6143
6109
|
for (const filePath of files) {
|
|
6144
6110
|
let content;
|
|
6145
6111
|
try {
|
|
6146
|
-
content = await
|
|
6112
|
+
content = await readFile12(filePath, "utf-8");
|
|
6147
6113
|
} catch {
|
|
6148
6114
|
continue;
|
|
6149
6115
|
}
|
|
@@ -6169,7 +6135,7 @@ var billingScanner = {
|
|
|
6169
6135
|
};
|
|
6170
6136
|
|
|
6171
6137
|
// src/preflight/scanners/privacy-scanner.ts
|
|
6172
|
-
import { readFile as
|
|
6138
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
6173
6139
|
var TRACKING_SDKS = [
|
|
6174
6140
|
{
|
|
6175
6141
|
name: "Facebook SDK",
|
|
@@ -6209,7 +6175,7 @@ var privacyScanner = {
|
|
|
6209
6175
|
for (const filePath of files) {
|
|
6210
6176
|
let content;
|
|
6211
6177
|
try {
|
|
6212
|
-
content = await
|
|
6178
|
+
content = await readFile13(filePath, "utf-8");
|
|
6213
6179
|
} catch {
|
|
6214
6180
|
continue;
|
|
6215
6181
|
}
|
|
@@ -6582,13 +6548,13 @@ function sortResults(items, sortSpec) {
|
|
|
6582
6548
|
|
|
6583
6549
|
// src/commands/plugin-scaffold.ts
|
|
6584
6550
|
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
6585
|
-
import { join as
|
|
6551
|
+
import { join as join10 } from "path";
|
|
6586
6552
|
async function scaffoldPlugin(options) {
|
|
6587
6553
|
const { name, dir, description = `GPC plugin: ${name}` } = options;
|
|
6588
6554
|
const pluginName = name.startsWith("gpc-plugin-") ? name : `gpc-plugin-${name}`;
|
|
6589
6555
|
const shortName = pluginName.replace(/^gpc-plugin-/, "");
|
|
6590
|
-
const srcDir =
|
|
6591
|
-
const testDir =
|
|
6556
|
+
const srcDir = join10(dir, "src");
|
|
6557
|
+
const testDir = join10(dir, "tests");
|
|
6592
6558
|
await mkdir7(srcDir, { recursive: true });
|
|
6593
6559
|
await mkdir7(testDir, { recursive: true });
|
|
6594
6560
|
const files = [];
|
|
@@ -6624,7 +6590,7 @@ async function scaffoldPlugin(options) {
|
|
|
6624
6590
|
vitest: "^3.0.0"
|
|
6625
6591
|
}
|
|
6626
6592
|
};
|
|
6627
|
-
await writeFile7(
|
|
6593
|
+
await writeFile7(join10(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
6628
6594
|
files.push("package.json");
|
|
6629
6595
|
const tsconfig = {
|
|
6630
6596
|
compilerOptions: {
|
|
@@ -6640,7 +6606,7 @@ async function scaffoldPlugin(options) {
|
|
|
6640
6606
|
},
|
|
6641
6607
|
include: ["src"]
|
|
6642
6608
|
};
|
|
6643
|
-
await writeFile7(
|
|
6609
|
+
await writeFile7(join10(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");
|
|
6644
6610
|
files.push("tsconfig.json");
|
|
6645
6611
|
const srcContent = `import { definePlugin } from "@gpc-cli/plugin-sdk";
|
|
6646
6612
|
import type { CommandEvent, CommandResult } from "@gpc-cli/plugin-sdk";
|
|
@@ -6673,7 +6639,7 @@ export const plugin = definePlugin({
|
|
|
6673
6639
|
},
|
|
6674
6640
|
});
|
|
6675
6641
|
`;
|
|
6676
|
-
await writeFile7(
|
|
6642
|
+
await writeFile7(join10(srcDir, "index.ts"), srcContent);
|
|
6677
6643
|
files.push("src/index.ts");
|
|
6678
6644
|
const testContent = `import { describe, it, expect, vi } from "vitest";
|
|
6679
6645
|
import { plugin } from "../src/index";
|
|
@@ -6698,7 +6664,7 @@ describe("${pluginName}", () => {
|
|
|
6698
6664
|
});
|
|
6699
6665
|
});
|
|
6700
6666
|
`;
|
|
6701
|
-
await writeFile7(
|
|
6667
|
+
await writeFile7(join10(testDir, "plugin.test.ts"), testContent);
|
|
6702
6668
|
files.push("tests/plugin.test.ts");
|
|
6703
6669
|
return { dir, files };
|
|
6704
6670
|
}
|
|
@@ -6809,7 +6775,7 @@ async function sendWebhook(config, payload, target) {
|
|
|
6809
6775
|
}
|
|
6810
6776
|
|
|
6811
6777
|
// src/commands/internal-sharing.ts
|
|
6812
|
-
import { extname as
|
|
6778
|
+
import { extname as extname3 } from "path";
|
|
6813
6779
|
async function uploadInternalSharing(client, packageName, filePath, fileType) {
|
|
6814
6780
|
const resolvedType = fileType ?? detectFileType(filePath);
|
|
6815
6781
|
const validation = await validateUploadFile(filePath);
|
|
@@ -6836,7 +6802,7 @@ ${validation.errors.join("\n")}`,
|
|
|
6836
6802
|
};
|
|
6837
6803
|
}
|
|
6838
6804
|
function detectFileType(filePath) {
|
|
6839
|
-
const ext =
|
|
6805
|
+
const ext = extname3(filePath).toLowerCase();
|
|
6840
6806
|
if (ext === ".aab") return "bundle";
|
|
6841
6807
|
if (ext === ".apk") return "apk";
|
|
6842
6808
|
throw new GpcError(
|
|
@@ -6884,7 +6850,7 @@ async function downloadGeneratedApk(client, packageName, versionCode, apkId, out
|
|
|
6884
6850
|
}
|
|
6885
6851
|
|
|
6886
6852
|
// src/commands/bundle-analysis.ts
|
|
6887
|
-
import { readFile as
|
|
6853
|
+
import { readFile as readFile14, stat as stat7 } from "fs/promises";
|
|
6888
6854
|
var EOCD_SIGNATURE = 101010256;
|
|
6889
6855
|
var CD_SIGNATURE = 33639248;
|
|
6890
6856
|
var MODULE_SUBDIRS = /* @__PURE__ */ new Set(["dex", "manifest", "res", "assets", "lib", "resources.pb", "root"]);
|
|
@@ -6960,11 +6926,11 @@ function detectFileType2(filePath) {
|
|
|
6960
6926
|
return "apk";
|
|
6961
6927
|
}
|
|
6962
6928
|
async function analyzeBundle(filePath) {
|
|
6963
|
-
const fileInfo = await
|
|
6929
|
+
const fileInfo = await stat7(filePath).catch(() => null);
|
|
6964
6930
|
if (!fileInfo || !fileInfo.isFile()) {
|
|
6965
6931
|
throw new Error(`File not found: ${filePath}`);
|
|
6966
6932
|
}
|
|
6967
|
-
const buf = await
|
|
6933
|
+
const buf = await readFile14(filePath);
|
|
6968
6934
|
const cdEntries = parseCentralDirectory(buf);
|
|
6969
6935
|
const fileType = detectFileType2(filePath);
|
|
6970
6936
|
const isAab = fileType === "aab";
|
|
@@ -7050,7 +7016,7 @@ function topFiles(analysis, n = 20) {
|
|
|
7050
7016
|
async function checkBundleSize(analysis, configPath = ".bundlesize.json") {
|
|
7051
7017
|
let config;
|
|
7052
7018
|
try {
|
|
7053
|
-
const raw = await
|
|
7019
|
+
const raw = await readFile14(configPath, "utf-8");
|
|
7054
7020
|
config = JSON.parse(raw);
|
|
7055
7021
|
} catch {
|
|
7056
7022
|
return { passed: true, violations: [] };
|
|
@@ -8371,6 +8337,202 @@ function compareFingerprints(a, b) {
|
|
|
8371
8337
|
return normalizeFingerprint(a) === normalizeFingerprint(b);
|
|
8372
8338
|
}
|
|
8373
8339
|
|
|
8340
|
+
// src/utils/hash.ts
|
|
8341
|
+
import { createHash } from "crypto";
|
|
8342
|
+
import { stat as stat8 } from "fs/promises";
|
|
8343
|
+
async function sha256File(filePath) {
|
|
8344
|
+
const hash = createHash("sha256");
|
|
8345
|
+
const { size } = await stat8(filePath);
|
|
8346
|
+
if (size === 0) return hash.digest("hex");
|
|
8347
|
+
const { createReadStream } = await import("fs");
|
|
8348
|
+
const stream = createReadStream(filePath);
|
|
8349
|
+
await new Promise((resolve2, reject) => {
|
|
8350
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
8351
|
+
stream.on("end", resolve2);
|
|
8352
|
+
stream.on("error", reject);
|
|
8353
|
+
});
|
|
8354
|
+
return hash.digest("hex");
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8357
|
+
// src/commands/image-sync.ts
|
|
8358
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
8359
|
+
import { join as join11, extname as extname4 } from "path";
|
|
8360
|
+
import { PlayApiError } from "@gpc-cli/api";
|
|
8361
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
8362
|
+
var ALL_IMAGE_TYPES2 = [
|
|
8363
|
+
"icon",
|
|
8364
|
+
"featureGraphic",
|
|
8365
|
+
"tvBanner",
|
|
8366
|
+
"phoneScreenshots",
|
|
8367
|
+
"sevenInchScreenshots",
|
|
8368
|
+
"tenInchScreenshots",
|
|
8369
|
+
"tvScreenshots",
|
|
8370
|
+
"wearScreenshots"
|
|
8371
|
+
];
|
|
8372
|
+
async function scanLocalImages(dir) {
|
|
8373
|
+
try {
|
|
8374
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
8375
|
+
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname4(e.name).toLowerCase())).map((e) => e.name).sort();
|
|
8376
|
+
} catch {
|
|
8377
|
+
return [];
|
|
8378
|
+
}
|
|
8379
|
+
}
|
|
8380
|
+
async function scanLanguages(dir) {
|
|
8381
|
+
try {
|
|
8382
|
+
const entries = await readdir6(dir, { withFileTypes: true });
|
|
8383
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
8384
|
+
} catch {
|
|
8385
|
+
return [];
|
|
8386
|
+
}
|
|
8387
|
+
}
|
|
8388
|
+
async function syncImages(client, packageName, dir, options) {
|
|
8389
|
+
const details = [];
|
|
8390
|
+
let uploaded = 0;
|
|
8391
|
+
let skipped = 0;
|
|
8392
|
+
let deleted = 0;
|
|
8393
|
+
const languages = options?.lang ? [options.lang] : await scanLanguages(dir);
|
|
8394
|
+
if (languages.length === 0) {
|
|
8395
|
+
throw new GpcError(
|
|
8396
|
+
`No language directories found in "${dir}"`,
|
|
8397
|
+
"IMAGE_SYNC_EMPTY",
|
|
8398
|
+
1,
|
|
8399
|
+
"The directory should contain subdirectories named by language code (e.g., en-US/) with image type subdirectories inside."
|
|
8400
|
+
);
|
|
8401
|
+
}
|
|
8402
|
+
const imageTypes = options?.type ? [options.type] : ALL_IMAGE_TYPES2;
|
|
8403
|
+
const edit = await client.edits.insert(packageName);
|
|
8404
|
+
try {
|
|
8405
|
+
for (const language of languages) {
|
|
8406
|
+
for (const imageType of imageTypes) {
|
|
8407
|
+
const localDir = join11(dir, language, imageType);
|
|
8408
|
+
const localFiles = await scanLocalImages(localDir);
|
|
8409
|
+
let remoteImages;
|
|
8410
|
+
try {
|
|
8411
|
+
remoteImages = await client.images.list(packageName, edit.id, language, imageType);
|
|
8412
|
+
} catch (error) {
|
|
8413
|
+
if (error instanceof PlayApiError && error.statusCode === 404) {
|
|
8414
|
+
remoteImages = [];
|
|
8415
|
+
} else {
|
|
8416
|
+
throw error;
|
|
8417
|
+
}
|
|
8418
|
+
}
|
|
8419
|
+
const remoteSha256Set = new Set(remoteImages.map((img) => img.sha256.toLowerCase()));
|
|
8420
|
+
const localHashes = /* @__PURE__ */ new Map();
|
|
8421
|
+
for (const file of localFiles) {
|
|
8422
|
+
const hash = await sha256File(join11(localDir, file));
|
|
8423
|
+
localHashes.set(file, hash);
|
|
8424
|
+
}
|
|
8425
|
+
const localSha256Set = new Set(localHashes.values());
|
|
8426
|
+
if (options?.delete) {
|
|
8427
|
+
for (const img of remoteImages) {
|
|
8428
|
+
if (!localSha256Set.has(img.sha256.toLowerCase())) {
|
|
8429
|
+
if (!options?.dryRun) {
|
|
8430
|
+
await client.images.delete(packageName, edit.id, language, imageType, img.id);
|
|
8431
|
+
}
|
|
8432
|
+
deleted++;
|
|
8433
|
+
details.push({
|
|
8434
|
+
language,
|
|
8435
|
+
imageType,
|
|
8436
|
+
file: img.id,
|
|
8437
|
+
action: "delete",
|
|
8438
|
+
reason: "not in local"
|
|
8439
|
+
});
|
|
8440
|
+
}
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
for (const file of localFiles) {
|
|
8444
|
+
const hash = localHashes.get(file);
|
|
8445
|
+
if (remoteSha256Set.has(hash)) {
|
|
8446
|
+
skipped++;
|
|
8447
|
+
details.push({ language, imageType, file, action: "skip", reason: "sha256 match" });
|
|
8448
|
+
} else {
|
|
8449
|
+
if (!options?.dryRun) {
|
|
8450
|
+
const filePath = join11(localDir, file);
|
|
8451
|
+
const check = await validateImage(filePath, imageType);
|
|
8452
|
+
if (!check.valid) {
|
|
8453
|
+
throw new GpcError(
|
|
8454
|
+
`Image validation failed for ${language}/${imageType}/${file}: ${check.errors.join("; ")}`,
|
|
8455
|
+
"IMAGE_INVALID",
|
|
8456
|
+
2,
|
|
8457
|
+
"Check image dimensions, file size, and format."
|
|
8458
|
+
);
|
|
8459
|
+
}
|
|
8460
|
+
await client.images.upload(packageName, edit.id, language, imageType, filePath);
|
|
8461
|
+
}
|
|
8462
|
+
uploaded++;
|
|
8463
|
+
details.push({ language, imageType, file, action: "upload", reason: "new or changed" });
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
if (options?.dryRun || uploaded === 0 && deleted === 0) {
|
|
8469
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
8470
|
+
});
|
|
8471
|
+
} else {
|
|
8472
|
+
await validateAndCommit(client, packageName, edit.id, options?.commitOptions);
|
|
8473
|
+
}
|
|
8474
|
+
return {
|
|
8475
|
+
uploaded,
|
|
8476
|
+
skipped,
|
|
8477
|
+
deleted,
|
|
8478
|
+
total: uploaded + skipped + deleted,
|
|
8479
|
+
details
|
|
8480
|
+
};
|
|
8481
|
+
} catch (error) {
|
|
8482
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
8483
|
+
});
|
|
8484
|
+
throw error;
|
|
8485
|
+
}
|
|
8486
|
+
}
|
|
8487
|
+
|
|
8488
|
+
// src/commands/bundles.ts
|
|
8489
|
+
import { PlayApiError as PlayApiError2 } from "@gpc-cli/api";
|
|
8490
|
+
async function listBundles(client, packageName) {
|
|
8491
|
+
const edit = await client.edits.insert(packageName);
|
|
8492
|
+
try {
|
|
8493
|
+
const bundles = await client.bundles.list(packageName, edit.id);
|
|
8494
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
8495
|
+
});
|
|
8496
|
+
return bundles;
|
|
8497
|
+
} catch (error) {
|
|
8498
|
+
await client.edits.delete(packageName, edit.id).catch(() => {
|
|
8499
|
+
});
|
|
8500
|
+
throw error;
|
|
8501
|
+
}
|
|
8502
|
+
}
|
|
8503
|
+
async function findBundle(client, packageName, versionCode) {
|
|
8504
|
+
const bundles = await listBundles(client, packageName);
|
|
8505
|
+
return bundles.find((b) => b.versionCode === versionCode) ?? null;
|
|
8506
|
+
}
|
|
8507
|
+
function isRetryableError(error) {
|
|
8508
|
+
if (!(error instanceof PlayApiError2)) return false;
|
|
8509
|
+
const status = error.statusCode;
|
|
8510
|
+
return status === 429 || status === 500 || status === 503 || status === 409;
|
|
8511
|
+
}
|
|
8512
|
+
async function waitForBundle(client, packageName, versionCode, options) {
|
|
8513
|
+
const timeout = options?.timeout ?? 6e5;
|
|
8514
|
+
const interval = options?.interval ?? 15e3;
|
|
8515
|
+
const deadline = Date.now() + timeout;
|
|
8516
|
+
while (Date.now() < deadline) {
|
|
8517
|
+
try {
|
|
8518
|
+
const bundles = await listBundles(client, packageName);
|
|
8519
|
+
const match = bundles.find((b) => b.versionCode === versionCode);
|
|
8520
|
+
if (match) return match;
|
|
8521
|
+
} catch (error) {
|
|
8522
|
+
if (!isRetryableError(error)) throw error;
|
|
8523
|
+
}
|
|
8524
|
+
const remaining = deadline - Date.now();
|
|
8525
|
+
if (remaining <= 0) break;
|
|
8526
|
+
await new Promise((r) => setTimeout(r, Math.min(interval, remaining)));
|
|
8527
|
+
}
|
|
8528
|
+
throw new GpcError(
|
|
8529
|
+
`Bundle version code ${versionCode} not found after ${Math.round(timeout / 1e3)}s`,
|
|
8530
|
+
"BUNDLE_WAIT_TIMEOUT",
|
|
8531
|
+
4,
|
|
8532
|
+
"The bundle may still be processing. Try again with a longer --timeout, or check the Play Console."
|
|
8533
|
+
);
|
|
8534
|
+
}
|
|
8535
|
+
|
|
8374
8536
|
// src/signing-consistency.ts
|
|
8375
8537
|
async function checkSigningConsistency(accessToken, packageName, apiHost = "androidpublisher.googleapis.com") {
|
|
8376
8538
|
const baseUrl = `https://${apiHost}/androidpublisher/v3/applications/${encodeURIComponent(packageName)}`;
|
|
@@ -8613,6 +8775,7 @@ export {
|
|
|
8613
8775
|
checkThreshold,
|
|
8614
8776
|
classifyError,
|
|
8615
8777
|
clearAuditLog,
|
|
8778
|
+
commitWithRescue,
|
|
8616
8779
|
compareBundles,
|
|
8617
8780
|
compareFingerprints,
|
|
8618
8781
|
compareVersionVitals,
|
|
@@ -8667,6 +8830,7 @@ export {
|
|
|
8667
8830
|
fetchAggregateCost,
|
|
8668
8831
|
fetchChangelog,
|
|
8669
8832
|
fetchReleaseNotes,
|
|
8833
|
+
findBundle,
|
|
8670
8834
|
formatChangelogEntry,
|
|
8671
8835
|
formatCustomPayload,
|
|
8672
8836
|
formatDiscordPayload,
|
|
@@ -8728,11 +8892,13 @@ export {
|
|
|
8728
8892
|
isValidBcp47,
|
|
8729
8893
|
isValidReportType,
|
|
8730
8894
|
isValidStatsDimension,
|
|
8895
|
+
isVersionedNotesDir,
|
|
8731
8896
|
lintListing,
|
|
8732
8897
|
lintListings,
|
|
8733
8898
|
lintLocalListings,
|
|
8734
8899
|
listAchievements,
|
|
8735
8900
|
listAuditEvents,
|
|
8901
|
+
listBundles,
|
|
8736
8902
|
listDeviceTiers,
|
|
8737
8903
|
listEvents,
|
|
8738
8904
|
listGeneratedApks,
|
|
@@ -8770,6 +8936,7 @@ export {
|
|
|
8770
8936
|
pullListings,
|
|
8771
8937
|
pushListings,
|
|
8772
8938
|
readListingsFromDir,
|
|
8939
|
+
readReleaseNotesForVersion,
|
|
8773
8940
|
readReleaseNotesFromDir,
|
|
8774
8941
|
redactAuditArgs,
|
|
8775
8942
|
redactSensitive,
|
|
@@ -8801,9 +8968,11 @@ export {
|
|
|
8801
8968
|
searchVitalsErrors,
|
|
8802
8969
|
sendNotification,
|
|
8803
8970
|
sendWebhook,
|
|
8971
|
+
sha256File,
|
|
8804
8972
|
sortResults,
|
|
8805
8973
|
startTrain,
|
|
8806
8974
|
statusHasBreach,
|
|
8975
|
+
syncImages,
|
|
8807
8976
|
syncInAppProducts,
|
|
8808
8977
|
topFiles,
|
|
8809
8978
|
trackBreachState,
|
|
@@ -8824,6 +8993,7 @@ export {
|
|
|
8824
8993
|
uploadImage,
|
|
8825
8994
|
uploadInternalSharing,
|
|
8826
8995
|
uploadRelease,
|
|
8996
|
+
validateAndCommit,
|
|
8827
8997
|
validateBundleForApply,
|
|
8828
8998
|
validateImage,
|
|
8829
8999
|
validateLanguageCode,
|
|
@@ -8834,6 +9004,7 @@ export {
|
|
|
8834
9004
|
validateTrackName,
|
|
8835
9005
|
validateUploadFile,
|
|
8836
9006
|
validateVersionCode,
|
|
9007
|
+
waitForBundle,
|
|
8837
9008
|
waitForBundleProcessing,
|
|
8838
9009
|
watchVitalsWithAutoHalt,
|
|
8839
9010
|
wordDiff,
|