@gpc-cli/core 0.9.52 → 0.9.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -60,6 +60,7 @@ const analysis = await analyzeBundle("./app.aab");
60
60
  | **Testers** | `listTesters`, `addTesters`, `removeTesters`, `importTestersFromCsv` |
61
61
  | **Bundle** | `analyzeBundle`, `compareBundles` (zero-dependency AAB/APK size analysis) |
62
62
  | **Publishing** | `publish` (end-to-end: upload + track + notes + commit) |
63
+ | **Changelog** | `generateChangelog`, `fetchChangelog`, `formatChangelogEntry`, `buildLocaleBundle`, `renderPlayStore`, `renderMarkdown`, `renderJson`, `renderPrompt`, `translateBundle`, `resolveLocales` |
63
64
  | **Validation** | `validateUploadFile`, `validateImage`, `validatePreSubmission` |
64
65
 
65
66
  ## Utilities
package/dist/index.d.ts CHANGED
@@ -109,6 +109,7 @@ interface AppInfo {
109
109
  }
110
110
  declare function getAppInfo(client: PlayApiClient, packageName: string): Promise<AppInfo>;
111
111
 
112
+ declare function waitForBundleProcessing(client: PlayApiClient, packageName: string, editId: string, versionCode: number, backoff?: number[]): Promise<void>;
112
113
  interface UploadResult {
113
114
  versionCode: number;
114
115
  track: string;
@@ -183,6 +184,19 @@ declare function fetchReleaseNotes(client: PlayApiClient, packageName: string, t
183
184
  language: string;
184
185
  text: string;
185
186
  }[]>;
187
+ interface ApplyReleaseNotesResult {
188
+ track: string;
189
+ versionCodes: string[];
190
+ localeCount: number;
191
+ releaseNotes: {
192
+ language: string;
193
+ text: string;
194
+ }[];
195
+ }
196
+ declare function applyReleaseNotes(client: PlayApiClient, packageName: string, track: string, releaseNotes: {
197
+ language: string;
198
+ text: string;
199
+ }[], commitOptions?: EditCommitOptions): Promise<ApplyReleaseNotesResult>;
186
200
  interface ReleaseDiff {
187
201
  field: string;
188
202
  track1Value: string;
@@ -1323,15 +1337,43 @@ declare function renderJson(g: GeneratedChangelog): string;
1323
1337
 
1324
1338
  declare function renderPrompt(g: GeneratedChangelog): string;
1325
1339
 
1340
+ type Provider = "anthropic" | "openai" | "google";
1341
+ type TranslationPath = "gateway" | "direct";
1342
+ type ErrorReason = "rate_limited" | "auth" | "safety_blocked" | "timeout" | "network" | "no_source" | "unknown";
1343
+ interface TranslationResult {
1344
+ text: string;
1345
+ tokensIn: number;
1346
+ tokensOut: number;
1347
+ }
1348
+ type Translator = (locale: string, sourceText: string) => Promise<TranslationResult>;
1349
+ interface TranslatorConfig {
1350
+ path: TranslationPath;
1351
+ provider: Provider;
1352
+ model: string;
1353
+ runId: string;
1354
+ }
1355
+ interface ResolveAiConfigOptions {
1356
+ provider?: string;
1357
+ model?: string;
1358
+ env?: NodeJS.ProcessEnv;
1359
+ }
1360
+ declare const PROVIDER_WHITELIST: readonly Provider[];
1361
+ declare const DEFAULT_MODELS: Record<Provider, string>;
1362
+ declare function resolveAiConfig(opts?: ResolveAiConfigOptions): TranslatorConfig;
1363
+ declare function classifyError(err: unknown): ErrorReason;
1364
+ declare function createTranslator(config: TranslatorConfig): Promise<Translator>;
1365
+ declare function fetchAggregateCost(runId: string): Promise<number | undefined>;
1366
+ declare function formatPathLabel(config: TranslatorConfig): string;
1367
+
1326
1368
  declare const PLAY_STORE_LIMIT = 500;
1327
- declare const PLACEHOLDER_TEXT = "[needs translation \u2014 pass --ai once v0.9.63 ships, or paste the prompt emitted by --format prompt]";
1369
+ declare const PLACEHOLDER_TEXT = "[needs translation \u2014 pass --ai, or paste the prompt emitted by --format prompt]";
1328
1370
  type PlayStoreFormat = "md" | "json" | "prompt";
1329
1371
  interface LocaleEntry {
1330
1372
  language: string;
1331
1373
  text: string;
1332
1374
  chars: number;
1333
1375
  limit: number;
1334
- status: "ok" | "over" | "placeholder" | "empty";
1376
+ status: "ok" | "over" | "placeholder" | "empty" | "failed";
1335
1377
  }
1336
1378
  interface LocaleBundle {
1337
1379
  from: string;
@@ -1354,6 +1396,27 @@ declare function renderPlayStore(g: GeneratedChangelog, opts: PlayStoreRenderOpt
1354
1396
  output: string;
1355
1397
  bundle: LocaleBundle;
1356
1398
  };
1399
+ interface TranslationFailure {
1400
+ language: string;
1401
+ reason: ErrorReason;
1402
+ }
1403
+ interface TranslateBundleOptions {
1404
+ translator: Translator;
1405
+ strict?: boolean;
1406
+ onError?: (failure: TranslationFailure, err: unknown) => void;
1407
+ onTranslated?: (entry: LocaleEntry) => void;
1408
+ }
1409
+ interface TranslatedBundle extends LocaleBundle {
1410
+ tokensIn: number;
1411
+ tokensOut: number;
1412
+ failures: TranslationFailure[];
1413
+ }
1414
+ declare function translateBundle(bundle: LocaleBundle, options: TranslateBundleOptions): Promise<TranslatedBundle>;
1415
+ declare function validateBundleForApply(bundle: LocaleBundle): string[];
1416
+ declare function bundleToReleaseNotes(bundle: LocaleBundle): {
1417
+ language: string;
1418
+ text: string;
1419
+ }[];
1357
1420
 
1358
1421
  type Renderer = (g: GeneratedChangelog) => string;
1359
1422
  declare const RENDERERS: Record<OutputMode, Renderer>;
@@ -1409,4 +1472,4 @@ declare function decodeNotification(base64Payload: string): DecodedNotification;
1409
1472
  */
1410
1473
  declare function formatNotification(notification: DecodedNotification): Record<string, unknown>;
1411
1474
 
1412
- export { ApiError, type AppInfo, type AppStatus, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type ChangelogEntry, type CommandContext, type CommitCluster, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_PREFLIGHT_CONFIG, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, type RawCommit, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewsFilterOptions, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WatchVitalsOptions, type WebhookPayload, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, batchGetOrders, batchSyncInAppProducts, buildLocaleBundle, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkThreshold, clearAuditLog, compareBundles, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, deactivateBasePlan, deactivateOffer, decodeNotification, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportImages, exportReviews, fetchChangelog, fetchReleaseNotes, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatNotification, formatOutput, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAllScannerNames, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, lintListing, lintListings, lintLocalListings, listAchievements, listAuditEvents, listDeviceTiers, listEvents, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, resolveLocales, revokeSubscriptionPurchase, runPreflight, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, startTrain, statusHasBreach, syncInAppProducts, topFiles, trackBreachState, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
1475
+ export { ApiError, type AppInfo, type AppStatus, type ApplyReleaseNotesResult, type AuditEntry, type BatchSyncResult, type BundleAnalysis, type BundleComparison, type BundleEntry, type BundleSizeCheckResult, type BundleSizeConfig, type ChangelogEntry, type CommandContext, type CommitCluster, ConfigError, type CreateEnterpriseAppParams, DEFAULT_LIMITS, DEFAULT_MODELS, DEFAULT_PREFLIGHT_CONFIG, type DecodedNotification, type DiffToken, type DiscoverPluginsOptions, type DryRunPublishResult, type DryRunResult, type DryRunUploadResult, type ErrorReason, type ExportImagesOptions, type ExportImagesSummary, type FastlaneDetection, type FastlaneLane, type FetchChangelogOptions, type FieldLintResult, type FileValidationResult, type FindingSeverity, GOOGLE_PLAY_LANGUAGES, type GenerateOptions, type GeneratedChangelog, type GetAppStatusOptions, type GitNotesOptions, type GitReleaseNotes, type GitRunner, GpcError, type ImageValidationResult, type InitOptions, type InitResult, type InternalSharingUploadResult, type ListIapOptions, type ListSubscriptionsOptions, type ListUsersOptions, type ListVoidedOptions, type ListingDiff, type ListingFieldLimits, type ListingLintResult, type ListingsResult, type LoadedPlugin, type LocaleBundle, type LocaleEntry, type MigrationResult, NetworkError, type OneTimeProductDiff, type OutputMode, PERMISSION_PROPAGATION_WARNING, PLACEHOLDER_TEXT, PLAY_STORE_LIMIT, PROVIDER_WHITELIST, type ParsedCommit, type ParsedManifest, type ParsedMonth, type PlayStoreFormat, type PlayStoreRenderOptions, PluginManager, type PreflightConfig, type PreflightFinding, type PreflightOptions, type PreflightResult, type PreflightScanner, type Provider, type PublishOptions, type PublishResult, type PushResult, type QuotaUsage, RENDERERS, type RawCommit, type ReleaseDiff, type ReleaseNotesValidation, type ReleaseStatusResult, type Renderer, type ResolveAiConfigOptions, type ResolveLocalesOptions, type ReviewAnalysis, type ReviewExportOptions, type ReviewsFilterOptions, type RtdnStatus, SECTION_ORDER, SENSITIVE_ARG_KEYS, SENSITIVE_KEYS, SEVERITY_ORDER, type ScaffoldOptions, type ScaffoldResult, type Spinner, type StatusDiff, type StatusRelease, type StatusReviews, type StatusVitalMetric, type SubscriptionAnalytics, type SubscriptionDiff, type SyncResult, type ThresholdResult, type TrainConfig, type TrainState, type TranslateBundleOptions, type TranslatedBundle, type TranslationFailure, type TranslationPath, type TranslationResult, type Translator, type TranslatorConfig, type UploadResult, type ValidateCheck, type ValidateOptions, type ValidateResult, type VersionVitalsComparison, type VersionVitalsRow, type VitalsOverview, type VitalsQueryOptions, type VitalsTrendComparison, type WatchOptions, type WatchVitalsOptions, type WebhookPayload, abortTrain, acknowledgeProductPurchase, activateBasePlan, activateOffer, addRecoveryTargeting, addTesters, advanceTrain, analyzeBundle, analyzeRemoteListings, analyzeReviews, applyReleaseNotes, batchGetOrders, batchSyncInAppProducts, buildLocaleBundle, bundleToReleaseNotes, cancelRecoveryAction, cancelSubscriptionPurchase, cancelSubscriptionV2, checkBundleSize, checkThreshold, classifyError, clearAuditLog, compareBundles, compareVersionVitals, compareVitalsTrend, computeStatusDiff, consumeProductPurchase, convertRegionPrices, createAuditEntry, createDeviceTier, createEnterpriseApp, createExternalTransaction, createGrant, createInAppProduct, createOffer, createOneTimeOffer, createOneTimeProduct, createRecoveryAction, createSpinner, createSubscription, createTrack, createTranslator, deactivateBasePlan, deactivateOffer, decodeNotification, defaultGitRunner, deferSubscriptionPurchase, deferSubscriptionV2, deleteBasePlan, deleteGrant, deleteImage, deleteInAppProduct, deleteListing, deleteOffer, deleteOneTimeOffer, deleteOneTimeProduct, deleteSubscription, deployRecoveryAction, detectFastlane, detectOutputFormat, diffListings, diffListingsCommand, diffListingsEnhanced, diffOneTimeProduct, diffReleases, diffSubscription, discoverPlugins, downloadGeneratedApk, downloadReport, exportImages, exportReviews, fetchAggregateCost, fetchChangelog, fetchReleaseNotes, formatChangelogEntry, formatCustomPayload, formatDiscordPayload, formatJunit, formatNotification, formatOutput, formatPathLabel, formatSlackPayload, formatStatusDiff, formatStatusSummary, formatStatusTable, formatWordDiff, generateChangelog, generateMigrationPlan, generateNotesFromGit, getAllScannerNames, getAppInfo, getAppStatus, getCountryAvailability, getDeviceTier, getExternalTransaction, getInAppProduct, getListings, getOffer, getOneTimeOffer, getOneTimeProduct, getOrderDetails, getProductPurchase, getProductPurchaseV2, getQuotaUsage, getReleasesStatus, getReview, getRtdnStatus, getSubscription, getSubscriptionAnalytics, getSubscriptionPurchase, getTrainStatus, getUser, getVitalsAnomalies, getVitalsAnr, getVitalsBattery, getVitalsCrashes, getVitalsErrorCount, getVitalsLmk, getVitalsMemory, getVitalsOverview, getVitalsRendering, getVitalsStartup, importDataSafety, importTestersFromCsv, initAudit, initProject, inviteUser, isFinancialReportType, isStatsReportType, isValidBcp47, isValidReportType, isValidStatsDimension, lintListing, lintListings, lintLocalListings, listAchievements, listAuditEvents, listDeviceTiers, listEvents, listGeneratedApks, listGrants, listImages, listInAppProducts, listLeaderboards, listOffers, listOneTimeOffers, listOneTimeProducts, listRecoveryActions, listReports, listReviews, listSubscriptions, listTesters, listTracks, listUsers, listVoidedPurchases, loadPreflightConfig, loadStatusCache, maybePaginate, migratePrices, parseAppfile, parseCommit, parseFastfile, parseGrantArg, parseMonth, parseRemoteUrl, pauseTrain, promoteRelease, publish, publishEnterpriseApp, pullListings, pushListings, readListingsFromDir, readReleaseNotesFromDir, redactAuditArgs, redactSensitive, refundExternalTransaction, refundOrder, relativeTime, removeTesters, removeUser, renderJson, renderMarkdown, renderPlayStore, renderPlayStoreJson, renderPlayStoreMd, renderPlayStorePrompt, renderPrompt, replyToReview, resolveAiConfig, resolveLocales, revokeSubscriptionPurchase, runPreflight, runWatchLoop, safePath, safePathWithin, saveStatusCache, scaffoldPlugin, searchAuditEvents, searchVitalsErrors, sendNotification, sendWebhook, sortResults, startTrain, statusHasBreach, syncInAppProducts, topFiles, trackBreachState, translateBundle, updateAppDetails, updateDataSafety, updateGrant, updateInAppProduct, updateListing, updateOffer, updateOneTimeOffer, updateOneTimeProduct, updateRollout, updateSubscription, updateTrackConfig, updateUser, uploadExternallyHosted, uploadImage, uploadInternalSharing, uploadRelease, validateBundleForApply, validateImage, validateLanguageCode, validatePackageName, validatePreSubmission, validateReleaseNotes, validateSku, validateTrackName, validateUploadFile, validateVersionCode, waitForBundleProcessing, watchVitalsWithAutoHalt, wordDiff, writeAuditLog, writeListingsToDir, writeMigrationOutput };
package/dist/index.js CHANGED
@@ -650,6 +650,20 @@ function formatSize(bytes) {
650
650
  }
651
651
 
652
652
  // src/commands/releases.ts
653
+ var BUNDLE_POLL_BACKOFF = [2e3, 3e3, 5e3, 8e3, 13e3];
654
+ async function waitForBundleProcessing(client, packageName, editId, versionCode, backoff = BUNDLE_POLL_BACKOFF) {
655
+ for (let i = 0; i < backoff.length; i++) {
656
+ const bundles = await client.bundles.list(packageName, editId);
657
+ if (bundles.some((b) => b.versionCode === versionCode)) return;
658
+ await new Promise((r) => setTimeout(r, backoff[i]));
659
+ }
660
+ throw new GpcError(
661
+ `Bundle versionCode ${versionCode} not ready after ${backoff.length} poll attempts (~${Math.round(backoff.reduce((a, b) => a + b, 0) / 1e3)}s)`,
662
+ "BUNDLE_PROCESSING_TIMEOUT",
663
+ 4,
664
+ "The AAB is still being processed by Google. Retry the upload, or use --status draft and commit later."
665
+ );
666
+ }
653
667
  async function withRetryOnConflict(client, packageName, operation) {
654
668
  const edit = await client.edits.insert(packageName);
655
669
  try {
@@ -763,6 +777,9 @@ ${validation.errors.join("\n")}`,
763
777
  uploadOpts,
764
778
  options.deviceTierConfigId
765
779
  );
780
+ if (!isApk) {
781
+ await waitForBundleProcessing(client, packageName, edit.id, bundle.versionCode);
782
+ }
766
783
  if (options.mappingFile) {
767
784
  await client.deobfuscation.upload(
768
785
  packageName,
@@ -1023,6 +1040,35 @@ async function fetchReleaseNotes(client, packageName, track) {
1023
1040
  });
1024
1041
  }
1025
1042
  }
1043
+ async function applyReleaseNotes(client, packageName, track, releaseNotes, commitOptions) {
1044
+ return withRetryOnConflict(client, packageName, async (edit) => {
1045
+ const trackData = await client.tracks.get(packageName, edit.id, track);
1046
+ const draft = trackData.releases?.find((r) => r.status === "draft");
1047
+ if (!draft) {
1048
+ throw new GpcError(
1049
+ `No draft release found on track "${track}"`,
1050
+ "RELEASE_NO_DRAFT",
1051
+ 1,
1052
+ `Upload an AAB/APK first to create a draft, or check the --track value. Current track: "${track}".`
1053
+ );
1054
+ }
1055
+ const patched = {
1056
+ ...draft,
1057
+ releaseNotes
1058
+ };
1059
+ await client.tracks.update(packageName, edit.id, track, patched);
1060
+ if (!commitOptions?.changesNotSentForReview) {
1061
+ await client.edits.validate(packageName, edit.id);
1062
+ }
1063
+ await client.edits.commit(packageName, edit.id, commitOptions);
1064
+ return {
1065
+ track,
1066
+ versionCodes: draft.versionCodes || [],
1067
+ localeCount: releaseNotes.length,
1068
+ releaseNotes
1069
+ };
1070
+ });
1071
+ }
1026
1072
  async function diffReleases(client, packageName, fromTrack, toTrack) {
1027
1073
  const edit = await client.edits.insert(packageName);
1028
1074
  try {
@@ -7377,7 +7423,16 @@ var KNOWN_TYPES = /* @__PURE__ */ new Set([
7377
7423
  "release"
7378
7424
  ]);
7379
7425
  var FILTERED_TYPES = /* @__PURE__ */ new Set(["chore", "refactor", "test", "build", "style", "merge"]);
7380
- var SECTION_ORDER = ["breaking", "feat", "fix", "perf", "docs", "ci", "release", "other"];
7426
+ var SECTION_ORDER = [
7427
+ "breaking",
7428
+ "feat",
7429
+ "fix",
7430
+ "perf",
7431
+ "docs",
7432
+ "ci",
7433
+ "release",
7434
+ "other"
7435
+ ];
7381
7436
  var FIXUP_PATTERNS = [
7382
7437
  /^wip\b/i,
7383
7438
  /^fix\s+typo\b/i,
@@ -7841,14 +7896,18 @@ function renderPrompt(g) {
7841
7896
  if (g.headlineCandidates.length > 0) {
7842
7897
  lines.push("HEADLINE CANDIDATES (largest first):");
7843
7898
  for (const c of g.headlineCandidates) {
7844
- lines.push(` ${c.label} (weight ${c.weight}, ${c.commits.length} commits, primary ${c.primaryType})`);
7899
+ lines.push(
7900
+ ` ${c.label} (weight ${c.weight}, ${c.commits.length} commits, primary ${c.primaryType})`
7901
+ );
7845
7902
  }
7846
7903
  lines.push("");
7847
7904
  }
7848
7905
  lines.push("CLUSTERED COMMITS:");
7849
7906
  lines.push("");
7850
7907
  for (const cluster of g.clusters) {
7851
- lines.push(`[cluster: ${cluster.label}, weight ${cluster.weight}, ${cluster.commits.length} commits, primary ${cluster.primaryType}]`);
7908
+ lines.push(
7909
+ `[cluster: ${cluster.label}, weight ${cluster.weight}, ${cluster.commits.length} commits, primary ${cluster.primaryType}]`
7910
+ );
7852
7911
  for (const commit of cluster.commits) {
7853
7912
  lines.push(`- ${commit.type}: ${safeLine(commit.subject)} (${commit.sha.slice(0, 7)})`);
7854
7913
  if (commit.files.length > 0) {
@@ -7881,9 +7940,179 @@ function renderPrompt(g) {
7881
7940
  return lines.join("\n");
7882
7941
  }
7883
7942
 
7943
+ // src/commands/changelog-ai.ts
7944
+ import { randomUUID } from "crypto";
7945
+ var PROVIDER_WHITELIST = ["anthropic", "openai", "google"];
7946
+ var DEFAULT_MODELS = {
7947
+ anthropic: "claude-sonnet-4-6",
7948
+ openai: "gpt-4o-mini",
7949
+ google: "gemini-2.5-flash"
7950
+ };
7951
+ function resolveAiConfig(opts = {}) {
7952
+ const env = opts.env ?? process.env;
7953
+ const hasGateway = typeof env["AI_GATEWAY_API_KEY"] === "string" && env["AI_GATEWAY_API_KEY"].length > 0;
7954
+ let provider;
7955
+ if (opts.provider) {
7956
+ const normalized = opts.provider.toLowerCase();
7957
+ if (!PROVIDER_WHITELIST.includes(normalized)) {
7958
+ throw new GpcError(
7959
+ `Unknown --provider "${opts.provider}"`,
7960
+ "CHANGELOG_AI_UNKNOWN_PROVIDER",
7961
+ 2,
7962
+ `Valid providers: ${PROVIDER_WHITELIST.join(", ")}`
7963
+ );
7964
+ }
7965
+ provider = normalized;
7966
+ } else if (hasGateway || env["ANTHROPIC_API_KEY"]) {
7967
+ provider = "anthropic";
7968
+ } else if (env["OPENAI_API_KEY"]) {
7969
+ provider = "openai";
7970
+ } else if (env["GOOGLE_GENERATIVE_AI_API_KEY"]) {
7971
+ provider = "google";
7972
+ } else {
7973
+ throw new GpcError(
7974
+ "No AI provider credentials found in env",
7975
+ "CHANGELOG_AI_NO_CREDENTIALS",
7976
+ 3,
7977
+ "Set one of: AI_GATEWAY_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY."
7978
+ );
7979
+ }
7980
+ const model = opts.model ?? DEFAULT_MODELS[provider];
7981
+ const path = hasGateway ? "gateway" : "direct";
7982
+ const runId = randomUUID();
7983
+ return { path, provider, model, runId };
7984
+ }
7985
+ function classifyError(err) {
7986
+ if (!err || typeof err !== "object") return "unknown";
7987
+ const e = err;
7988
+ const name = typeof e["name"] === "string" ? e["name"] : "";
7989
+ const statusCode = typeof e["statusCode"] === "number" ? e["statusCode"] : typeof e["status"] === "number" ? e["status"] : 0;
7990
+ const message = typeof e["message"] === "string" ? e["message"].toLowerCase() : "";
7991
+ const finishReason = typeof e["finishReason"] === "string" ? e["finishReason"] : "";
7992
+ if (name === "RateLimitError" || statusCode === 429 || statusCode === 529) return "rate_limited";
7993
+ if (statusCode === 401 || statusCode === 403) return "auth";
7994
+ if (message.includes("api key invalid") || message.includes("invalid api key")) return "auth";
7995
+ if (finishReason === "SAFETY") return "safety_blocked";
7996
+ if (statusCode === 400 && (message.includes("content_policy") || message.includes("content policy") || message.includes("safety"))) {
7997
+ return "safety_blocked";
7998
+ }
7999
+ if (name === "AbortError" || name === "TimeoutError") return "timeout";
8000
+ if (message.includes("timeout") || message.includes("timed out")) return "timeout";
8001
+ if (message.includes("econnrefused") || message.includes("enotfound") || message.includes("etimedout") || message.includes("network") || name === "TypeError" && message.includes("fetch")) {
8002
+ return "network";
8003
+ }
8004
+ return "unknown";
8005
+ }
8006
+ function buildSystemPrompt() {
8007
+ return [
8008
+ `You translate Play Store "What's new" release notes for Android apps.`,
8009
+ "Rules:",
8010
+ "- Translation MUST be at most 500 Unicode code points.",
8011
+ '- Preserve bullet format (one item per line, starts with "- ").',
8012
+ "- User-facing tone. Avoid internal jargon.",
8013
+ '- Do not translate technical names (package names, CLI flags, "GPC").',
8014
+ "- Drop the conventional-commit prefix (feat:/fix:/docs:) if it feels unnatural in the target language.",
8015
+ "Respond with the translated text only. No explanations, no markdown headings."
8016
+ ].join("\n");
8017
+ }
8018
+ function buildUserPrompt(locale, sourceText) {
8019
+ return `Translate the following release notes into ${locale}:
8020
+
8021
+ ${sourceText}`;
8022
+ }
8023
+ function providerSpecificOptions(provider) {
8024
+ if (provider === "google") {
8025
+ return { google: { thinkingConfig: { thinkingBudget: 0 } } };
8026
+ }
8027
+ return {};
8028
+ }
8029
+ function readUsage(usage) {
8030
+ if (!usage || typeof usage !== "object") return { tokensIn: 0, tokensOut: 0 };
8031
+ const u = usage;
8032
+ const tokensIn = typeof u["inputTokens"] === "number" ? u["inputTokens"] : typeof u["promptTokens"] === "number" ? u["promptTokens"] : 0;
8033
+ const tokensOut = typeof u["outputTokens"] === "number" ? u["outputTokens"] : typeof u["completionTokens"] === "number" ? u["completionTokens"] : 0;
8034
+ return { tokensIn, tokensOut };
8035
+ }
8036
+ async function createTranslator(config) {
8037
+ const ai = await import("ai");
8038
+ const generateText = ai.generateText;
8039
+ if (config.path === "gateway") {
8040
+ const modelString = `${config.provider}/${config.model}`;
8041
+ return async (locale, sourceText) => {
8042
+ const result = await generateText({
8043
+ model: ai.gateway(modelString),
8044
+ system: buildSystemPrompt(),
8045
+ prompt: buildUserPrompt(locale, sourceText),
8046
+ temperature: 0.2,
8047
+ providerOptions: {
8048
+ gateway: { tags: [`gpc-changelog-${config.runId}`] },
8049
+ ...providerSpecificOptions(config.provider)
8050
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8051
+ }
8052
+ });
8053
+ const { tokensIn, tokensOut } = readUsage(result.usage);
8054
+ return { text: result.text, tokensIn, tokensOut };
8055
+ };
8056
+ }
8057
+ let modelFactory;
8058
+ if (config.provider === "anthropic") {
8059
+ const mod = await import("@ai-sdk/anthropic");
8060
+ modelFactory = (id) => mod.anthropic(id);
8061
+ } else if (config.provider === "openai") {
8062
+ const mod = await import("@ai-sdk/openai");
8063
+ modelFactory = (id) => mod.openai(id);
8064
+ } else {
8065
+ const mod = await import("@ai-sdk/google");
8066
+ modelFactory = (id) => mod.google(id);
8067
+ }
8068
+ return async (locale, sourceText) => {
8069
+ const result = await generateText({
8070
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8071
+ model: modelFactory(config.model),
8072
+ system: buildSystemPrompt(),
8073
+ prompt: buildUserPrompt(locale, sourceText),
8074
+ temperature: 0.2,
8075
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8076
+ providerOptions: providerSpecificOptions(config.provider)
8077
+ });
8078
+ const { tokensIn, tokensOut } = readUsage(result.usage);
8079
+ return { text: result.text, tokensIn, tokensOut };
8080
+ };
8081
+ }
8082
+ async function fetchAggregateCost(runId) {
8083
+ try {
8084
+ const { gateway } = await import("ai");
8085
+ const now = /* @__PURE__ */ new Date();
8086
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
8087
+ const today = now.toISOString().slice(0, 10);
8088
+ const startDate = yesterday.toISOString().slice(0, 10);
8089
+ const report = await gateway.getSpendReport({
8090
+ startDate,
8091
+ endDate: today,
8092
+ groupBy: "tag",
8093
+ tags: [`gpc-changelog-${runId}`]
8094
+ });
8095
+ const results = report.results ?? [];
8096
+ return results.reduce((sum, row) => sum + (row.totalCost ?? 0), 0);
8097
+ } catch {
8098
+ return void 0;
8099
+ }
8100
+ }
8101
+ var PROVIDER_BRAND = {
8102
+ anthropic: "Anthropic",
8103
+ openai: "OpenAI",
8104
+ google: "Google"
8105
+ };
8106
+ function formatPathLabel(config) {
8107
+ if (config.path === "gateway") {
8108
+ return `routing via AI Gateway (${config.provider}/${config.model})`;
8109
+ }
8110
+ return `direct ${PROVIDER_BRAND[config.provider]} SDK (${config.model})`;
8111
+ }
8112
+
7884
8113
  // src/commands/changelog-renderers/play-store.ts
7885
8114
  var PLAY_STORE_LIMIT = 500;
7886
- var PLACEHOLDER_TEXT = "[needs translation \u2014 pass --ai once v0.9.63 ships, or paste the prompt emitted by --format prompt]";
8115
+ var PLACEHOLDER_TEXT = "[needs translation \u2014 pass --ai, or paste the prompt emitted by --format prompt]";
7887
8116
  function safeLine2(s) {
7888
8117
  return s.replace(/[\r\n]+/g, " ").trim();
7889
8118
  }
@@ -7989,7 +8218,9 @@ function renderPlayStorePrompt(bundle, g) {
7989
8218
  const source = bundle.locales.find((e) => e.language === bundle.sourceLanguage);
7990
8219
  const targets = bundle.locales.filter((e) => e.language !== bundle.sourceLanguage);
7991
8220
  const lines = [];
7992
- lines.push(`You are translating Play Store "What's new" release notes from ${bundle.sourceLanguage}.`);
8221
+ lines.push(
8222
+ `You are translating Play Store "What's new" release notes from ${bundle.sourceLanguage}.`
8223
+ );
7993
8224
  lines.push("");
7994
8225
  lines.push("TARGETS:");
7995
8226
  for (const t of targets) lines.push(` - ${t.language}`);
@@ -8000,7 +8231,9 @@ function renderPlayStorePrompt(bundle, g) {
8000
8231
  lines.push('- Preserve the bullet format (one item per line, starts with "- ")');
8001
8232
  lines.push("- Keep a user-facing tone (no internal jargon)");
8002
8233
  lines.push('- Do not translate technical names (package names, CLI flags, "GPC")');
8003
- lines.push("- Drop the conventional-commit prefix (feat:/fix:) if it feels unnatural in the target language");
8234
+ lines.push(
8235
+ "- Drop the conventional-commit prefix (feat:/fix:) if it feels unnatural in the target language"
8236
+ );
8004
8237
  lines.push("");
8005
8238
  lines.push(`SOURCE (${bundle.sourceLanguage}, ${source?.chars ?? 0}/${bundle.limit} chars):`);
8006
8239
  lines.push("```");
@@ -8038,6 +8271,103 @@ function renderPlayStore(g, opts) {
8038
8271
  return { output: renderPlayStorePrompt(bundle, g), bundle };
8039
8272
  }
8040
8273
  }
8274
+ async function translateBundle(bundle, options) {
8275
+ const source = bundle.locales.find((e) => e.language === bundle.sourceLanguage);
8276
+ const sourceText = source?.text ?? "";
8277
+ const hasSource = source !== void 0 && sourceText.trim().length > 0;
8278
+ let tokensIn = 0;
8279
+ let tokensOut = 0;
8280
+ const failures = [];
8281
+ const newLocales = [];
8282
+ for (const entry of bundle.locales) {
8283
+ if (entry.status !== "placeholder") {
8284
+ newLocales.push(entry);
8285
+ continue;
8286
+ }
8287
+ if (!hasSource) {
8288
+ const failure = { language: entry.language, reason: "no_source" };
8289
+ failures.push(failure);
8290
+ options.onError?.(failure, new Error("source locale missing or empty"));
8291
+ const failedText = `[translation failed: no_source]`;
8292
+ newLocales.push({
8293
+ language: entry.language,
8294
+ text: failedText,
8295
+ chars: countChars(failedText),
8296
+ limit: PLAY_STORE_LIMIT,
8297
+ status: "failed"
8298
+ });
8299
+ continue;
8300
+ }
8301
+ try {
8302
+ const result = await options.translator(entry.language, sourceText);
8303
+ tokensIn += result.tokensIn;
8304
+ tokensOut += result.tokensOut;
8305
+ let text = result.text.trim();
8306
+ let chars = countChars(text);
8307
+ let status = "ok";
8308
+ if (chars > PLAY_STORE_LIMIT) {
8309
+ text = truncateToLimit(text, PLAY_STORE_LIMIT);
8310
+ chars = countChars(text);
8311
+ status = "over";
8312
+ }
8313
+ const translated2 = {
8314
+ language: entry.language,
8315
+ text,
8316
+ chars,
8317
+ limit: PLAY_STORE_LIMIT,
8318
+ status
8319
+ };
8320
+ newLocales.push(translated2);
8321
+ options.onTranslated?.(translated2);
8322
+ } catch (err) {
8323
+ const reason = classifyError(err);
8324
+ const failure = { language: entry.language, reason };
8325
+ failures.push(failure);
8326
+ options.onError?.(failure, err);
8327
+ const failedText = `[translation failed: ${reason}]`;
8328
+ newLocales.push({
8329
+ language: entry.language,
8330
+ text: failedText,
8331
+ chars: countChars(failedText),
8332
+ limit: PLAY_STORE_LIMIT,
8333
+ status: "failed"
8334
+ });
8335
+ }
8336
+ }
8337
+ const overflows = newLocales.filter((e) => e.status === "over").map((e) => e.language);
8338
+ const translated = {
8339
+ ...bundle,
8340
+ locales: newLocales,
8341
+ overflows,
8342
+ tokensIn,
8343
+ tokensOut,
8344
+ failures
8345
+ };
8346
+ if (options.strict && failures.length > 0) {
8347
+ throw new GpcError(
8348
+ `${failures.length} locale${failures.length === 1 ? "" : "s"} failed to translate: ${failures.map((f) => `${f.language}=${f.reason}`).join(", ")}`,
8349
+ "CHANGELOG_AI_TRANSLATION_FAILED",
8350
+ 1,
8351
+ "Remove --strict to continue on errors, or check credentials and retry."
8352
+ );
8353
+ }
8354
+ return translated;
8355
+ }
8356
+ function validateBundleForApply(bundle) {
8357
+ const errors = [];
8358
+ for (const entry of bundle.locales) {
8359
+ if (entry.status === "placeholder") {
8360
+ errors.push(`${entry.language}: untranslated placeholder \u2014 use --ai or remove this locale`);
8361
+ }
8362
+ if (entry.status === "failed") {
8363
+ errors.push(`${entry.language}: translation failed \u2014 retry or remove this locale`);
8364
+ }
8365
+ }
8366
+ return errors;
8367
+ }
8368
+ function bundleToReleaseNotes(bundle) {
8369
+ return bundle.locales.filter((e) => e.status !== "placeholder" && e.status !== "failed").map((e) => ({ language: e.language, text: e.text }));
8370
+ }
8041
8371
 
8042
8372
  // src/commands/changelog-renderers/index.ts
8043
8373
  var RENDERERS = {
@@ -8204,6 +8534,7 @@ export {
8204
8534
  ApiError,
8205
8535
  ConfigError,
8206
8536
  DEFAULT_LIMITS,
8537
+ DEFAULT_MODELS,
8207
8538
  DEFAULT_PREFLIGHT_CONFIG,
8208
8539
  GOOGLE_PLAY_LANGUAGES,
8209
8540
  GpcError,
@@ -8211,6 +8542,7 @@ export {
8211
8542
  PERMISSION_PROPAGATION_WARNING,
8212
8543
  PLACEHOLDER_TEXT,
8213
8544
  PLAY_STORE_LIMIT,
8545
+ PROVIDER_WHITELIST,
8214
8546
  PluginManager,
8215
8547
  RENDERERS,
8216
8548
  SECTION_ORDER,
@@ -8227,14 +8559,17 @@ export {
8227
8559
  analyzeBundle,
8228
8560
  analyzeRemoteListings,
8229
8561
  analyzeReviews2 as analyzeReviews,
8562
+ applyReleaseNotes,
8230
8563
  batchGetOrders,
8231
8564
  batchSyncInAppProducts,
8232
8565
  buildLocaleBundle,
8566
+ bundleToReleaseNotes,
8233
8567
  cancelRecoveryAction,
8234
8568
  cancelSubscriptionPurchase,
8235
8569
  cancelSubscriptionV2,
8236
8570
  checkBundleSize,
8237
8571
  checkThreshold,
8572
+ classifyError,
8238
8573
  clearAuditLog,
8239
8574
  compareBundles,
8240
8575
  compareVersionVitals,
@@ -8255,6 +8590,7 @@ export {
8255
8590
  createSpinner,
8256
8591
  createSubscription,
8257
8592
  createTrack,
8593
+ createTranslator,
8258
8594
  deactivateBasePlan,
8259
8595
  deactivateOffer,
8260
8596
  decodeNotification,
@@ -8284,6 +8620,7 @@ export {
8284
8620
  downloadReport,
8285
8621
  exportImages,
8286
8622
  exportReviews,
8623
+ fetchAggregateCost,
8287
8624
  fetchChangelog,
8288
8625
  fetchReleaseNotes,
8289
8626
  formatChangelogEntry,
@@ -8292,6 +8629,7 @@ export {
8292
8629
  formatJunit,
8293
8630
  formatNotification,
8294
8631
  formatOutput,
8632
+ formatPathLabel,
8295
8633
  formatSlackPayload,
8296
8634
  formatStatusDiff,
8297
8635
  formatStatusSummary,
@@ -8399,6 +8737,7 @@ export {
8399
8737
  renderPlayStorePrompt,
8400
8738
  renderPrompt,
8401
8739
  replyToReview,
8740
+ resolveAiConfig,
8402
8741
  resolveLocales,
8403
8742
  revokeSubscriptionPurchase,
8404
8743
  runPreflight,
@@ -8417,6 +8756,7 @@ export {
8417
8756
  syncInAppProducts,
8418
8757
  topFiles,
8419
8758
  trackBreachState,
8759
+ translateBundle,
8420
8760
  updateAppDetails,
8421
8761
  updateDataSafety,
8422
8762
  updateGrant,
@@ -8433,6 +8773,7 @@ export {
8433
8773
  uploadImage,
8434
8774
  uploadInternalSharing,
8435
8775
  uploadRelease,
8776
+ validateBundleForApply,
8436
8777
  validateImage,
8437
8778
  validateLanguageCode,
8438
8779
  validatePackageName,
@@ -8442,6 +8783,7 @@ export {
8442
8783
  validateTrackName,
8443
8784
  validateUploadFile,
8444
8785
  validateVersionCode,
8786
+ waitForBundleProcessing,
8445
8787
  watchVitalsWithAutoHalt,
8446
8788
  wordDiff,
8447
8789
  writeAuditLog,