@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,145 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { openAscStorePresenceSession } from './app-store-presence-asc.js';
4
+ import { readStorePresenceSource, storePresencePlanCommand, } from './store-presence.js';
5
+ import { applyAppStorePresence, planAppStorePresence, } from './store-presence-app-store.js';
6
+ import { applyPlayPresence, planPlayPresence, } from './store-presence-play.js';
7
+ import { assertStoreSyncStore, readStorePlanFile, relocateStoreSyncFile, resolveStoreSyncApp, StorePlanStaleError, writeStorePlanFile, } from './store-sync.js';
8
+ const defaultPorts = {
9
+ googlePlay: loadPlayStoreListing,
10
+ appStore: openAscStorePresenceSession,
11
+ };
12
+ export async function planStorePresence(options, environment = process.env, ports = {}) {
13
+ const { app, source } = resolveInputs(options, environment);
14
+ const plan = await withSession(app, ports, options.signal, (session) => {
15
+ if (session.store === 'google-play') {
16
+ return planPlayPresence({
17
+ packageName: session.packageName,
18
+ presence: source.presence,
19
+ directory: source.directory,
20
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
21
+ }, session.port);
22
+ }
23
+ return planAppStorePresence({
24
+ appId: session.appId,
25
+ bundleId: session.bundleId,
26
+ presence: source.presence,
27
+ directory: source.directory,
28
+ }, session.port);
29
+ });
30
+ return {
31
+ schemaVersion: 1,
32
+ kind: 'mpgd-store-presence-plan',
33
+ gameRoot: path.resolve(options.game),
34
+ profile: options.profile,
35
+ presenceFile: source.file,
36
+ plan,
37
+ };
38
+ }
39
+ export function saveStorePresencePlan(file, plan) {
40
+ return writeStorePlanFile(file, plan);
41
+ }
42
+ export async function applyStorePresence(options, environment = process.env, ports = {}) {
43
+ if (!options.approved) {
44
+ throw new Error('Writing the store listing requires --approve.');
45
+ }
46
+ const saved = readSavedPlan(options.planFile);
47
+ const gameRoot = options.game === undefined ? saved.gameRoot : path.resolve(options.game);
48
+ const planOptions = {
49
+ game: gameRoot,
50
+ profile: saved.profile,
51
+ store: saved.plan.store,
52
+ presenceFile: relocateStoreSyncFile(saved.gameRoot, options.game === undefined ? undefined : gameRoot, saved.presenceFile),
53
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
54
+ };
55
+ const { app, source } = resolveInputs(planOptions, environment);
56
+ const removal = options.confirmImageRemoval === true ? { confirmImageRemoval: true } : {};
57
+ return withSession(app, ports, options.signal, (session) => {
58
+ if (session.store === 'google-play' && saved.plan.store === 'google-play') {
59
+ return applyPlayPresence({
60
+ saved: saved.plan,
61
+ plan: {
62
+ packageName: session.packageName,
63
+ presence: source.presence,
64
+ directory: source.directory,
65
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
66
+ },
67
+ port: session.port,
68
+ approved: true,
69
+ ...removal,
70
+ ...(options.changesNotSentForReview === true ? { changesNotSentForReview: true } : {}),
71
+ });
72
+ }
73
+ if (session.store === 'app-store' && saved.plan.store === 'app-store') {
74
+ if (options.changesNotSentForReview === true) {
75
+ throw new Error('--changes-not-sent-for-review applies to Google Play only.');
76
+ }
77
+ return applyAppStorePresence({
78
+ saved: saved.plan,
79
+ plan: {
80
+ appId: session.appId,
81
+ bundleId: session.bundleId,
82
+ presence: source.presence,
83
+ directory: source.directory,
84
+ },
85
+ port: session.port,
86
+ approved: true,
87
+ ...removal,
88
+ });
89
+ }
90
+ throw new StorePlanStaleError('The saved plan is for another store.', storePresencePlanCommand);
91
+ });
92
+ }
93
+ function resolveInputs(options, environment) {
94
+ assertStoreSyncStore(options.store);
95
+ // The game file first: a malformed file reports its own error without a credential.
96
+ const source = readStorePresenceSource({
97
+ gameRoot: options.game,
98
+ ...(options.presenceFile === undefined ? {} : { presenceFile: options.presenceFile }),
99
+ });
100
+ const app = resolveStoreSyncApp(options, environment);
101
+ return { app, source };
102
+ }
103
+ /** One store session per command. */
104
+ async function withSession(app, overrides, signal, action) {
105
+ const ports = { ...defaultPorts, ...overrides };
106
+ if (app.store === 'google-play') {
107
+ const port = await ports.googlePlay(app.serviceAccountFile);
108
+ return action({ store: 'google-play', packageName: app.packageName, port });
109
+ }
110
+ const session = await ports.appStore(app.credential, signal);
111
+ try {
112
+ return await action({
113
+ store: 'app-store',
114
+ appId: app.appId,
115
+ bundleId: app.bundleId,
116
+ port: session.port,
117
+ });
118
+ }
119
+ finally {
120
+ session.close();
121
+ }
122
+ }
123
+ function readSavedPlan(file) {
124
+ const raw = readStorePlanFile(file, 'store presence plan');
125
+ const plan = raw.plan;
126
+ const storeFields = typeof plan === 'object' && plan !== null && ((plan.store === 'google-play' && typeof plan.packageName === 'string' && Array.isArray(plan.languages))
127
+ || (plan.store === 'app-store' && typeof plan.appId === 'string' && typeof plan.bundleId === 'string'
128
+ && Array.isArray(plan.locales)));
129
+ if (raw.schemaVersion !== 1 || raw.kind !== 'mpgd-store-presence-plan'
130
+ || typeof raw.gameRoot !== 'string' || !path.isAbsolute(raw.gameRoot)
131
+ || typeof raw.profile !== 'string' || typeof raw.presenceFile !== 'string'
132
+ || typeof plan !== 'object' || plan === null || plan.schemaVersion !== 1 || !storeFields
133
+ || typeof plan.remoteSha256 !== 'string' || typeof plan.desiredSha256 !== 'string'
134
+ || typeof plan.changed !== 'boolean' || typeof plan.imageRemovals !== 'number') {
135
+ throw new Error('Saved store presence plan is malformed.');
136
+ }
137
+ return raw;
138
+ }
139
+ async function loadPlayStoreListing(serviceAccountFile) {
140
+ if (!existsSync(serviceAccountFile) || !statSync(serviceAccountFile).isFile()) {
141
+ throw new Error('Google Play service account file is missing.');
142
+ }
143
+ const { createServiceAccountStoreListing } = await import(new URL('./play-sdk-adapter.js', import.meta.url).href);
144
+ return createServiceAccountStoreListing(serviceAccountFile);
145
+ }
@@ -0,0 +1,116 @@
1
+ import { type StorePresence } from '@mpgd/catalog';
2
+ import type { PlayAppDetails, PlayImageType, PlayListing, PlayStoreListingPort } from './play-listing-port.js';
3
+ import { type StorePresenceTextChange } from './store-presence.js';
4
+ /**
5
+ * Google Play store presence sync: compare the store presence file with the
6
+ * main store listing (`edits.details`, `edits.listings`, `edits.images`)
7
+ * and plan the writes. Everything goes through one edit: plan opens one to
8
+ * read and discards it; apply opens one, re-plans inside it, writes, and
9
+ * commits once at the end, or discards the edit when anything fails, so
10
+ * Play never holds half an apply.
11
+ *
12
+ * Languages Play lists that the file does not name are kept. Images Play
13
+ * holds that the file does not name are kept unless the file marks the set
14
+ * `complete`; removing one then needs `--confirm-image-removal`.
15
+ */
16
+ export type PlayPresenceAction = 'create' | 'update' | 'unchanged';
17
+ export interface PlannedPlayImage {
18
+ /** Path relative to the store presence file. */
19
+ readonly path: string;
20
+ readonly sha256: string;
21
+ }
22
+ export interface PlannedPlayImageSlot {
23
+ readonly imageType: PlayImageType;
24
+ /** `add` uploads after Play's images; `replace` deletes the slot first. */
25
+ readonly action: 'unchanged' | 'add' | 'replace';
26
+ readonly complete: boolean;
27
+ /** Uploads, in order. */
28
+ readonly upload: readonly PlannedPlayImage[];
29
+ /** SHA-256 of Play's images that stay as they are. */
30
+ readonly keep: readonly string[];
31
+ /** Play's images the write removes and does not upload again. */
32
+ readonly remove: readonly {
33
+ readonly id: string;
34
+ readonly sha256: string;
35
+ }[];
36
+ readonly notes: readonly string[];
37
+ }
38
+ export interface PlannedPlayLanguage {
39
+ /** The language as Play spells it (BCP-47 casing for a new one). */
40
+ readonly language: string;
41
+ readonly action: PlayPresenceAction;
42
+ readonly text: readonly StorePresenceTextChange[];
43
+ /** `edits.listings.update` body when the text changes. */
44
+ readonly listing?: PlayListing & {
45
+ readonly language: string;
46
+ };
47
+ readonly images: readonly PlannedPlayImageSlot[];
48
+ }
49
+ export interface PlayPresencePlan {
50
+ readonly schemaVersion: 1;
51
+ readonly store: 'google-play';
52
+ readonly packageName: string;
53
+ readonly details?: {
54
+ readonly changes: readonly {
55
+ readonly field: string;
56
+ readonly from?: string;
57
+ readonly to: string;
58
+ }[];
59
+ /** `edits.details.update` body: Play's details with the file's contact fields. */
60
+ readonly write: PlayAppDetails;
61
+ };
62
+ readonly languages: readonly PlannedPlayLanguage[];
63
+ /** Languages on Play that the file does not name. Never deleted. */
64
+ readonly remoteOnlyLanguages: readonly string[];
65
+ /** Play images a complete set removes; apply needs --confirm-image-removal. */
66
+ readonly imageRemovals: number;
67
+ readonly changed: boolean;
68
+ readonly notes: readonly string[];
69
+ /** What the sync cannot do; the reviewer does it in Play Console. */
70
+ readonly manual: readonly string[];
71
+ /** Digest of everything read from Play. */
72
+ readonly remoteSha256: string;
73
+ /** Digest of the planned writes. */
74
+ readonly desiredSha256: string;
75
+ }
76
+ export interface PlayPresencePlanInput {
77
+ readonly packageName: string;
78
+ readonly presence: StorePresence;
79
+ /** Directory of the store presence file; graphics resolve against it. */
80
+ readonly directory: string;
81
+ readonly signal?: AbortSignal;
82
+ }
83
+ /** Read Play through a throwaway edit and plan; never writes. */
84
+ export declare function planPlayPresence(input: PlayPresencePlanInput, port: PlayStoreListingPort): Promise<PlayPresencePlan>;
85
+ export interface PlayPresenceApplyResult {
86
+ readonly packageName: string;
87
+ readonly status: 'committed' | 'unchanged';
88
+ readonly languages: readonly {
89
+ readonly language: string;
90
+ readonly listing: PlayPresenceAction;
91
+ readonly images: number;
92
+ }[];
93
+ }
94
+ export declare class PlayPresenceApplyError extends Error {
95
+ readonly stage: string;
96
+ /** `discarded`: Play discarded the edit; `unknown`: the commit may or may not have landed. */
97
+ readonly outcome: 'discarded' | 'unknown';
98
+ constructor(stage: string,
99
+ /** `discarded`: Play discarded the edit; `unknown`: the commit may or may not have landed. */
100
+ outcome: 'discarded' | 'unknown', cause: unknown);
101
+ }
102
+ /**
103
+ * Apply a reviewed plan in one edit: re-plan inside the edit, refuse when
104
+ * Play or the files moved past the reviewed plan, write, then commit once.
105
+ * A plan whose changes already landed is a no-op, so a re-run is safe.
106
+ */
107
+ export declare function applyPlayPresence(input: {
108
+ readonly saved: PlayPresencePlan;
109
+ readonly plan: PlayPresencePlanInput;
110
+ readonly port: PlayStoreListingPort;
111
+ readonly approved: boolean;
112
+ readonly confirmImageRemoval?: boolean;
113
+ readonly changesNotSentForReview?: boolean;
114
+ }): Promise<PlayPresenceApplyResult>;
115
+ /** The plan for the terminal: per language, the text and image changes. */
116
+ export declare function formatPlayPresencePlan(plan: PlayPresencePlan): string;