storemeta 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.env.release.example +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/dist/auth/apple/credential-state.js +14 -0
  5. package/dist/auth/apple/jwt.js +56 -0
  6. package/dist/auth/apple/load-credentials.js +36 -0
  7. package/dist/auth/credential-state.js +6 -0
  8. package/dist/auth/google/credential-state.js +10 -0
  9. package/dist/auth/google/load-credentials.js +18 -0
  10. package/dist/auth/google/service-account-auth.js +25 -0
  11. package/dist/auth/redact.js +17 -0
  12. package/dist/cli/auth-check.js +46 -0
  13. package/dist/cli/config-doctor.js +40 -0
  14. package/dist/cli/context.js +23 -0
  15. package/dist/cli/errors.js +10 -0
  16. package/dist/cli/exit-code.js +6 -0
  17. package/dist/cli/init.js +60 -0
  18. package/dist/cli/locales-list.js +37 -0
  19. package/dist/cli/metadata-diff.js +57 -0
  20. package/dist/cli/metadata-pull.js +52 -0
  21. package/dist/cli/metadata-push.js +95 -0
  22. package/dist/cli/render.js +57 -0
  23. package/dist/cli/result-types.js +1 -0
  24. package/dist/cli/scaffold.js +76 -0
  25. package/dist/cli/screenshots-diff.js +65 -0
  26. package/dist/cli/screenshots-pull.js +72 -0
  27. package/dist/cli/screenshots-push.js +96 -0
  28. package/dist/cli/validate.js +74 -0
  29. package/dist/cli.js +222 -0
  30. package/dist/config/apps.js +21 -0
  31. package/dist/config/load-config.js +30 -0
  32. package/dist/config/schema.js +97 -0
  33. package/dist/config/select-app.js +10 -0
  34. package/dist/config/select-platforms.js +17 -0
  35. package/dist/config/single-app.js +16 -0
  36. package/dist/config/types.js +1 -0
  37. package/dist/config/validate-base-dirs.js +33 -0
  38. package/dist/config/validate-credential-env-names.js +32 -0
  39. package/dist/config/validate-platform-identifiers.js +24 -0
  40. package/dist/formats/load-metadata.js +38 -0
  41. package/dist/formats/metadata-types.js +1 -0
  42. package/dist/formats/screenshot-types.js +1 -0
  43. package/dist/formats/serialize-metadata.js +20 -0
  44. package/dist/locales/groups.js +12 -0
  45. package/dist/locales/map.js +15 -0
  46. package/dist/locales/normalize.js +22 -0
  47. package/dist/locales/types.js +1 -0
  48. package/dist/locales/validate-groups.js +14 -0
  49. package/dist/platforms/apple/client.js +109 -0
  50. package/dist/platforms/apple/metadata/pull.js +120 -0
  51. package/dist/platforms/apple/metadata/push.js +272 -0
  52. package/dist/platforms/apple/metadata/types.js +1 -0
  53. package/dist/platforms/apple/screenshots/pull.js +160 -0
  54. package/dist/platforms/apple/screenshots/push.js +418 -0
  55. package/dist/platforms/google/client.js +42 -0
  56. package/dist/platforms/google/edits.js +38 -0
  57. package/dist/platforms/google/metadata/pull.js +23 -0
  58. package/dist/platforms/google/metadata/push.js +73 -0
  59. package/dist/platforms/google/metadata/types.js +1 -0
  60. package/dist/platforms/google/screenshots/pull.js +129 -0
  61. package/dist/platforms/google/screenshots/push.js +252 -0
  62. package/dist/platforms/google/screenshots/types.js +7 -0
  63. package/dist/validation/metadata/apple.js +31 -0
  64. package/dist/validation/metadata/files.js +58 -0
  65. package/dist/validation/metadata/google.js +46 -0
  66. package/dist/validation/screenshots/extensions.js +17 -0
  67. package/dist/validation/screenshots/files.js +55 -0
  68. package/dist/validation/screenshots/folders.js +41 -0
  69. package/dist/validation/screenshots/order.js +25 -0
  70. package/dist/writers/ensure-directory.js +16 -0
  71. package/dist/writers/order-screenshots.js +19 -0
  72. package/dist/writers/resolve-screenshot-path.js +8 -0
  73. package/dist/writers/resolve-within-base-dir.js +17 -0
  74. package/dist/writers/write-metadata.js +17 -0
  75. package/docs/API_EDITABLE_METADATA_SURFACE.md +319 -0
  76. package/docs/AUTH_SETUP.md +67 -0
  77. package/docs/DOCUMENTATION.md +285 -0
  78. package/docs/PROJECT_PLAN.md +687 -0
  79. package/docs/RELEASE_VERIFICATION.md +70 -0
  80. package/docs/TODO.md +308 -0
  81. package/examples/README.md +8 -0
  82. package/examples/metadata/apple/en-US.yml +11 -0
  83. package/examples/metadata/apple/tr.yml +11 -0
  84. package/examples/metadata/google/en-US.yml +6 -0
  85. package/examples/metadata/google/tr.yml +6 -0
  86. package/examples/screenshots/apple/en-US/APP_IPHONE_65/1.png +1 -0
  87. package/examples/screenshots/apple/tr/APP_IPHONE_65/1.png +1 -0
  88. package/examples/screenshots/google/en-US/phoneScreenshots/1.png +1 -0
  89. package/examples/screenshots/google/tr/phoneScreenshots/1.png +1 -0
  90. package/examples/storemeta.yml +29 -0
  91. package/package.json +55 -0
  92. package/storemeta.release.example.yml +27 -0
@@ -0,0 +1,95 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { StoremetaError } from "./errors.js";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { normalizeLocaleCode } from "../locales/normalize.js";
5
+ import { createAppStoreConnectClient } from "../platforms/apple/client.js";
6
+ import { createMissingAppleAppInfoLocalizations, createMissingAppleAppStoreVersionLocalizations, loadAppleMetadataDocuments, resolveAppleAppInfoResource, resolveEditableAppleAppStoreVersionResource, updateExistingAppleAppInfoLocalizations, updateExistingAppleAppStoreVersionLocalizations, } from "../platforms/apple/metadata/push.js";
7
+ import { createGooglePlayClient } from "../platforms/google/client.js";
8
+ import { withGoogleEditSession } from "../platforms/google/edits.js";
9
+ import { loadGoogleMetadataDocuments, mapGoogleMetadataDocuments, uploadGoogleListings, } from "../platforms/google/metadata/push.js";
10
+ import { createCommandContext } from "./context.js";
11
+ function filterLocalizedDocumentsByLocale(documents, locale) {
12
+ if (locale === undefined) {
13
+ return documents;
14
+ }
15
+ const normalizedLocale = normalizeLocaleCode(locale);
16
+ return documents.filter((document) => normalizeLocaleCode(document.locale) === normalizedLocale);
17
+ }
18
+ function createSuccessSummary(results) {
19
+ return {
20
+ status: "success",
21
+ successCount: results.length,
22
+ failureCount: 0,
23
+ skippedCount: 0,
24
+ results,
25
+ };
26
+ }
27
+ export async function runMetadataPushCommand(options) {
28
+ const context = await createCommandContext(options);
29
+ const app = context.app;
30
+ const selectedPlatforms = resolveSelectedPlatforms(app, context.platform);
31
+ const metadataBaseDir = resolve(dirname(context.configPath), app.settings.metadata.baseDir);
32
+ const results = [];
33
+ for (const platform of selectedPlatforms) {
34
+ if (platform === "apple") {
35
+ const appleSettings = app.settings.apple;
36
+ if (appleSettings === undefined) {
37
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define App Store Connect settings`);
38
+ }
39
+ const documents = filterLocalizedDocumentsByLocale(await loadAppleMetadataDocuments(metadataBaseDir), context.locale);
40
+ results.push(...documents.map((document) => ({
41
+ target: `apple/${document.locale}`,
42
+ success: true,
43
+ message: context.dryRun ? "Would sync Apple metadata" : "Synced Apple metadata",
44
+ })));
45
+ if (context.dryRun) {
46
+ for (const document of documents) {
47
+ console.log(`DRY RUN apple metadata ${document.locale}`);
48
+ }
49
+ continue;
50
+ }
51
+ for (const document of documents) {
52
+ console.log(`Syncing apple metadata ${document.locale}`);
53
+ }
54
+ const client = createAppStoreConnectClient(appleSettings.credentials);
55
+ const appInfo = await resolveAppleAppInfoResource(client, appleSettings.appId);
56
+ const editableVersion = await resolveEditableAppleAppStoreVersionResource(client, appleSettings.appId);
57
+ await updateExistingAppleAppInfoLocalizations(client, appInfo.id, documents);
58
+ await createMissingAppleAppInfoLocalizations(client, appInfo.id, documents);
59
+ await updateExistingAppleAppStoreVersionLocalizations(client, editableVersion.id, documents);
60
+ await createMissingAppleAppStoreVersionLocalizations(client, editableVersion.id, documents);
61
+ for (const document of documents) {
62
+ console.log(`Synced apple metadata ${document.locale}`);
63
+ }
64
+ continue;
65
+ }
66
+ const googleSettings = app.settings.google;
67
+ if (googleSettings === undefined) {
68
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define Google Play settings`);
69
+ }
70
+ const documents = filterLocalizedDocumentsByLocale(await loadGoogleMetadataDocuments(metadataBaseDir), context.locale);
71
+ const updates = mapGoogleMetadataDocuments(documents);
72
+ results.push(...updates.map((update) => ({
73
+ target: `google/${update.language}`,
74
+ success: true,
75
+ message: context.dryRun
76
+ ? "Would upload listing metadata"
77
+ : "Uploaded listing metadata",
78
+ })));
79
+ if (context.dryRun) {
80
+ for (const update of updates) {
81
+ console.log(`DRY RUN google metadata ${update.language}`);
82
+ }
83
+ continue;
84
+ }
85
+ const client = createGooglePlayClient(googleSettings.credentials);
86
+ await withGoogleEditSession(client, googleSettings.packageName, async (edit) => {
87
+ await uploadGoogleListings(client, googleSettings.packageName, edit.id, updates, {
88
+ onUploaded: (update) => {
89
+ console.log(`Uploaded google metadata ${update.language}`);
90
+ },
91
+ });
92
+ });
93
+ }
94
+ return createSuccessSummary(results);
95
+ }
@@ -0,0 +1,57 @@
1
+ import { StoremetaError } from "./errors.js";
2
+ function renderStatusLabel(success) {
3
+ return success ? "OK" : "ERR";
4
+ }
5
+ function renderCause(cause) {
6
+ if (cause instanceof Error) {
7
+ return `${cause.name}: ${cause.message}`;
8
+ }
9
+ if (typeof cause === "string") {
10
+ return cause;
11
+ }
12
+ return undefined;
13
+ }
14
+ function renderErrorLabel(error) {
15
+ switch (error.code) {
16
+ case "CONFIG_ERROR":
17
+ return "Config error";
18
+ case "AUTH_ERROR":
19
+ return "Auth error";
20
+ case "VALIDATION_ERROR":
21
+ return "Validation error";
22
+ case "API_ERROR":
23
+ return "API error";
24
+ case "FILESYSTEM_ERROR":
25
+ return "Filesystem error";
26
+ }
27
+ }
28
+ export function renderCommandSummary(summary) {
29
+ const lines = [
30
+ "Command Summary",
31
+ `Status: ${summary.status}`,
32
+ `Succeeded: ${summary.successCount}`,
33
+ `Failed: ${summary.failureCount}`,
34
+ `Skipped: ${summary.skippedCount}`,
35
+ ];
36
+ if (summary.results.length > 0) {
37
+ lines.push("Results:");
38
+ lines.push(...summary.results.map((result) => `- ${renderStatusLabel(result.success)} ${result.target}${result.message === undefined ? "" : ` - ${result.message}`}`));
39
+ }
40
+ return lines.join("\n");
41
+ }
42
+ export function renderCommandError(error, options) {
43
+ if (error instanceof StoremetaError) {
44
+ const lines = [`${renderErrorLabel(error)}: ${error.message}`];
45
+ if (options?.verbose) {
46
+ const cause = renderCause(error.cause);
47
+ if (cause !== undefined) {
48
+ lines.push(`Cause: ${cause}`);
49
+ }
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+ if (error instanceof Error) {
54
+ return options?.verbose ? `${error.name}: ${error.message}` : error.message;
55
+ }
56
+ return "An unknown error occurred";
57
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,76 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { normalizeLocaleCode } from "../locales/normalize.js";
5
+ import { createCommandContext } from "./context.js";
6
+ const DEFAULT_APPLE_SCREENSHOT_TYPES = ["APP_IPHONE_65"];
7
+ const DEFAULT_GOOGLE_SCREENSHOT_TYPES = ["phoneScreenshots"];
8
+ async function writeFileIfMissing(filePath, contents) {
9
+ try {
10
+ await writeFile(filePath, contents, {
11
+ encoding: "utf8",
12
+ flag: "wx",
13
+ });
14
+ return true;
15
+ }
16
+ catch (cause) {
17
+ if (typeof cause === "object" &&
18
+ cause !== null &&
19
+ "code" in cause &&
20
+ cause.code === "EEXIST") {
21
+ return false;
22
+ }
23
+ throw cause;
24
+ }
25
+ }
26
+ function defaultLocalesForPlatform(context, platform) {
27
+ const configuredLocales = platform === "apple"
28
+ ? context.app.settings.apple?.locales?.default
29
+ : context.app.settings.google?.locales?.default;
30
+ return (configuredLocales ?? ["en-US"]).map(normalizeLocaleCode);
31
+ }
32
+ function screenshotTypesForPlatform(platform) {
33
+ return platform === "apple"
34
+ ? DEFAULT_APPLE_SCREENSHOT_TYPES
35
+ : DEFAULT_GOOGLE_SCREENSHOT_TYPES;
36
+ }
37
+ function renderMetadataSeed(locale) {
38
+ return `locale: ${locale}\n`;
39
+ }
40
+ export async function runScaffoldCommand(options) {
41
+ const context = await createCommandContext(options);
42
+ const platforms = resolveSelectedPlatforms(context.app, context.platform);
43
+ const projectRoot = dirname(context.configPath);
44
+ const metadataBaseDir = resolve(projectRoot, context.app.settings.metadata.baseDir);
45
+ const screenshotsBaseDir = resolve(projectRoot, context.app.settings.screenshots.baseDir);
46
+ const results = [];
47
+ for (const platform of platforms) {
48
+ const locales = defaultLocalesForPlatform(context, platform);
49
+ for (const locale of locales) {
50
+ const metadataPath = join(metadataBaseDir, platform, `${locale}.yml`);
51
+ await mkdir(dirname(metadataPath), { recursive: true });
52
+ const createdMetadata = await writeFileIfMissing(metadataPath, renderMetadataSeed(locale));
53
+ results.push({
54
+ target: `metadata/${platform}/${locale}.yml`,
55
+ success: true,
56
+ message: createdMetadata ? "Created metadata file" : "Metadata file exists",
57
+ });
58
+ for (const screenshotType of screenshotTypesForPlatform(platform)) {
59
+ const screenshotDir = join(screenshotsBaseDir, platform, locale, screenshotType);
60
+ await mkdir(screenshotDir, { recursive: true });
61
+ results.push({
62
+ target: `screenshots/${platform}/${locale}/${screenshotType}`,
63
+ success: true,
64
+ message: "Ensured screenshot directory",
65
+ });
66
+ }
67
+ }
68
+ }
69
+ return {
70
+ status: "success",
71
+ successCount: results.length,
72
+ failureCount: 0,
73
+ skippedCount: 0,
74
+ results,
75
+ };
76
+ }
@@ -0,0 +1,65 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { normalizeLocaleCode } from "../locales/normalize.js";
5
+ import { createCommandContext } from "./context.js";
6
+ async function listLocalScreenshotLocales(platformDir) {
7
+ try {
8
+ const entries = await readdir(platformDir, { withFileTypes: true });
9
+ return entries
10
+ .filter((entry) => entry.isDirectory())
11
+ .map((entry) => normalizeLocaleCode(entry.name))
12
+ .sort((left, right) => left.localeCompare(right));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
18
+ async function countScreenshotSets(platformDir, locale) {
19
+ try {
20
+ const entries = await readdir(join(platformDir, locale), { withFileTypes: true });
21
+ return entries.filter((entry) => entry.isDirectory()).length;
22
+ }
23
+ catch {
24
+ return 0;
25
+ }
26
+ }
27
+ function expectedLocalesForPlatform(context, platform) {
28
+ const locales = platform === "apple"
29
+ ? context.app.settings.apple?.locales?.default
30
+ : context.app.settings.google?.locales?.default;
31
+ return (locales ?? []).map(normalizeLocaleCode).sort((left, right) => left.localeCompare(right));
32
+ }
33
+ export async function runScreenshotsDiffCommand(options) {
34
+ const context = await createCommandContext(options);
35
+ const platforms = resolveSelectedPlatforms(context.app, context.platform);
36
+ const screenshotsBaseDir = resolve(dirname(context.configPath), context.app.settings.screenshots.baseDir);
37
+ const results = [];
38
+ for (const platform of platforms) {
39
+ const platformDir = resolve(screenshotsBaseDir, platform);
40
+ const expected = expectedLocalesForPlatform(context, platform);
41
+ const actual = await listLocalScreenshotLocales(platformDir);
42
+ const missing = expected.filter((locale) => !actual.includes(locale));
43
+ const setCounts = await Promise.all(actual.map(async (locale) => `${locale}:${await countScreenshotSets(platformDir, locale)}`));
44
+ results.push({
45
+ target: `screenshots.${platform}`,
46
+ success: missing.length === 0,
47
+ message: [
48
+ `local locales: ${actual.length === 0 ? "none" : actual.join(", ")}`,
49
+ setCounts.length === 0 ? undefined : `sets: ${setCounts.join(", ")}`,
50
+ expected.length === 0 ? "expected: not configured" : `expected: ${expected.join(", ")}`,
51
+ missing.length === 0 ? undefined : `missing: ${missing.join(", ")}`,
52
+ ]
53
+ .filter((part) => part !== undefined)
54
+ .join("; "),
55
+ });
56
+ }
57
+ const failureCount = results.filter((result) => !result.success).length;
58
+ return {
59
+ status: failureCount === 0 ? "success" : "partial",
60
+ successCount: results.length - failureCount,
61
+ failureCount,
62
+ skippedCount: 0,
63
+ results,
64
+ };
65
+ }
@@ -0,0 +1,72 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { StoremetaError } from "./errors.js";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { normalizeLocaleCode } from "../locales/normalize.js";
5
+ import { createAppStoreConnectClient } from "../platforms/apple/client.js";
6
+ import { downloadAppleScreenshotSet, fetchAppleScreenshotLocalizations, fetchAppleScreenshotsForSets, fetchAppleScreenshotSetsForLocalizations, filterAppleScreenshotLocalizationsByLocale, } from "../platforms/apple/screenshots/pull.js";
7
+ import { createGooglePlayClient } from "../platforms/google/client.js";
8
+ import { withGoogleEditSession } from "../platforms/google/edits.js";
9
+ import { downloadGoogleScreenshotSet, expandGoogleScreenshotPullLocales, listGoogleImagesForLocalesAndTypes, } from "../platforms/google/screenshots/pull.js";
10
+ import { GOOGLE_SCREENSHOT_IMAGE_TYPES } from "../platforms/google/screenshots/types.js";
11
+ import { createCommandContext } from "./context.js";
12
+ function createSuccessSummary(results) {
13
+ return {
14
+ status: "success",
15
+ successCount: results.length,
16
+ failureCount: 0,
17
+ skippedCount: 0,
18
+ results,
19
+ };
20
+ }
21
+ export async function runScreenshotsPullCommand(options) {
22
+ const context = await createCommandContext(options);
23
+ const app = context.app;
24
+ const selectedPlatforms = resolveSelectedPlatforms(app, context.platform);
25
+ const screenshotsBaseDir = resolve(dirname(context.configPath), app.settings.screenshots.baseDir);
26
+ const results = [];
27
+ for (const platform of selectedPlatforms) {
28
+ if (platform === "apple") {
29
+ const appleSettings = app.settings.apple;
30
+ if (appleSettings === undefined) {
31
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define App Store Connect settings`);
32
+ }
33
+ const client = createAppStoreConnectClient(appleSettings.credentials);
34
+ const localizations = filterAppleScreenshotLocalizationsByLocale(await fetchAppleScreenshotLocalizations(client, appleSettings.appId), context.locale);
35
+ const screenshotSetsByLocalization = await fetchAppleScreenshotSetsForLocalizations(client, localizations);
36
+ const screenshotSets = await fetchAppleScreenshotsForSets(client, screenshotSetsByLocalization);
37
+ for (const screenshotSet of screenshotSets) {
38
+ const downloadedSet = await downloadAppleScreenshotSet(screenshotsBaseDir, screenshotSet);
39
+ console.log(`Pulled apple screenshots ${downloadedSet.locale}/${downloadedSet.assetType} (${downloadedSet.files.length} files)`);
40
+ results.push({
41
+ target: `apple/${downloadedSet.locale}/${downloadedSet.assetType}`,
42
+ success: true,
43
+ message: `Pulled ${downloadedSet.files.length} screenshots`,
44
+ });
45
+ }
46
+ continue;
47
+ }
48
+ const googleSettings = app.settings.google;
49
+ if (googleSettings === undefined) {
50
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define Google Play settings`);
51
+ }
52
+ const locales = expandGoogleScreenshotPullLocales(context.locale === undefined
53
+ ? (googleSettings.locales?.default ?? []).map(normalizeLocaleCode)
54
+ : [context.locale], googleSettings.screenshots);
55
+ const client = createGooglePlayClient(googleSettings.credentials);
56
+ await withGoogleEditSession(client, googleSettings.packageName, async (edit) => {
57
+ const screenshotSets = await listGoogleImagesForLocalesAndTypes(client, googleSettings.packageName, edit.id, locales, GOOGLE_SCREENSHOT_IMAGE_TYPES);
58
+ for (const screenshotSet of screenshotSets) {
59
+ const downloadedSet = await downloadGoogleScreenshotSet(screenshotsBaseDir, screenshotSet);
60
+ console.log(`Pulled google screenshots ${downloadedSet.locale}/${downloadedSet.assetType} (${downloadedSet.files.length} files)`);
61
+ results.push({
62
+ target: `google/${downloadedSet.locale}/${downloadedSet.assetType}`,
63
+ success: true,
64
+ message: `Pulled ${downloadedSet.files.length} screenshots`,
65
+ });
66
+ }
67
+ }, {
68
+ autoCommit: false,
69
+ });
70
+ }
71
+ return createSuccessSummary(results);
72
+ }
@@ -0,0 +1,96 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { StoremetaError } from "./errors.js";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { createAppStoreConnectClient } from "../platforms/apple/client.js";
5
+ import { clearAppleScreenshotUploadTargets, commitAppleScreenshotUploads, loadAppleScreenshotSets, reserveAppleScreenshotUploads, resolveOrCreateAppleScreenshotLocalizations, resolveOrCreateAppleScreenshotUploadTargets, uploadReservedAppleScreenshots, } from "../platforms/apple/screenshots/push.js";
6
+ import { createGooglePlayClient } from "../platforms/google/client.js";
7
+ import { withGoogleEditSession } from "../platforms/google/edits.js";
8
+ import { clearGoogleScreenshotTargets, loadGoogleScreenshotSets, mapGoogleScreenshotSetsToTargets, uploadGoogleScreenshotTargets, } from "../platforms/google/screenshots/push.js";
9
+ import { createCommandContext } from "./context.js";
10
+ function createSuccessSummary(results) {
11
+ return {
12
+ status: "success",
13
+ successCount: results.length,
14
+ failureCount: 0,
15
+ skippedCount: 0,
16
+ results,
17
+ };
18
+ }
19
+ export async function runScreenshotsPushCommand(options) {
20
+ const context = await createCommandContext(options);
21
+ const app = context.app;
22
+ const selectedPlatforms = resolveSelectedPlatforms(app, context.platform);
23
+ const screenshotsBaseDir = resolve(dirname(context.configPath), app.settings.screenshots.baseDir);
24
+ const selectedLocale = context.locale;
25
+ const results = [];
26
+ for (const platform of selectedPlatforms) {
27
+ if (platform === "apple") {
28
+ const appleSettings = app.settings.apple;
29
+ if (appleSettings === undefined) {
30
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define App Store Connect settings`);
31
+ }
32
+ const screenshotSets = (await loadAppleScreenshotSets(screenshotsBaseDir)).filter((set) => selectedLocale === undefined || set.locale === selectedLocale);
33
+ results.push(...screenshotSets.map((set) => ({
34
+ target: `apple/${set.locale}/${set.assetType}`,
35
+ success: true,
36
+ message: context.dryRun
37
+ ? `Would upload ${set.files.length} screenshots`
38
+ : `Uploaded ${set.files.length} screenshots`,
39
+ })));
40
+ if (context.dryRun) {
41
+ for (const screenshotSet of screenshotSets) {
42
+ console.log(`DRY RUN apple screenshots ${screenshotSet.locale}/${screenshotSet.assetType} (${screenshotSet.files.length} files)`);
43
+ }
44
+ continue;
45
+ }
46
+ const client = createAppStoreConnectClient(appleSettings.credentials);
47
+ const localizations = await resolveOrCreateAppleScreenshotLocalizations(client, appleSettings.appId, screenshotSets);
48
+ const uploadTargets = await resolveOrCreateAppleScreenshotUploadTargets(client, localizations, screenshotSets);
49
+ for (const uploadTarget of uploadTargets) {
50
+ if (options.replace) {
51
+ console.log(`Replacing apple screenshots ${uploadTarget.locale}/${uploadTarget.assetType} by deleting existing remote screenshots`);
52
+ await clearAppleScreenshotUploadTargets(client, [uploadTarget], {
53
+ clearExisting: true,
54
+ });
55
+ }
56
+ console.log(`Uploading apple screenshots ${uploadTarget.locale}/${uploadTarget.assetType} (${uploadTarget.files.length} files)`);
57
+ const reservations = await reserveAppleScreenshotUploads(client, [uploadTarget]);
58
+ await uploadReservedAppleScreenshots(reservations);
59
+ await commitAppleScreenshotUploads(client, reservations);
60
+ console.log(`Uploaded apple screenshots ${uploadTarget.locale}/${uploadTarget.assetType} (${uploadTarget.files.length} files)`);
61
+ }
62
+ continue;
63
+ }
64
+ const googleSettings = app.settings.google;
65
+ if (googleSettings === undefined) {
66
+ throw new StoremetaError("CONFIG_ERROR", `Configured app "${app.id}" does not define Google Play settings`);
67
+ }
68
+ const uploadTargets = mapGoogleScreenshotSetsToTargets(await loadGoogleScreenshotSets(screenshotsBaseDir), googleSettings).filter((target) => selectedLocale === undefined || target.targetLocale === selectedLocale);
69
+ results.push(...uploadTargets.map((target) => ({
70
+ target: `google/${target.targetLocale}/${target.imageType}`,
71
+ success: true,
72
+ message: context.dryRun
73
+ ? `Would upload ${target.files.length} screenshots`
74
+ : `Uploaded ${target.files.length} screenshots`,
75
+ })));
76
+ if (context.dryRun) {
77
+ for (const target of uploadTargets) {
78
+ console.log(`DRY RUN google screenshots ${target.targetLocale}/${target.imageType} (${target.files.length} files)`);
79
+ }
80
+ continue;
81
+ }
82
+ const client = createGooglePlayClient(googleSettings.credentials);
83
+ await withGoogleEditSession(client, googleSettings.packageName, async (edit) => {
84
+ if (options.replace) {
85
+ const clearResults = await clearGoogleScreenshotTargets(client, googleSettings.packageName, edit.id, uploadTargets, {
86
+ clearExisting: true,
87
+ });
88
+ for (const clearResult of clearResults) {
89
+ console.log(`Replacing google screenshots ${clearResult.targetLocale}/${clearResult.imageType} by deleting ${clearResult.deleted.length} existing screenshots`);
90
+ }
91
+ }
92
+ await uploadGoogleScreenshotTargets(client, googleSettings.packageName, edit.id, uploadTargets);
93
+ });
94
+ }
95
+ return createSuccessSummary(results);
96
+ }
@@ -0,0 +1,74 @@
1
+ import { requireAppleCredentials } from "../auth/apple/load-credentials.js";
2
+ import { requireGoogleCredentials } from "../auth/google/load-credentials.js";
3
+ import { resolveSelectedPlatforms } from "../config/select-platforms.js";
4
+ import { validateMetadataFiles } from "../validation/metadata/files.js";
5
+ import { validateScreenshotFileExtensions } from "../validation/screenshots/extensions.js";
6
+ import { validateScreenshotFolderStructure } from "../validation/screenshots/folders.js";
7
+ import { validateScreenshotFileOrdering } from "../validation/screenshots/order.js";
8
+ import { createCommandContext } from "./context.js";
9
+ export async function runValidateCommand(options) {
10
+ const context = await createCommandContext(options);
11
+ const results = [
12
+ {
13
+ target: "config.root",
14
+ success: true,
15
+ message: "Root config schema is valid",
16
+ },
17
+ ];
18
+ const app = context.app;
19
+ const platforms = resolveSelectedPlatforms(app, context.platform);
20
+ results.push({
21
+ target: `app.${app.id}`,
22
+ success: true,
23
+ message: `Selected platforms: ${platforms.join(", ") || "none"}`,
24
+ });
25
+ for (const platform of platforms) {
26
+ if (platform === "apple" && app.settings.apple !== undefined) {
27
+ requireAppleCredentials(app.settings.apple.credentials);
28
+ results.push({
29
+ target: `credentials.${platform}`,
30
+ success: true,
31
+ message: "Credentials are present",
32
+ });
33
+ }
34
+ if (platform === "google" && app.settings.google !== undefined) {
35
+ requireGoogleCredentials(app.settings.google.credentials);
36
+ results.push({
37
+ target: `credentials.${platform}`,
38
+ success: true,
39
+ message: "Credentials are present",
40
+ });
41
+ }
42
+ }
43
+ await validateMetadataFiles(context.configPath, app, platforms);
44
+ results.push({
45
+ target: "metadata",
46
+ success: true,
47
+ message: "Metadata files passed validation",
48
+ });
49
+ await validateScreenshotFolderStructure(context.configPath, app, platforms);
50
+ results.push({
51
+ target: "screenshots.structure",
52
+ success: true,
53
+ message: "Screenshot folder structure is valid",
54
+ });
55
+ await validateScreenshotFileExtensions(context.configPath, app, platforms);
56
+ results.push({
57
+ target: "screenshots.extensions",
58
+ success: true,
59
+ message: "Screenshot file extensions are supported",
60
+ });
61
+ await validateScreenshotFileOrdering(context.configPath, app, platforms);
62
+ results.push({
63
+ target: "screenshots.order",
64
+ success: true,
65
+ message: "Screenshot filenames are numbered correctly",
66
+ });
67
+ return {
68
+ status: "success",
69
+ successCount: results.length,
70
+ failureCount: 0,
71
+ skippedCount: 0,
72
+ results,
73
+ };
74
+ }