@mpgd/cli 0.62.0 → 0.64.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 (39) hide show
  1. package/dist/app-store-iap-asc.d.ts +14 -0
  2. package/dist/app-store-iap-asc.js +219 -0
  3. package/dist/app-store-iap-port.d.ts +125 -0
  4. package/dist/app-store-iap-port.js +14 -0
  5. package/dist/app-store-presence-asc.d.ts +23 -0
  6. package/dist/app-store-presence-asc.js +162 -0
  7. package/dist/app-store-presence-port.d.ts +101 -0
  8. package/dist/app-store-presence-port.js +13 -0
  9. package/dist/asc-session.d.ts +43 -0
  10. package/dist/asc-session.js +118 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.js +169 -1
  13. package/dist/native-build-info.json +2 -2
  14. package/dist/native-build-target.js +411 -339
  15. package/dist/play-listing-port.d.ts +88 -0
  16. package/dist/play-listing-port.js +12 -0
  17. package/dist/play-product-port.d.ts +101 -0
  18. package/dist/play-product-port.js +7 -0
  19. package/dist/play-sdk-adapter.js +168 -15
  20. package/dist/store-presence-app-store.d.ts +139 -0
  21. package/dist/store-presence-app-store.js +537 -0
  22. package/dist/store-presence-command.d.ts +50 -0
  23. package/dist/store-presence-command.js +145 -0
  24. package/dist/store-presence-play.d.ts +116 -0
  25. package/dist/store-presence-play.js +495 -0
  26. package/dist/store-presence.d.ts +106 -0
  27. package/dist/store-presence.js +173 -0
  28. package/dist/store-products-app-store.d.ts +144 -0
  29. package/dist/store-products-app-store.js +632 -0
  30. package/dist/store-products-command.d.ts +51 -0
  31. package/dist/store-products-command.js +155 -0
  32. package/dist/store-products-play.d.ts +112 -0
  33. package/dist/store-products-play.js +622 -0
  34. package/dist/store-products.d.ts +38 -0
  35. package/dist/store-products.js +56 -0
  36. package/dist/store-sync.d.ts +50 -0
  37. package/dist/store-sync.js +137 -0
  38. package/dist/testflight-submission.d.ts +1 -1
  39. package/package.json +13 -13
@@ -0,0 +1,13 @@
1
+ /**
2
+ * App Store Connect store page port: app information localizations (name,
3
+ * subtitle, privacy policy URL), App Store version localizations (description,
4
+ * keywords, promotional text, what's new, URLs) and their screenshot sets.
5
+ * `app-store-presence-asc.ts` implements it with the pinned `asc` CLI; the
6
+ * planner and tests see only these shapes.
7
+ *
8
+ * Resources (App Store Connect API):
9
+ * - App infos and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-infos
10
+ * - App Store versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-store-versions
11
+ * - Screenshot sets and screenshots: https://developer.apple.com/documentation/appstoreconnectapi/app-screenshot-sets
12
+ */
13
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * One session on the pinned `asc` 5.5.0 CLI, with the same binary pin,
3
+ * isolated environment and API key as the TestFlight submission, and the
4
+ * JSON helpers every asc-backed port reads its output with. Each command is
5
+ * one `asc ... --output json` subprocess; the JSON is App Store Connect's
6
+ * own resource document, as `asc` received it
7
+ * (`internal/asc/types/resources.go` at the 5.5.0 tag).
8
+ */
9
+ /** Runs one asc command with `--output json` appended and returns the parsed JSON. */
10
+ export type AscCommandRunner = (args: readonly string[], timeoutMs: number) => Promise<unknown>;
11
+ export interface AscCredential {
12
+ readonly ascBinary: string;
13
+ readonly apiKeyId: string;
14
+ readonly apiIssuerId: string;
15
+ readonly apiPrivateKeyBase64: string;
16
+ }
17
+ export declare const ascLookupTimeoutMs = 60000;
18
+ export declare const ascListTimeoutMs: number;
19
+ export declare const ascUploadTimeoutMs: number;
20
+ /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
21
+ export declare const ascExitNotFound = 4;
22
+ /** Copy and verify the pinned binary into a private directory; `close` removes it. */
23
+ export declare function openAscSession(credential: AscCredential, signal?: AbortSignal): Promise<{
24
+ readonly run: AscCommandRunner;
25
+ close(): void;
26
+ }>;
27
+ export interface AscJsonResource {
28
+ readonly id: string;
29
+ readonly attributes: Record<string, unknown>;
30
+ readonly relationships?: unknown;
31
+ }
32
+ export declare function single(value: unknown, command: string): AscJsonResource;
33
+ export declare function list(value: unknown, command: string): AscJsonResource[];
34
+ export declare function toResource(value: unknown): AscJsonResource | undefined;
35
+ /**
36
+ * App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
37
+ * decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
38
+ */
39
+ export declare function isEmptyResource(value: unknown): boolean;
40
+ export declare function relationshipId(relationships: unknown, name: string): string | undefined;
41
+ export declare function requiredString(value: unknown, label: string): string;
42
+ export declare function optionalString<K extends string>(key: K, value: unknown): Partial<Record<K, string>>;
43
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,118 @@
1
+ import { chmodSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { runReleaseProcess } from './deploy-process.js';
5
+ import { isolatedAscEnvironment, verifyPinnedAscBinary } from './testflight-submission.js';
6
+ export const ascLookupTimeoutMs = 60_000;
7
+ export const ascListTimeoutMs = 3 * 60_000;
8
+ export const ascUploadTimeoutMs = 10 * 60_000;
9
+ const maxJsonBytes = 8 * 1024 * 1024;
10
+ /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
11
+ export const ascExitNotFound = 4;
12
+ /** Copy and verify the pinned binary into a private directory; `close` removes it. */
13
+ export async function openAscSession(credential, signal) {
14
+ if (!path.isAbsolute(credential.ascBinary)) {
15
+ throw new Error('asc binary must be an absolute file path.');
16
+ }
17
+ const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-asc-iap-'));
18
+ try {
19
+ const binary = path.join(tempRoot, 'asc');
20
+ copyFileSync(credential.ascBinary, binary);
21
+ chmodSync(binary, 0o700);
22
+ await verifyPinnedAscBinary(binary);
23
+ const environment = isolatedAscEnvironment(credential, tempRoot);
24
+ const run = async (args, timeoutMs) => {
25
+ const result = await runReleaseProcess({
26
+ command: binary,
27
+ args: [...args, '--output', 'json'],
28
+ cwd: tempRoot,
29
+ environment,
30
+ timeoutMs,
31
+ maxOutputBytes: maxJsonBytes,
32
+ captureMachineStdout: true,
33
+ secretValues: [credential.apiKeyId, credential.apiIssuerId, credential.apiPrivateKeyBase64],
34
+ signal,
35
+ });
36
+ if (result.truncated || result.machineStdout === undefined) {
37
+ throw new Error('asc JSON output was truncated or unavailable.');
38
+ }
39
+ try {
40
+ return JSON.parse(result.machineStdout);
41
+ }
42
+ catch {
43
+ throw new Error('asc returned invalid JSON.');
44
+ }
45
+ };
46
+ return {
47
+ run,
48
+ close: () => rmSync(tempRoot, { recursive: true, force: true }),
49
+ };
50
+ }
51
+ catch (error) {
52
+ rmSync(tempRoot, { recursive: true, force: true });
53
+ throw error;
54
+ }
55
+ }
56
+ export function single(value, command) {
57
+ const resource = isRecord(value) ? toResource(value.data) : undefined;
58
+ if (resource === undefined) {
59
+ throw new Error(`asc ${command} returned no resource.`);
60
+ }
61
+ return resource;
62
+ }
63
+ export function list(value, command) {
64
+ const resources = isRecord(value) && Array.isArray(value.data)
65
+ ? value.data.map(toResource)
66
+ : undefined;
67
+ if (resources === undefined || resources.some((resource) => resource === undefined)) {
68
+ throw new Error(`asc ${command} returned no resource list.`);
69
+ }
70
+ return resources;
71
+ }
72
+ export function toResource(value) {
73
+ if (!isRecord(value) || typeof value.id !== 'string' || value.id === '') {
74
+ return undefined;
75
+ }
76
+ return {
77
+ id: value.id,
78
+ attributes: isRecord(value.attributes) ? value.attributes : {},
79
+ ...(value.relationships === undefined ? {} : { relationships: value.relationships }),
80
+ };
81
+ }
82
+ /**
83
+ * App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
84
+ * decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
85
+ */
86
+ export function isEmptyResource(value) {
87
+ if (!isRecord(value)) {
88
+ return false;
89
+ }
90
+ return value.data === null
91
+ || (isRecord(value.data) && (value.data.id === undefined || value.data.id === ''));
92
+ }
93
+ export function relationshipId(relationships, name) {
94
+ if (!isRecord(relationships)) {
95
+ return undefined;
96
+ }
97
+ const relationship = relationships[name];
98
+ if (!isRecord(relationship) || !isRecord(relationship.data)) {
99
+ return undefined;
100
+ }
101
+ return typeof relationship.data.id === 'string' ? relationship.data.id : undefined;
102
+ }
103
+ export function requiredString(value, label) {
104
+ if (typeof value !== 'string') {
105
+ throw new Error(`asc output has no ${label}.`);
106
+ }
107
+ return value;
108
+ }
109
+ export function optionalString(key, value) {
110
+ const result = {};
111
+ if (typeof value === 'string' && value !== '') {
112
+ result[key] = value;
113
+ }
114
+ return result;
115
+ }
116
+ export function isRecord(value) {
117
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
118
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,16 @@ export { runNativeDeployment, readStoreCredential, type RunNativeDeploymentInput
9
9
  export { planRecordedAndroidSubmission, submitRecordedNativeTarget, type NativeStoreCredential, type SubmitRecordedNativeTargetInput, } from './native-deploy-submission.js';
10
10
  export { PlaySubmissionUncertainError, planVerifiedAndroidBundleSubmission, submitVerifiedAndroidBundle, type PlayInternalSubmissionInput, type PlayInternalSubmissionResult, type PlaySubmissionPlan, } from './play-internal-submission.js';
11
11
  export { createPlayReleaseRequest, defaultPlayReleaseRequest, maximumPlayReleaseNoteCharacters, parsePlayReleaseNotes, type CreatePlayReleaseRequestInput, type PlayNewReleaseStatus, type PlayReleaseNote, type PlayReleaseRequest, type PlayReleaseStatus, } from './play-release-options.js';
12
+ export { applyStoreProducts, planStoreProducts, saveStoreProductPlan, type StoreProductApplyOptions, type StoreProductPlanFile, type StoreProductPlanOptions, type StoreProductPorts, type StoreProductStore, } from './store-products-command.js';
13
+ export { applyPlayProductPlan, formatPlayProductPlan, planPlayProducts, PlayProductApplyError, type PlannedPlayProduct, type PlayProductApplyResult, type PlayProductChange, type PlayProductPlan, type PlayProductPlanInput, } from './store-products-play.js';
14
+ export { AppStoreProductApplyError, applyAppStoreProductPlan, formatAppStoreProductPlan, planAppStoreProducts, type AppStoreProductApplyResult, type AppStoreProductChange, type AppStoreProductPlan, type AppStoreProductPlanInput, type PlannedAppStoreProduct, } from './store-products-app-store.js';
15
+ export { StoreProductPlanStaleError } from './store-products.js';
16
+ export { StorePlanStaleError } from './store-sync.js';
17
+ export { applyStorePresence, planStorePresence, saveStorePresencePlan, type StorePresenceApplyOptions, type StorePresencePlanFile, type StorePresencePlanOptions, type StorePresencePorts, type StorePresenceStore, } from './store-presence-command.js';
18
+ export { applyPlayPresence, formatPlayPresencePlan, planPlayPresence, PlayPresenceApplyError, type PlannedPlayImageSlot, type PlannedPlayLanguage, type PlayPresenceApplyResult, type PlayPresencePlan, type PlayPresencePlanInput, } from './store-presence-play.js';
19
+ export { applyAppStorePresence, AppStorePresenceApplyError, formatAppStorePresencePlan, planAppStorePresence, type AppStorePresenceApplyResult, type AppStorePresencePlan, type AppStorePresencePlanInput, type PlannedAppStoreLocale, type PlannedAscScreenshotSet, } from './store-presence-app-store.js';
20
+ export type { AscAppInfo, AscAppInfoLocalization, AscAppStoreVersion, AscScreenshot, AscScreenshotSet, AscStorePresencePort, AscVersionLocalization, AscVersionLocalizationFields, } from './app-store-presence-port.js';
21
+ export type { PlayAppDetails, PlayImage, PlayImageType, PlayListing, PlayStoreListingPort, } from './play-listing-port.js';
12
22
  export { PlayPromoteUncertainError, promotePlayRelease, type PlayPromoteInput, type PlayPromoteResult, } from './play-release-promote.js';
13
23
  export { PlayRolloutUncertainError, changePlayRollout, type PlayRolloutAction, type PlayRolloutInput, type PlayRolloutResult, } from './play-release-rollout.js';
14
24
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, type IosTestFlightStatus, type IosTestFlightSubmissionInput, type IosTestFlightSubmissionResult, } from './testflight-submission.js';
package/dist/index.js CHANGED
@@ -36,6 +36,12 @@ import { readStoreCredential, runNativeDeployment } from './native-deploy-run.js
36
36
  import { isNativeDeployApproved, planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
37
37
  import { createPlayReleaseRequest, parsePlayUserFraction, readPlayReleaseNotesFile, } from './play-release-options.js';
38
38
  import { promotePlayRelease } from './play-release-promote.js';
39
+ import { applyStoreProducts, planStoreProducts, saveStoreProductPlan, } from './store-products-command.js';
40
+ import { formatAppStoreProductPlan } from './store-products-app-store.js';
41
+ import { formatPlayProductPlan } from './store-products-play.js';
42
+ import { applyStorePresence, planStorePresence, saveStorePresencePlan, } from './store-presence-command.js';
43
+ import { formatPlayPresencePlan } from './store-presence-play.js';
44
+ import { formatAppStorePresencePlan } from './store-presence-app-store.js';
39
45
  import { changePlayRollout } from './play-release-rollout.js';
40
46
  import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
41
47
  import { uploadRecordedCrashSymbols } from './crash-symbols-upload.js';
@@ -50,6 +56,14 @@ export { runNativeDeployment, readStoreCredential, } from './native-deploy-run.j
50
56
  export { planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
51
57
  export { PlaySubmissionUncertainError, planVerifiedAndroidBundleSubmission, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
52
58
  export { createPlayReleaseRequest, defaultPlayReleaseRequest, maximumPlayReleaseNoteCharacters, parsePlayReleaseNotes, } from './play-release-options.js';
59
+ export { applyStoreProducts, planStoreProducts, saveStoreProductPlan, } from './store-products-command.js';
60
+ export { applyPlayProductPlan, formatPlayProductPlan, planPlayProducts, PlayProductApplyError, } from './store-products-play.js';
61
+ export { AppStoreProductApplyError, applyAppStoreProductPlan, formatAppStoreProductPlan, planAppStoreProducts, } from './store-products-app-store.js';
62
+ export { StoreProductPlanStaleError } from './store-products.js';
63
+ export { StorePlanStaleError } from './store-sync.js';
64
+ export { applyStorePresence, planStorePresence, saveStorePresencePlan, } from './store-presence-command.js';
65
+ export { applyPlayPresence, formatPlayPresencePlan, planPlayPresence, PlayPresenceApplyError, } from './store-presence-play.js';
66
+ export { applyAppStorePresence, AppStorePresenceApplyError, formatAppStorePresencePlan, planAppStorePresence, } from './store-presence-app-store.js';
53
67
  export { PlayPromoteUncertainError, promotePlayRelease, } from './play-release-promote.js';
54
68
  export { PlayRolloutUncertainError, changePlayRollout, } from './play-release-rollout.js';
55
69
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
@@ -210,6 +224,7 @@ export async function runMpgdCli(args) {
210
224
  assets: assetsCommand,
211
225
  game: gameCommand,
212
226
  deploy: deployCommand,
227
+ store: storeCommand,
213
228
  legal: legalCommand,
214
229
  target: targetCommand,
215
230
  kit: kitCommand,
@@ -262,7 +277,7 @@ const entryCommand = defineI18n({
262
277
  ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
263
278
  }),
264
279
  run: () => {
265
- console.info('Use a sub-command: assets, game, deploy, legal, target, kit.');
280
+ console.info('Use a sub-command: assets, game, deploy, store, legal, target, kit.');
266
281
  console.info('Run "pnpm mpgd --help" for available commands.');
267
282
  },
268
283
  });
@@ -743,6 +758,159 @@ const deployCommand = defineI18n({
743
758
  }),
744
759
  },
745
760
  });
761
+ const storeCommand = defineI18n({
762
+ name: 'store',
763
+ description: 'Sync store-side configuration from game files.',
764
+ resource: commandResource({
765
+ en: 'Sync store-side configuration from game files.',
766
+ ko: '게임 파일에서 스토어 쪽 설정을 동기화합니다.',
767
+ }),
768
+ subCommands: {
769
+ products: defineI18n({
770
+ name: 'products',
771
+ description: 'Plan and apply one-time products from the catalog and store listing file.',
772
+ resource: commandResource({
773
+ en: 'Plan and apply one-time products from the catalog and store listing file.',
774
+ ko: '카탈로그와 스토어 리스팅 파일로 일회성 상품을 계획하고 적용합니다.',
775
+ }),
776
+ subCommands: {
777
+ plan: defineI18n({
778
+ name: 'plan',
779
+ description: 'Read the store products and print the changes; never writes to the store.',
780
+ resource: commandResource({
781
+ en: 'Read the store products and print the changes; never writes to the store.',
782
+ ko: '스토어 상품을 읽고 변경 사항을 출력합니다. 스토어에 쓰지 않습니다.',
783
+ }),
784
+ args: {
785
+ game: { type: 'string', required: false, description: 'Game project directory.' },
786
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
787
+ store: { type: 'string', required: true, description: 'google-play or app-store.' },
788
+ catalog: { type: 'string', required: false, description: 'Product catalog path, relative to the current directory; defaults to mpgd.catalog.json in the game.' },
789
+ listings: { type: 'string', required: false, description: 'Store listing file path, relative to the current directory; defaults to mpgd.store-listings.json in the game.' },
790
+ 'catalog-target': { type: 'string', required: false, description: 'Catalog platformProductIds key of the store; defaults to android (Google Play) or ios (App Store).' },
791
+ 'keep-live-prices': { type: 'boolean', required: false, description: 'Leave the prices of products players can already buy unchanged.' },
792
+ out: { type: 'string', required: false, description: 'New plan JSON path for apply; existing files are not overwritten.' },
793
+ },
794
+ run: async (ctx) => {
795
+ const catalog = readOptionalString(ctx.values.catalog);
796
+ const listings = readOptionalString(ctx.values.listings);
797
+ const catalogTarget = readOptionalString(ctx.values['catalog-target']);
798
+ const result = await planStoreProducts({
799
+ game: readOptionalString(ctx.values.game) ?? '.',
800
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
801
+ store: readRequiredCliOption(ctx.values.store, '--store'),
802
+ ...(catalog === undefined ? {} : { catalogFile: catalog }),
803
+ ...(listings === undefined ? {} : { listingsFile: listings }),
804
+ ...(catalogTarget === undefined ? {} : { catalogTarget }),
805
+ ...(ctx.values['keep-live-prices'] === true ? { keepLivePrices: true } : {}),
806
+ });
807
+ console.info(result.plan.store === 'google-play'
808
+ ? formatPlayProductPlan(result.plan)
809
+ : formatAppStoreProductPlan(result.plan));
810
+ const out = readOptionalString(ctx.values.out);
811
+ if (out !== undefined) {
812
+ console.info(`Wrote store product plan: ${saveStoreProductPlan(out, result)}`);
813
+ }
814
+ },
815
+ }),
816
+ apply: defineI18n({
817
+ name: 'apply',
818
+ description: 'Write the reviewed plan; refuses when the store or the files changed since.',
819
+ resource: commandResource({
820
+ en: 'Write the reviewed plan; refuses when the store or the files changed since.',
821
+ ko: '검토한 계획을 적용합니다. 이후 스토어나 파일이 바뀌었으면 거부합니다.',
822
+ }),
823
+ args: {
824
+ plan: { type: 'string', required: true, description: 'Plan JSON written by "store products plan --out".' },
825
+ game: { type: 'string', required: false, description: 'Current game directory if the game moved.' },
826
+ approve: { type: 'boolean', required: false, description: 'Approve the store writes.' },
827
+ 'confirm-price-change': { type: 'boolean', required: false, description: 'Confirm price changes on products players can already buy.' },
828
+ },
829
+ run: async (ctx) => {
830
+ const game = readOptionalString(ctx.values.game);
831
+ const result = await applyStoreProducts({
832
+ planFile: readRequiredCliOption(ctx.values.plan, '--plan'),
833
+ ...(game === undefined ? {} : { game }),
834
+ approved: ctx.values.approve === true,
835
+ ...(ctx.values['confirm-price-change'] === true ? { confirmPriceChange: true } : {}),
836
+ });
837
+ console.info(JSON.stringify(result, null, 2));
838
+ },
839
+ }),
840
+ },
841
+ }),
842
+ presence: defineI18n({
843
+ name: 'presence',
844
+ description: 'Plan and apply the store page text and graphics from mpgd.store-presence.json.',
845
+ resource: commandResource({
846
+ en: 'Plan and apply the store page text and graphics from mpgd.store-presence.json.',
847
+ ko: 'mpgd.store-presence.json으로 스토어 페이지 문구와 그래픽을 계획하고 적용합니다.',
848
+ }),
849
+ subCommands: {
850
+ plan: defineI18n({
851
+ name: 'plan',
852
+ description: 'Read the store listing and print the changes; never writes to the store.',
853
+ resource: commandResource({
854
+ en: 'Read the store listing and print the changes; never writes to the store.',
855
+ ko: '스토어 리스팅을 읽고 변경 사항을 출력합니다. 스토어에 쓰지 않습니다.',
856
+ }),
857
+ args: {
858
+ game: { type: 'string', required: false, description: 'Game project directory.' },
859
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
860
+ store: { type: 'string', required: true, description: 'google-play or app-store.' },
861
+ presence: { type: 'string', required: false, description: 'Store presence file path, relative to the current directory; defaults to mpgd.store-presence.json in the game.' },
862
+ out: { type: 'string', required: false, description: 'New plan JSON path for apply; existing files are not overwritten.' },
863
+ },
864
+ run: async (ctx) => {
865
+ const presence = readOptionalString(ctx.values.presence);
866
+ const result = await planStorePresence({
867
+ game: readOptionalString(ctx.values.game) ?? '.',
868
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
869
+ store: readRequiredCliOption(ctx.values.store, '--store'),
870
+ ...(presence === undefined ? {} : { presenceFile: presence }),
871
+ });
872
+ console.info(result.plan.store === 'google-play'
873
+ ? formatPlayPresencePlan(result.plan)
874
+ : formatAppStorePresencePlan(result.plan));
875
+ const out = readOptionalString(ctx.values.out);
876
+ if (out !== undefined) {
877
+ console.info(`Wrote store presence plan: ${saveStorePresencePlan(out, result)}`);
878
+ }
879
+ },
880
+ }),
881
+ apply: defineI18n({
882
+ name: 'apply',
883
+ description: 'Write the reviewed plan (one Play edit); refuses when the store or the files changed since.',
884
+ resource: commandResource({
885
+ en: 'Write the reviewed plan (one Play edit); refuses when the store or the files changed since.',
886
+ ko: '검토한 계획을 적용합니다(Play는 한 번의 편집). 이후 스토어나 파일이 바뀌었으면 거부합니다.',
887
+ }),
888
+ args: {
889
+ plan: { type: 'string', required: true, description: 'Plan JSON written by "store presence plan --out".' },
890
+ game: { type: 'string', required: false, description: 'Current game directory if the game moved.' },
891
+ approve: { type: 'boolean', required: false, description: 'Approve the store writes.' },
892
+ 'confirm-image-removal': { type: 'boolean', required: false, description: 'Confirm removing store images a complete set no longer lists.' },
893
+ 'changes-not-sent-for-review': { type: 'boolean', required: false, description: 'Google Play: commit without sending for review; send it from Play Console.' },
894
+ },
895
+ run: async (ctx) => {
896
+ const game = readOptionalString(ctx.values.game);
897
+ const result = await applyStorePresence({
898
+ planFile: readRequiredCliOption(ctx.values.plan, '--plan'),
899
+ ...(game === undefined ? {} : { game }),
900
+ approved: ctx.values.approve === true,
901
+ ...(ctx.values['confirm-image-removal'] === true ? { confirmImageRemoval: true } : {}),
902
+ ...(ctx.values['changes-not-sent-for-review'] === true ? { changesNotSentForReview: true } : {}),
903
+ });
904
+ console.info(JSON.stringify(result, null, 2));
905
+ },
906
+ }),
907
+ },
908
+ }),
909
+ },
910
+ run: () => {
911
+ console.info('Use "mpgd store products plan|apply" or "mpgd store presence plan|apply".');
912
+ },
913
+ });
746
914
  const gameCommand = defineI18n({
747
915
  name: 'game',
748
916
  description: 'Create standalone Phaser game starters.',
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "ea82dfadb6cdcd6b842b753f4ce10b52ad1a1fb4",
2
+ "kitGitSha": "ab7954c63c4b6648148b4af4f34058d2f3ca7631",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.62.0"
4
+ "packageVersion": "0.64.0"
5
5
  }