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