@mpgd/cli 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 (29) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +3 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.js +930 -0
  6. package/package.json +64 -0
  7. package/templates/phaser-game/README.md +66 -0
  8. package/templates/phaser-game/agent/acceptance.md +16 -0
  9. package/templates/phaser-game/agent/brief.md +20 -0
  10. package/templates/phaser-game/agent/game-manifest.json +41 -0
  11. package/templates/phaser-game/index.html +12 -0
  12. package/templates/phaser-game/mpgd.targets.json +42 -0
  13. package/templates/phaser-game/package.json +35 -0
  14. package/templates/phaser-game/src/env.d.ts +12 -0
  15. package/templates/phaser-game/src/game/state.ts +29 -0
  16. package/templates/phaser-game/src/i18n/messages.ts +64 -0
  17. package/templates/phaser-game/src/main.ts +78 -0
  18. package/templates/phaser-game/src/platform/gameServices.ts +70 -0
  19. package/templates/phaser-game/src/platform/installPlatform.ts +96 -0
  20. package/templates/phaser-game/src/platform/runtimeDetector.ts +30 -0
  21. package/templates/phaser-game/src/runtime/createGame.ts +28 -0
  22. package/templates/phaser-game/src/runtime/gameContext.ts +18 -0
  23. package/templates/phaser-game/src/runtime/id.ts +6 -0
  24. package/templates/phaser-game/src/scenes/BootScene.ts +11 -0
  25. package/templates/phaser-game/src/scenes/LobbyScene.ts +65 -0
  26. package/templates/phaser-game/src/scenes/PlayScene.ts +121 -0
  27. package/templates/phaser-game/src/styles.css +31 -0
  28. package/templates/phaser-game/tsconfig.json +27 -0
  29. package/templates/phaser-game/vite.config.ts +35 -0
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@mpgd/cli",
3
+ "version": "0.1.0",
4
+ "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/imjlk/mpgd-kit.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/imjlk/mpgd-kit/issues"
13
+ },
14
+ "homepage": "https://github.com/imjlk/mpgd-kit#readme",
15
+ "keywords": [
16
+ "mpgd",
17
+ "phaser",
18
+ "vite",
19
+ "typescript",
20
+ "capacitor",
21
+ "apps-in-toss",
22
+ "devvit",
23
+ "game-development",
24
+ "game-distribution",
25
+ "cli"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "templates"
38
+ ],
39
+ "bin": {
40
+ "mpgd": "./dist/bin.js"
41
+ },
42
+ "dependencies": {
43
+ "@gunshi/plugin-completion": "^0.35.1",
44
+ "@gunshi/plugin-i18n": "^0.35.1",
45
+ "@gunshi/resources": "^0.35.1",
46
+ "gunshi": "^0.35.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^24.0.0",
50
+ "ttsc": "0.16.9",
51
+ "typescript": "7.0.1-rc"
52
+ },
53
+ "main": "./dist/index.js",
54
+ "types": "./dist/index.d.ts",
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "check": "ttsc --noEmit -p tsconfig.json",
60
+ "lint": "ttsc --noEmit -p tsconfig.json",
61
+ "format": "ttsc format",
62
+ "fix": "ttsc fix"
63
+ }
64
+ }
@@ -0,0 +1,66 @@
1
+ # __GAME_TITLE__
2
+
3
+ Standalone Phaser 4 game starter generated by `@mpgd/create-game`.
4
+
5
+ ## Local Loop
6
+
7
+ ```sh
8
+ pnpm install
9
+ pnpm dev
10
+ pnpm check
11
+ pnpm build
12
+ ```
13
+
14
+ The starter keeps game code behind `PlatformGateway` and demonstrates:
15
+
16
+ - target-aware adapter selection through `APP_TARGET`
17
+ - target-config/effective-config runtime snapshots
18
+ - local translation keys
19
+ - optional `@mpgd/game-services` client wiring
20
+ - best-effort analytics events
21
+ - rewarded ad smoke action
22
+
23
+ ## Target Builds
24
+
25
+ Target builds require an `mpgd-kit` checkout because the target wrappers live in
26
+ the kit repository. Pass that checkout with `--kit-path`:
27
+
28
+ ```sh
29
+ pnpm dlx @mpgd/cli target build-all \
30
+ --targets-file ./mpgd.targets.json \
31
+ --kit-path ../mpgd-kit \
32
+ --targets web,ait \
33
+ --ait-variant wrapper
34
+ pnpm dlx @mpgd/cli target smoke-all \
35
+ --targets-file ./mpgd.targets.json \
36
+ --kit-path ../mpgd-kit \
37
+ --targets web,ait
38
+ ```
39
+
40
+ When developing the kit itself, the same commands work through the repository
41
+ script:
42
+
43
+ ```sh
44
+ pnpm --dir ../mpgd-kit mpgd target build-all \
45
+ --targets-file "$PWD/mpgd.targets.json" \
46
+ --kit-path ../mpgd-kit \
47
+ --targets web,ait \
48
+ --ait-variant wrapper
49
+ ```
50
+
51
+ `mpgd.targets.json` contains `${MPGD_KIT_PATH}` tokens. The CLI resolves them
52
+ into `.mpgd.targets.generated.json` before calling the kit's existing target
53
+ build and smoke scripts.
54
+
55
+ ## Game Services
56
+
57
+ The backend client is disabled unless a backend URL is configured:
58
+
59
+ ```sh
60
+ VITE_MPGD_GAME_SERVICES_URL=http://localhost:5173
61
+ VITE_MPGD_GAME_SERVICES_TARGET=android
62
+ VITE_MPGD_GAME_SERVICES_TRANSPORT=http
63
+ ```
64
+
65
+ Use `VITE_MPGD_GAME_SERVICES_TRANSPORT=orpc` with a `/rpc` URL to test the oRPC
66
+ client path.
@@ -0,0 +1,16 @@
1
+ # Acceptance
2
+
3
+ Run these checks before handing off a generated game starter:
4
+
5
+ ```sh
6
+ pnpm check
7
+ pnpm build
8
+ pnpm --dir ../mpgd-kit mpgd target build-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait --ait-variant wrapper
9
+ pnpm --dir ../mpgd-kit mpgd target smoke-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait
10
+ ```
11
+
12
+ For Apps in Toss changes, use the apps-in-toss MCP before implementation and
13
+ keep SDK calls inside adapters or target wrappers.
14
+
15
+ For Reddit Devvit changes, keep Devvit SDK calls inside the target wrapper and
16
+ continue to expose game-facing behavior through `PlatformGateway`.
@@ -0,0 +1,20 @@
1
+ # __GAME_TITLE__ Brief
2
+
3
+ Build a Phaser 4 game on top of the mpgd platform boundary.
4
+
5
+ Boundaries:
6
+
7
+ - Phaser scenes render and collect input only.
8
+ - Target services go through `PlatformGateway`.
9
+ - Purchases, rewarded ad grants, and leaderboard records should go through
10
+ backend ledger APIs before mutating durable game save state.
11
+ - Platform SDK imports belong in adapters or target wrappers.
12
+
13
+ Useful commands:
14
+
15
+ ```sh
16
+ pnpm dev
17
+ pnpm check
18
+ pnpm build
19
+ pnpm --dir ../mpgd-kit mpgd target build-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait --ait-variant wrapper
20
+ ```
@@ -0,0 +1,41 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "game": {
4
+ "id": "__GAME_NAME__",
5
+ "title": "__GAME_TITLE__",
6
+ "engine": "phaser-4"
7
+ },
8
+ "targets": ["web-preview", "android", "ios", "ait", "reddit"],
9
+ "futureTargets": ["telegram", "tauri"],
10
+ "platformBoundary": "PlatformGateway",
11
+ "mcpRequirements": {
12
+ "ait": "Use apps-in-toss MCP before Apps in Toss SDK or review-flow changes.",
13
+ "reddit": "Use Devvit documentation/MCP before Devvit SDK or publish-flow changes."
14
+ },
15
+ "blocks": [
16
+ {
17
+ "id": "simulation.loop.phase",
18
+ "owner": "src/game/state.ts"
19
+ },
20
+ {
21
+ "id": "runtime.platform.gateway",
22
+ "owner": "src/platform/installPlatform.ts"
23
+ },
24
+ {
25
+ "id": "services.backend.game-services",
26
+ "owner": "src/platform/gameServices.ts"
27
+ },
28
+ {
29
+ "id": "localization.messages",
30
+ "owner": "src/i18n/messages.ts"
31
+ },
32
+ {
33
+ "id": "telemetry.analytics.best-effort",
34
+ "owner": "src/main.ts"
35
+ }
36
+ ],
37
+ "acceptance": [
38
+ "pnpm check",
39
+ "pnpm build"
40
+ ]
41
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>__GAME_TITLE__</title>
7
+ </head>
8
+ <body>
9
+ <div id="game"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,42 @@
1
+ {
2
+ "targets": {
3
+ "web-preview": {
4
+ "kind": "web",
5
+ "gameApp": ".",
6
+ "adapter": "browser",
7
+ "output": "artifacts/web-preview"
8
+ },
9
+ "android": {
10
+ "kind": "capacitor-android",
11
+ "gameApp": ".",
12
+ "shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
13
+ "adapter": "capacitor",
14
+ "webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
15
+ "artifact": "aab"
16
+ },
17
+ "ios": {
18
+ "kind": "capacitor-ios",
19
+ "gameApp": ".",
20
+ "shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
21
+ "adapter": "capacitor",
22
+ "webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
23
+ "artifact": "ipa"
24
+ },
25
+ "ait": {
26
+ "kind": "apps-in-toss",
27
+ "gameApp": ".",
28
+ "wrapperApp": "${MPGD_KIT_PATH}/apps/target-ait",
29
+ "adapter": "ait",
30
+ "webDir": "${MPGD_KIT_PATH}/apps/target-ait/public/game",
31
+ "artifact": ".ait"
32
+ },
33
+ "reddit": {
34
+ "kind": "devvit-web",
35
+ "gameApp": ".",
36
+ "wrapperApp": "${MPGD_KIT_PATH}/apps/target-devvit",
37
+ "adapter": "devvit",
38
+ "webDir": "${MPGD_KIT_PATH}/apps/target-devvit/dist/client",
39
+ "artifact": "devvit"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "description": "__GAME_TITLE__ mpgd Phaser game.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0",
9
+ "build": "__WORKSPACE_I18N_BUILD_PREFIX__vite build",
10
+ "check": "ttsc --noEmit -p tsconfig.json",
11
+ "lint": "ttsc --noEmit -p tsconfig.json",
12
+ "format": "ttsc format",
13
+ "fix": "ttsc fix"
14
+ },
15
+ "dependencies": {
16
+ "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION__",
17
+ "@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION__",
18
+ "@mpgd/adapter-capacitor": "__MPGD_DEPENDENCY_VERSION__",
19
+ "@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION__",
20
+ "@mpgd/analytics": "__MPGD_DEPENDENCY_VERSION__",
21
+ "@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION__",
22
+ "@mpgd/game-services": "__MPGD_DEPENDENCY_VERSION__",
23
+ "@mpgd/i18n": "__MPGD_DEPENDENCY_VERSION__",
24
+ "@mpgd/platform": "__MPGD_DEPENDENCY_VERSION__",
25
+ "@mpgd/target-config": "__MPGD_DEPENDENCY_VERSION__",
26
+ "phaser": "4.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "@ttsc/unplugin": "0.16.9",
30
+ "@types/node": "^24.0.0",
31
+ "ttsc": "0.16.9",
32
+ "typescript": "7.0.1-rc",
33
+ "vite": "^8.1.3"
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare const __APP_TARGET__: string;
4
+ declare const __APP_VERSION__: string;
5
+ declare const __BUILD_ID__: string;
6
+ declare const __DEBUG_BUILD__: boolean;
7
+
8
+ interface ImportMetaEnv {
9
+ readonly VITE_MPGD_GAME_SERVICES_URL?: string;
10
+ readonly VITE_MPGD_GAME_SERVICES_TARGET?: string;
11
+ readonly VITE_MPGD_GAME_SERVICES_TRANSPORT?: string;
12
+ }
@@ -0,0 +1,29 @@
1
+ const scoreIntervalMs = 100;
2
+ const phasePeriodMs = 2400;
3
+
4
+ export interface StarterRunState {
5
+ readonly elapsedMs: number;
6
+ readonly score: number;
7
+ readonly phase: number;
8
+ }
9
+
10
+ export function createStarterRunState(): StarterRunState {
11
+ return {
12
+ elapsedMs: 0,
13
+ score: 0,
14
+ phase: 0,
15
+ };
16
+ }
17
+
18
+ export function stepStarterRunState(
19
+ state: StarterRunState,
20
+ deltaMs: number,
21
+ ): StarterRunState {
22
+ const elapsedMs = state.elapsedMs + deltaMs;
23
+
24
+ return {
25
+ elapsedMs,
26
+ score: Math.floor(elapsedMs / scoreIntervalMs),
27
+ phase: (elapsedMs % phasePeriodMs) / phasePeriodMs,
28
+ };
29
+ }
@@ -0,0 +1,64 @@
1
+ import type { Locale } from '@mpgd/i18n';
2
+
3
+ export type GameMessageKey =
4
+ | 'title'
5
+ | 'target'
6
+ | 'backend'
7
+ | 'features'
8
+ | 'featuresNone'
9
+ | 'tapToStart'
10
+ | 'score'
11
+ | 'reward'
12
+ | 'rewardError'
13
+ | 'rewardPending'
14
+ | 'rewardUnavailable'
15
+ | 'analytics'
16
+ | 'bootError';
17
+
18
+ const messages = {
19
+ en: {
20
+ title: '__GAME_TITLE__',
21
+ target: 'Target: {target}',
22
+ backend: 'Game Services: {mode}',
23
+ features: 'Features: {features}',
24
+ featuresNone: 'none',
25
+ tapToStart: 'Tap or press Enter to start',
26
+ score: 'Score {score}',
27
+ reward: 'Rewarded ad smoke: {status}',
28
+ rewardError: 'Rewarded ad smoke failed',
29
+ rewardPending: 'Press R to request a rewarded ad smoke',
30
+ rewardUnavailable: 'Rewarded ads unavailable on this target',
31
+ analytics: 'Analytics events: {count}',
32
+ bootError: 'Failed to start game',
33
+ },
34
+ ko: {
35
+ title: '__GAME_TITLE__',
36
+ target: '타깃: {target}',
37
+ backend: 'Game Services: {mode}',
38
+ features: '기능: {features}',
39
+ featuresNone: '없음',
40
+ tapToStart: '탭하거나 Enter를 눌러 시작',
41
+ score: '점수 {score}',
42
+ reward: '리워드 광고 스모크: {status}',
43
+ rewardError: '리워드 광고 스모크 실패',
44
+ rewardPending: 'R 키로 리워드 광고 스모크 요청',
45
+ rewardUnavailable: '이 타깃에서는 리워드 광고를 사용할 수 없음',
46
+ analytics: '애널리틱스 이벤트: {count}',
47
+ bootError: '게임 시작 실패',
48
+ },
49
+ } satisfies Record<Locale, Record<GameMessageKey, string>>;
50
+
51
+ export function t(
52
+ locale: Locale,
53
+ key: GameMessageKey,
54
+ values: Record<string, string | number> = {},
55
+ ): string {
56
+ const language = locale === 'ko' ? 'ko' : 'en';
57
+ let text: string = messages[language][key] ?? messages.en[key];
58
+
59
+ for (const [name, value] of Object.entries(values)) {
60
+ text = text.replaceAll(`{${name}}`, String(value));
61
+ }
62
+
63
+ return text;
64
+ }
@@ -0,0 +1,78 @@
1
+ import './styles.css';
2
+
3
+ import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
4
+ import { resolveMpgdLocale, type Locale } from '@mpgd/i18n';
5
+
6
+ import { t } from './i18n/messages';
7
+ import { createClientId } from './runtime/id';
8
+ import { createStarterGame } from './runtime/createGame';
9
+ import { detectRuntime } from './platform/runtimeDetector';
10
+ import { createStarterGameServices } from './platform/gameServices';
11
+ import { installPlatform } from './platform/installPlatform';
12
+
13
+ await bootstrap();
14
+
15
+ async function bootstrap(): Promise<void> {
16
+ let locale: Locale = 'en';
17
+
18
+ try {
19
+ const runtimeConfig = detectRuntime();
20
+ const platform = await installPlatform(runtimeConfig);
21
+ const runtime = await platform.getTargetRuntime();
22
+ const player =
23
+ (await platform.identity.getPlayer()) ?? {
24
+ playerId: 'local-player',
25
+ displayName: 'Local Player',
26
+ };
27
+ locale = resolveMpgdLocale(runtime.capabilities);
28
+ const analyticsSink = createBufferedAnalyticsSink();
29
+ const analytics = createAnalyticsReporter({
30
+ target: platform.target,
31
+ sessionId: createClientId('session'),
32
+ sink: analyticsSink,
33
+ });
34
+ const gameServices = createStarterGameServices({
35
+ gateway: platform,
36
+ playerId: player.playerId,
37
+ });
38
+
39
+ await analytics.track({
40
+ name: 'game_started',
41
+ properties: {
42
+ target: platform.target,
43
+ configTarget: runtime.configTarget,
44
+ },
45
+ });
46
+
47
+ createStarterGame({
48
+ mountId: 'game',
49
+ context: {
50
+ platform,
51
+ runtime,
52
+ player,
53
+ locale,
54
+ gameServices,
55
+ analytics,
56
+ analyticsSink,
57
+ },
58
+ });
59
+ } catch (error) {
60
+ renderBootstrapError(error, locale);
61
+ console.error('[bootstrap]', error);
62
+ }
63
+ }
64
+
65
+ function renderBootstrapError(error: unknown, locale: Locale): void {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ const root = document.querySelector<HTMLDivElement>('#game');
68
+
69
+ if (root === null) {
70
+ return;
71
+ }
72
+
73
+ root.replaceChildren();
74
+ const panel = document.createElement('div');
75
+ panel.className = 'boot-error';
76
+ panel.textContent = `${t(locale, 'bootError')}: ${message}`;
77
+ root.append(panel);
78
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ createGameServicesClient,
3
+ createGameServicesFetchBackendTransport,
4
+ createGameServicesHttpBackendApi,
5
+ createGameServicesOrpcBackendApi,
6
+ createGameServicesOrpcClient,
7
+ type GameServicesClient,
8
+ type GameServicesStoreTarget,
9
+ } from '@mpgd/game-services';
10
+ import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
11
+
12
+ export type StarterBackendMode = 'disabled' | 'http' | 'orpc';
13
+
14
+ export interface StarterGameServices {
15
+ readonly mode: StarterBackendMode;
16
+ readonly baseUrl?: string;
17
+ readonly target?: GameServicesStoreTarget;
18
+ readonly client?: GameServicesClient;
19
+ }
20
+
21
+ export function createStarterGameServices(input: {
22
+ readonly gateway: PlatformGateway;
23
+ readonly playerId: string;
24
+ }): StarterGameServices {
25
+ const baseUrl = import.meta.env.VITE_MPGD_GAME_SERVICES_URL;
26
+ const requestedMode = import.meta.env.VITE_MPGD_GAME_SERVICES_TRANSPORT;
27
+ const storeTarget = readStoreTarget(
28
+ import.meta.env.VITE_MPGD_GAME_SERVICES_TARGET ?? input.gateway.target,
29
+ );
30
+
31
+ if (baseUrl === undefined || baseUrl.length === 0 || storeTarget === null) {
32
+ return {
33
+ mode: 'disabled',
34
+ };
35
+ }
36
+
37
+ const mode = requestedMode === 'orpc' ? 'orpc' : 'http';
38
+ const backend =
39
+ mode === 'orpc'
40
+ ? createGameServicesOrpcBackendApi(
41
+ createGameServicesOrpcClient({
42
+ url: baseUrl,
43
+ }),
44
+ )
45
+ : createGameServicesHttpBackendApi({
46
+ transport: createGameServicesFetchBackendTransport({
47
+ baseUrl,
48
+ }),
49
+ });
50
+
51
+ return {
52
+ mode,
53
+ baseUrl,
54
+ target: storeTarget,
55
+ client: createGameServicesClient({
56
+ gateway: input.gateway,
57
+ backend,
58
+ playerId: input.playerId,
59
+ target: storeTarget,
60
+ }),
61
+ };
62
+ }
63
+
64
+ function readStoreTarget(target: PlatformTarget | string): GameServicesStoreTarget | null {
65
+ if (target === 'android' || target === 'ios' || target === 'ait') {
66
+ return target;
67
+ }
68
+
69
+ return null;
70
+ }
@@ -0,0 +1,96 @@
1
+ import type { AdPlacements, ProductCatalog } from '@mpgd/catalog';
2
+ import adPlacementsJson from '@mpgd/catalog/placements.json';
3
+ import productCatalogJson from '@mpgd/catalog/catalog.json';
4
+ import type { PlatformGateway } from '@mpgd/platform';
5
+ import {
6
+ createEffectiveTargetConfig,
7
+ getTargetConfig,
8
+ targetConfigKeyForPlatform,
9
+ withTargetAvailability,
10
+ type TargetConfigMatrix,
11
+ type TargetConfiguredGateway,
12
+ } from '@mpgd/target-config';
13
+ import targetConfigMatrixJson from '@mpgd/target-config/targets.json';
14
+
15
+ import type { RuntimeConfig } from './runtimeDetector';
16
+
17
+ const devvitSandboxBuildId = 'devvit-sandbox';
18
+ const targetConfigMatrix = targetConfigMatrixJson as TargetConfigMatrix;
19
+ const productCatalog = productCatalogJson as ProductCatalog;
20
+ const adPlacements = adPlacementsJson as AdPlacements;
21
+ const targetAdPlacements = adPlacements.placements.map((placement) => ({
22
+ id: placement.id,
23
+ type: placement.type,
24
+ }));
25
+ const adPlacementTypes = new Map<string, 'rewarded' | 'interstitial'>(
26
+ targetAdPlacements.map((placement) => [placement.id, placement.type]),
27
+ );
28
+
29
+ export async function installPlatform(runtime: RuntimeConfig): Promise<TargetConfiguredGateway> {
30
+ let gateway: PlatformGateway;
31
+
32
+ switch (runtime.target) {
33
+ case 'android':
34
+ case 'ios': {
35
+ const { createCapacitorPlatformGateway } = await import('@mpgd/adapter-capacitor');
36
+ gateway = createCapacitorPlatformGateway({
37
+ target: runtime.target,
38
+ appVersion: runtime.appVersion,
39
+ buildId: runtime.buildId,
40
+ });
41
+ break;
42
+ }
43
+
44
+ case 'ait': {
45
+ const { createAitPlatformGateway, createAitSandboxBridge } = await import(
46
+ '@mpgd/adapter-ait'
47
+ );
48
+ gateway = createAitPlatformGateway({
49
+ appVersion: runtime.appVersion,
50
+ buildId: runtime.buildId,
51
+ ...(runtime.debug ? { fallbackBridge: createAitSandboxBridge() } : {}),
52
+ });
53
+ break;
54
+ }
55
+
56
+ case 'reddit': {
57
+ const { createDevvitPlatformGateway, createDevvitSandboxBridge } = await import(
58
+ '@mpgd/adapter-devvit'
59
+ );
60
+ gateway = createDevvitPlatformGateway({
61
+ appVersion: runtime.appVersion,
62
+ buildId: runtime.buildId,
63
+ ...(shouldUseDevvitSandbox(runtime) ? { fallbackBridge: createDevvitSandboxBridge() } : {}),
64
+ });
65
+ break;
66
+ }
67
+
68
+ default: {
69
+ const { createBrowserPlatformGateway } = await import('@mpgd/adapter-browser');
70
+ gateway = createBrowserPlatformGateway();
71
+ }
72
+ }
73
+
74
+ const configTarget = targetConfigKeyForPlatform(runtime.target);
75
+ const targetConfig = getTargetConfig(targetConfigMatrix, configTarget);
76
+ const effectiveConfig = createEffectiveTargetConfig({
77
+ target: configTarget,
78
+ targetConfigVersion: targetConfigMatrix.version,
79
+ config: targetConfig,
80
+ catalog: productCatalog,
81
+ adPlacements,
82
+ });
83
+
84
+ return withTargetAvailability(gateway, targetConfig, {
85
+ configTarget,
86
+ effectiveConfig,
87
+ adPlacements: targetAdPlacements,
88
+ resolveAdPlacementType(placementId) {
89
+ return adPlacementTypes.get(placementId);
90
+ },
91
+ });
92
+ }
93
+
94
+ function shouldUseDevvitSandbox(runtime: RuntimeConfig): boolean {
95
+ return runtime.debug && runtime.buildId === devvitSandboxBuildId;
96
+ }
@@ -0,0 +1,30 @@
1
+ import type { PlatformTarget } from '@mpgd/platform';
2
+
3
+ const validTargets = new Set<string>([
4
+ 'android',
5
+ 'ios',
6
+ 'ait',
7
+ 'reddit',
8
+ 'telegram',
9
+ 'tauri',
10
+ ] satisfies readonly PlatformTarget[]);
11
+
12
+ export interface RuntimeConfig {
13
+ readonly target: PlatformTarget;
14
+ readonly appVersion: string;
15
+ readonly buildId: string;
16
+ readonly debug: boolean;
17
+ }
18
+
19
+ export function detectRuntime(): RuntimeConfig {
20
+ return {
21
+ target: normalizeTarget(__APP_TARGET__),
22
+ appVersion: __APP_VERSION__,
23
+ buildId: __BUILD_ID__,
24
+ debug: __DEBUG_BUILD__,
25
+ };
26
+ }
27
+
28
+ export function normalizeTarget(value: string): PlatformTarget {
29
+ return validTargets.has(value) ? (value as PlatformTarget) : 'browser';
30
+ }