@mpgd/cli 0.11.0 → 0.12.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.
package/dist/index.js CHANGED
@@ -18,13 +18,18 @@ const packageRoot = resolvePackageRoot(sourceDir);
18
18
  const detectedKitRoot = resolveDefaultKitRoot();
19
19
  const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
20
20
  const cliVersion = readPackageVersion(packageRoot);
21
- const recommendedMatrixTargets = 'web,microsoft-store,ait,reddit';
21
+ const recommendedMatrixTargets = 'web,microsoft-store,verse8,ait,reddit';
22
22
  const defaultDependencyVersion = `^${cliVersion}`;
23
+ const standaloneTemplateDependencyVersionFallbacks = {
24
+ // Initial-published independently from the CLI release line; package metadata wins once concrete.
25
+ '@mpgd/adapter-verse8': '^0.1.0',
26
+ };
23
27
  const mpgdTemplateDependencyPackages = [
24
28
  { name: '@mpgd/adapter-ait', packageDir: 'adapters/ait' },
25
29
  { name: '@mpgd/adapter-browser', packageDir: 'adapters/browser' },
26
30
  { name: '@mpgd/adapter-capacitor', packageDir: 'adapters/capacitor' },
27
31
  { name: '@mpgd/adapter-devvit', packageDir: 'adapters/devvit' },
32
+ { name: '@mpgd/adapter-verse8', packageDir: 'adapters/verse8' },
28
33
  { name: '@mpgd/analytics', packageDir: 'packages/analytics' },
29
34
  { name: '@mpgd/bridge', packageDir: 'packages/bridge' },
30
35
  { name: '@mpgd/catalog', packageDir: 'packages/catalog' },
@@ -35,10 +40,17 @@ const mpgdTemplateDependencyPackages = [
35
40
  { name: '@mpgd/platform', packageDir: 'packages/platform' },
36
41
  { name: '@mpgd/target-config', packageDir: 'packages/target-config' },
37
42
  ];
38
- const recommendedMatrixTargetOrder = ['web-preview', 'microsoft-store', 'ait', 'reddit'];
43
+ const recommendedMatrixTargetOrder = [
44
+ 'web-preview',
45
+ 'microsoft-store',
46
+ 'verse8',
47
+ 'ait',
48
+ 'reddit',
49
+ ];
39
50
  const allMatrixTargetOrder = [
40
51
  'web-preview',
41
52
  'microsoft-store',
53
+ 'verse8',
42
54
  'android',
43
55
  'ios',
44
56
  'ait',
@@ -50,6 +62,7 @@ const supportedBuildTargets = [
50
62
  'web-preview',
51
63
  'microsoft-store',
52
64
  'msstore',
65
+ 'verse8',
53
66
  'android',
54
67
  'ios',
55
68
  'ait',
@@ -1256,6 +1269,7 @@ function resolveMpgdDependencyVersionReplacements(input) {
1256
1269
  ?? (input.workspace ? 'workspace:*' : undefined)
1257
1270
  ?? resolvePublishedTemplateDependencyVersion(dependency.name, packageJson)
1258
1271
  ?? resolveWorkspaceTemplateDependencyVersion(kitRoot, dependency.packageDir)
1272
+ ?? standaloneTemplateDependencyVersionFallbacks[dependency.name]
1259
1273
  ?? defaultDependencyVersion;
1260
1274
  return {
1261
1275
  placeholder: mpgdDependencyVersionPlaceholder(dependency.name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,18 +49,19 @@
49
49
  "@types/node": "^24.0.0",
50
50
  "ttsc": "0.18.4",
51
51
  "typescript": "7.0.2",
52
- "@mpgd/adapter-ait": "0.4.3",
53
- "@mpgd/adapter-capacitor": "0.4.3",
54
- "@mpgd/adapter-devvit": "0.8.2",
55
- "@mpgd/adapter-browser": "0.4.3",
56
- "@mpgd/analytics": "0.3.6",
57
- "@mpgd/bridge": "0.6.0",
58
- "@mpgd/i18n": "0.5.2",
59
- "@mpgd/catalog": "0.4.0",
60
- "@mpgd/target-config": "0.7.0",
61
- "@mpgd/game-services": "0.8.2",
62
- "@mpgd/phaser-assets": "0.4.1",
63
- "@mpgd/platform": "0.6.0"
52
+ "@mpgd/adapter-browser": "0.5.0",
53
+ "@mpgd/adapter-capacitor": "0.4.4",
54
+ "@mpgd/adapter-devvit": "0.8.3",
55
+ "@mpgd/adapter-verse8": "0.2.0",
56
+ "@mpgd/adapter-ait": "0.4.4",
57
+ "@mpgd/bridge": "0.6.1",
58
+ "@mpgd/catalog": "0.5.0",
59
+ "@mpgd/analytics": "0.3.7",
60
+ "@mpgd/game-services": "0.9.0",
61
+ "@mpgd/i18n": "0.5.3",
62
+ "@mpgd/target-config": "0.8.0",
63
+ "@mpgd/platform": "0.7.0",
64
+ "@mpgd/phaser-assets": "0.4.1"
64
65
  },
65
66
  "main": "./dist/index.js",
66
67
  "types": "./dist/index.d.ts",
@@ -31,17 +31,17 @@ platform-specific artifacts. Keep an `mpgd-kit` checkout nearby and pass it with
31
31
  pnpm exec mpgd target build-all \
32
32
  --targets-file ./mpgd.targets.json \
33
33
  --kit-path ../mpgd-kit \
34
- --targets web,microsoft-store,ait,reddit \
34
+ --targets web,microsoft-store,verse8,ait,reddit \
35
35
  --profile staging \
36
36
  --ait-variant wrapper
37
37
  pnpm exec mpgd target smoke-all \
38
38
  --targets-file ./mpgd.targets.json \
39
39
  --kit-path ../mpgd-kit \
40
- --targets web,microsoft-store,ait,reddit
40
+ --targets web,microsoft-store,verse8,ait,reddit
41
41
  pnpm exec mpgd target doctor \
42
42
  --targets-file ./mpgd.targets.json \
43
43
  --kit-path ../mpgd-kit \
44
- --targets web,microsoft-store,ait,reddit
44
+ --targets web,microsoft-store,verse8,ait,reddit
45
45
  ```
46
46
 
47
47
  When developing the kit itself, the same commands work through the repository
@@ -51,7 +51,7 @@ script:
51
51
  pnpm --dir ../mpgd-kit mpgd target build-all \
52
52
  --targets-file "$PWD/mpgd.targets.json" \
53
53
  --kit-path ../mpgd-kit \
54
- --targets web,microsoft-store,ait,reddit \
54
+ --targets web,microsoft-store,verse8,ait,reddit \
55
55
  --profile staging \
56
56
  --ait-variant wrapper
57
57
  ```
@@ -285,7 +285,26 @@ submission.
285
285
  app; do not force activation with `skipWaiting()`. Keep the web manifest `id`
286
286
  game-specific because the cache namespace uses it to isolate apps sharing an
287
287
  origin.
288
- - Reddit Devvit is game-owned in `apps/target-devvit`.
288
+ - Verse8 is a game-owned iframe web target that writes to `artifacts/verse8`.
289
+ The adapter verifies the host-provided auth credential and distinguishes
290
+ Verse8-signed accounts from self-signed guest accounts. Ads are available
291
+ only when placement mappings are present. VXShop stays capability-disabled
292
+ until the gateway receives the literal `purchaseEventAuthority:
293
+ 'agent8-server'`, catalog product mappings, and an authenticated Agent8
294
+ entitlement reader. The client purchase result is always `pending`; grants
295
+ belong only in the reserved Agent8 `$onItemPurchased` event. Do not use
296
+ `VXShop.onClose`, client metadata, or a client-created transaction ID as
297
+ purchase evidence. Keep `@agent8/gameserver-node` in the separate Agent8
298
+ server project and use `@mpgd/adapter-verse8/agent8` there for the locked,
299
+ consume-once catalog grant. Authenticated-encrypted Agent8 cloud state is
300
+ available only after the gateway receives a game-owned remote storage client;
301
+ the starter does not install the React browser SDK, configure that client, or
302
+ ship server persistence secrets. Agent8 verified
303
+ leaderboards stay server-only and require a game-specific completion
304
+ verifier, so the generic platform leaderboard capability remains disabled.
305
+ - Reddit Devvit is game-owned in `apps/target-devvit`. Its Redis-backed storage
306
+ bridge fails closed on identity, provider, serialization, and quota errors;
307
+ it never switches progress to browser `localStorage`.
289
308
  - Apps in Toss currently uses the kit reference wrapper at
290
309
  `${MPGD_KIT_PATH}/apps/target-ait` for smoke packaging. Before a real Toss
291
310
  submission, create a game-owned wrapper/config so the app name, CLI state, and
@@ -36,5 +36,19 @@ For Microsoft Store changes, keep the first pass as a PWA/web target that uses
36
36
  the browser adapter. Add a dedicated Store commerce adapter only when wiring
37
37
  Digital Goods API and Payment Request through backend ledger verification.
38
38
 
39
+ ## Verse8 Agent8 Structured Server
40
+
41
+ Keep the Agent8 server in a separate game-owned project. Its acceptance flow
42
+ must call `createVerse8Agent8LeaderboardBoundary` with an authenticated sender
43
+ account and a game-specific completion verifier, then cover rejected malformed
44
+ submissions, verified recording, and account-scoped snapshot reads.
45
+
46
+ Inject endpoints, persistence secrets, encryption keys, and authentication at
47
+ runtime. Do not commit personal MCP configuration, `.env` files, credentials,
48
+ tokens, or Agent8 authentication state to this generated game. The kit command
49
+ `pnpm smoke:verse8-agent8-acceptance` validates the generic structured-server
50
+ contract and generated Verse8 target surface; it does not validate a deployed
51
+ game server or replace game-owned production evidence.
52
+
39
53
  Verify the first screen reports the viewport orientation policy, and treat
40
54
  locked orientation modes as soft prompts instead of unsafe WebView hard locks.
@@ -20,7 +20,7 @@ pnpm check
20
20
  pnpm build
21
21
  pnpm icons:generate
22
22
  pnpm icons:verify
23
- pnpm exec mpgd target build-all --targets-file ./mpgd.targets.json --kit-path ../mpgd-kit --targets web,microsoft-store,ait,reddit --profile staging --ait-variant wrapper
23
+ pnpm exec mpgd target build-all --targets-file ./mpgd.targets.json --kit-path ../mpgd-kit --targets web,microsoft-store,verse8,ait,reddit --profile staging --ait-variant wrapper
24
24
  pnpm devvit:login
25
25
  pnpm devvit:init
26
26
  pnpm devvit:playtest
@@ -5,7 +5,7 @@
5
5
  "title": "__GAME_TITLE__",
6
6
  "engine": "phaser-4"
7
7
  },
8
- "targets": ["web-preview", "microsoft-store", "android", "ios", "ait", "reddit"],
8
+ "targets": ["web-preview", "microsoft-store", "verse8", "android", "ios", "ait", "reddit"],
9
9
  "futureTargets": ["telegram", "tauri"],
10
10
  "platformBoundary": "PlatformGateway",
11
11
  "mcpRequirements": {
@@ -3,9 +3,11 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
3
3
  import { createServer, context, getServerPort, reddit, redis } from '@devvit/web/server';
4
4
  import type { UiResponse } from '@devvit/web/shared';
5
5
  import {
6
+ bridgeStorageLoadProtocol,
6
7
  createBridgeError,
7
8
  type BridgeRequest,
8
9
  type BridgeResponse,
10
+ type BridgeStorageLoadData,
9
11
  } from '@mpgd/bridge';
10
12
  import {
11
13
  createBridgeRpcRouter,
@@ -15,6 +17,7 @@ import { createBridgeRpcNodeHandler } from '@mpgd/bridge/orpc/node';
15
17
 
16
18
  const maxStorageKeyLength = 128;
17
19
  const maxEncodedStorageKeyLength = 384;
20
+ const maxStorageValueBytes = 262_144;
18
21
  const maxRequestBodySize = 1_048_576;
19
22
  const gameName = '__GAME_NAME__';
20
23
  const gameTitle = __GAME_TITLE_TS_LITERAL__;
@@ -263,7 +266,11 @@ async function loadStorage(input: BridgeRequest): Promise<BridgeResponse> {
263
266
  const playerId = currentPlayerId();
264
267
 
265
268
  if (playerId === undefined) {
266
- return ok(input, null);
269
+ return createBridgeError(
270
+ input.id,
271
+ 'DEVVIT_STORAGE_IDENTITY_REQUIRED',
272
+ 'A current Reddit player is required to load storage.',
273
+ );
267
274
  }
268
275
 
269
276
  const key = storageKey(input, playerId);
@@ -272,14 +279,36 @@ async function loadStorage(input: BridgeRequest): Promise<BridgeResponse> {
272
279
  return key;
273
280
  }
274
281
 
275
- const stored = await redis.get(key);
282
+ let stored: string | null | undefined;
283
+
284
+ try {
285
+ stored = await redis.get(key);
286
+ } catch (error) {
287
+ console.warn(`devvit storage load failed: ${errorMessage(error)}`);
288
+ return createBridgeError(
289
+ input.id,
290
+ 'DEVVIT_STORAGE_LOAD_FAILED',
291
+ 'Devvit storage could not be loaded.',
292
+ true,
293
+ );
294
+ }
276
295
 
277
296
  if (stored === undefined || stored === null) {
278
- return ok(input, null);
297
+ return ok(input, {
298
+ __mpgdBridgeProtocol: bridgeStorageLoadProtocol,
299
+ found: false,
300
+ } satisfies BridgeStorageLoadData);
279
301
  }
280
302
 
281
303
  try {
282
- return ok(input, JSON.parse(stored));
304
+ return ok(
305
+ input,
306
+ {
307
+ __mpgdBridgeProtocol: bridgeStorageLoadProtocol,
308
+ found: true,
309
+ value: JSON.parse(stored),
310
+ } satisfies BridgeStorageLoadData,
311
+ );
283
312
  } catch {
284
313
  return createBridgeError(input.id, 'CORRUPTED_STORAGE_VALUE', 'Stored data is not valid JSON.');
285
314
  }
@@ -289,9 +318,11 @@ async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
289
318
  const playerId = currentPlayerId();
290
319
 
291
320
  if (playerId === undefined) {
292
- return ok(input, {
293
- saved: false,
294
- });
321
+ return createBridgeError(
322
+ input.id,
323
+ 'DEVVIT_STORAGE_IDENTITY_REQUIRED',
324
+ 'A current Reddit player is required to save storage.',
325
+ );
295
326
  }
296
327
 
297
328
  const key = storageKey(input, playerId);
@@ -301,16 +332,42 @@ async function saveStorage(input: BridgeRequest): Promise<BridgeResponse> {
301
332
  }
302
333
 
303
334
  const payload = optionalObjectPayload(input.payload) as { readonly value?: unknown };
335
+ let serialized: string;
304
336
 
305
337
  try {
306
- await redis.set(key, JSON.stringify(payload.value ?? null));
307
- } catch (error) {
308
- console.warn(`devvit storage save was not persisted for key ${key}: ${errorMessage(error)}`);
338
+ const candidate = JSON.stringify(payload.value);
309
339
 
310
- return ok(input, {
311
- saved: false,
312
- playerId,
313
- });
340
+ if (typeof candidate !== 'string') {
341
+ throw new Error('JSON serialization did not produce a string.');
342
+ }
343
+
344
+ serialized = candidate;
345
+ } catch {
346
+ return createBridgeError(
347
+ input.id,
348
+ 'INVALID_STORAGE_VALUE',
349
+ 'Storage values must be JSON serializable.',
350
+ );
351
+ }
352
+
353
+ if (new TextEncoder().encode(serialized).length > maxStorageValueBytes) {
354
+ return createBridgeError(
355
+ input.id,
356
+ 'DEVVIT_STORAGE_QUOTA_EXCEEDED',
357
+ `Storage values must not exceed ${String(maxStorageValueBytes)} UTF-8 bytes.`,
358
+ );
359
+ }
360
+
361
+ try {
362
+ await redis.set(key, serialized);
363
+ } catch (error) {
364
+ console.warn(`devvit storage save was not persisted: ${errorMessage(error)}`);
365
+ return createBridgeError(
366
+ input.id,
367
+ 'DEVVIT_STORAGE_SAVE_FAILED',
368
+ 'Devvit storage could not be saved.',
369
+ true,
370
+ );
314
371
  }
315
372
 
316
373
  return ok(input, {
@@ -14,6 +14,13 @@
14
14
  "icon": { "profile": "microsoft-pwa" },
15
15
  "output": "artifacts/microsoft-store"
16
16
  },
17
+ "verse8": {
18
+ "kind": "web",
19
+ "gameApp": ".",
20
+ "adapter": "verse8",
21
+ "icon": { "profile": "web-preview" },
22
+ "output": "artifacts/verse8"
23
+ },
17
24
  "android": {
18
25
  "kind": "capacitor-android",
19
26
  "gameApp": ".",
@@ -30,6 +30,8 @@
30
30
  "ait:console:deploy": "aitcc app deploy",
31
31
  "build:microsoft-store": "pnpm exec mpgd target build microsoft-store production --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
32
32
  "smoke:microsoft-store": "pnpm exec mpgd target smoke microsoft-store --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
33
+ "build:verse8": "pnpm exec mpgd target build verse8 production --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
34
+ "smoke:verse8": "pnpm exec mpgd target smoke verse8 --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
33
35
  "build:devvit": "pnpm exec mpgd target build reddit production --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
34
36
  "build:devvit:staging": "pnpm exec mpgd target build reddit staging --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
35
37
  "smoke:devvit": "pnpm exec mpgd target smoke reddit --targets-file ./mpgd.targets.json --kit-path ${MPGD_KIT_PATH:-__DEFAULT_KIT_PATH__}",
@@ -59,6 +61,7 @@
59
61
  "@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION_ADAPTER_BROWSER__",
60
62
  "@mpgd/adapter-capacitor": "__MPGD_DEPENDENCY_VERSION_ADAPTER_CAPACITOR__",
61
63
  "@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION_ADAPTER_DEVVIT__",
64
+ "@mpgd/adapter-verse8": "__MPGD_DEPENDENCY_VERSION_ADAPTER_VERSE8__",
62
65
  "@mpgd/analytics": "__MPGD_DEPENDENCY_VERSION_ANALYTICS__",
63
66
  "@mpgd/bridge": "__MPGD_DEPENDENCY_VERSION_BRIDGE__",
64
67
  "@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION_CATALOG__",
@@ -0,0 +1,25 @@
1
+ import { createVerse8PlatformGateway } from '@mpgd/adapter-verse8';
2
+ import type { AdPlacements } from '@mpgd/catalog';
3
+ import adPlacementsJson from '@mpgd/catalog/placements.json';
4
+ import type { PlatformGateway } from '@mpgd/platform';
5
+
6
+ import type { RuntimeConfig } from '../runtimeDetector';
7
+
8
+ export async function createBuildGateway(_runtime: RuntimeConfig): Promise<PlatformGateway> {
9
+ const adPlacements = adPlacementsJson as AdPlacements;
10
+ const placementIds = new Map(
11
+ adPlacements.placements.flatMap((placement) => {
12
+ const platformPlacementId = placement.platformPlacementIds.verse8;
13
+
14
+ return platformPlacementId === undefined
15
+ ? []
16
+ : [[placement.id, platformPlacementId] as const];
17
+ }),
18
+ );
19
+
20
+ return createVerse8PlatformGateway({
21
+ resolveAdPlacementId(placementId) {
22
+ return placementIds.get(placementId);
23
+ },
24
+ });
25
+ }
@@ -5,6 +5,7 @@ const validTargets = new Set<string>([
5
5
  'ios',
6
6
  'ait',
7
7
  'reddit',
8
+ 'verse8',
8
9
  'telegram',
9
10
  'tauri',
10
11
  ] satisfies readonly PlatformTarget[]);
@@ -88,6 +88,8 @@ export function resolveBuildGatewayModule(input: {
88
88
  return input.debug && input.buildId === devvitSandboxBuildId
89
89
  ? 'src/platform/buildGateways/redditSandbox.ts'
90
90
  : 'src/platform/buildGateways/reddit.ts';
91
+ case 'verse8':
92
+ return 'src/platform/buildGateways/verse8.ts';
91
93
  default:
92
94
  return 'src/platform/buildGateways/browser.ts';
93
95
  }