@mpgd/cli 0.5.0 → 0.7.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 (44) hide show
  1. package/dist/game-acceptance.d.ts +65 -0
  2. package/dist/game-acceptance.js +245 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +469 -2
  5. package/dist/production-target-readiness.d.ts +8 -0
  6. package/dist/production-target-readiness.js +207 -0
  7. package/package.json +12 -12
  8. package/templates/phaser-game/README.md +71 -1
  9. package/templates/phaser-game/agent/acceptance.md +14 -6
  10. package/templates/phaser-game/agent/brief.md +3 -1
  11. package/templates/phaser-game/agent/game-manifest.json +11 -0
  12. package/templates/phaser-game/apps/target-devvit/README.md +30 -0
  13. package/templates/phaser-game/apps/target-devvit/devvit.json +7 -0
  14. package/templates/phaser-game/apps/target-devvit/package.json +8 -5
  15. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +46 -1
  16. package/templates/phaser-game/apps/target-devvit/src/server/postOperationStore.ts +9 -0
  17. package/templates/phaser-game/apps/target-devvit/vite.server.config.ts +2 -1
  18. package/templates/phaser-game/game.html +13 -0
  19. package/templates/phaser-game/gitignore +2 -0
  20. package/templates/phaser-game/index.html +2 -2
  21. package/templates/phaser-game/mpgd.game.json +8 -0
  22. package/templates/phaser-game/mpgd.targets.json +6 -0
  23. package/templates/phaser-game/package.json +7 -1
  24. package/templates/phaser-game/public/manifest.webmanifest +1 -0
  25. package/templates/phaser-game/src/entry.ts +7 -0
  26. package/templates/phaser-game/src/env.d.ts +4 -0
  27. package/templates/phaser-game/src/gameEntry.ts +3 -0
  28. package/templates/phaser-game/src/main.ts +48 -3
  29. package/templates/phaser-game/src/platform/buildGatewayModule.ts +5 -0
  30. package/templates/phaser-game/src/platform/buildGateways/ait.ts +11 -0
  31. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +12 -0
  32. package/templates/phaser-game/src/platform/buildGateways/browser.ts +8 -0
  33. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +12 -0
  34. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +12 -0
  35. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +11 -0
  36. package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +15 -0
  37. package/templates/phaser-game/src/platform/devvitEntrypoint.ts +57 -0
  38. package/templates/phaser-game/src/platform/devvitInlinePreview.css +75 -0
  39. package/templates/phaser-game/src/platform/gameServices.ts +18 -61
  40. package/templates/phaser-game/src/platform/installPlatform.ts +6 -50
  41. package/templates/phaser-game/src/platform/microsoftStorePwa.ts +99 -0
  42. package/templates/phaser-game/src/runtime/gameContext.ts +3 -1
  43. package/templates/phaser-game/tools/run-game-acceptance.mjs +22 -0
  44. package/templates/phaser-game/vite.config.ts +161 -2
package/dist/index.js CHANGED
@@ -6,6 +6,9 @@ import completion from '@gunshi/plugin-completion';
6
6
  import i18n, { defineI18n } from '@gunshi/plugin-i18n';
7
7
  import resources from '@gunshi/resources';
8
8
  import { cli } from 'gunshi';
9
+ import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
10
+ export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, } from './game-acceptance.js';
11
+ export { assertProductionTargetReadiness, } from './production-target-readiness.js';
9
12
  const sourceDir = path.dirname(fileURLToPath(import.meta.url));
10
13
  const packageRoot = resolvePackageRoot(sourceDir);
11
14
  const detectedKitRoot = resolveDefaultKitRoot();
@@ -50,6 +53,15 @@ const supportedBuildTargets = [
50
53
  'reddit',
51
54
  ];
52
55
  const supportedVariants = ['wrapper', 'sync', 'simulator', 'archive'];
56
+ const acceptanceStepIds = {
57
+ check: 'check',
58
+ test: 'test',
59
+ build: 'build',
60
+ graph: 'graph-preflight',
61
+ playtest: 'playtest',
62
+ targetBuild: 'target-build',
63
+ targetSmoke: 'target-smoke',
64
+ };
53
65
  export async function runMpgdCli(args) {
54
66
  await cli([...args], entryCommand, {
55
67
  name: 'mpgd',
@@ -213,15 +225,402 @@ const gameCommand = defineI18n({
213
225
  });
214
226
  },
215
227
  }),
228
+ accept: defineI18n({
229
+ name: 'accept',
230
+ description: 'Run the reusable game handoff acceptance workflow.',
231
+ resource: commandResource({
232
+ en: 'Run check, test, build, graph, playtest, and target acceptance with handoff reports.',
233
+ ko: 'check, test, build, graph, playtest, 타깃 인수 검증과 인계 리포트를 실행합니다.',
234
+ }, {
235
+ game: {
236
+ en: 'Game root containing package.json and mpgd.targets.json.',
237
+ ko: 'package.json과 mpgd.targets.json이 있는 게임 루트.',
238
+ },
239
+ targets: {
240
+ en: 'Comma-separated target list, default, or all.',
241
+ ko: '쉼표로 구분한 타깃 목록, default 또는 all.',
242
+ },
243
+ profile: {
244
+ en: 'Target build profile. Defaults to staging.',
245
+ ko: '타깃 빌드 프로필. 기본값은 staging.',
246
+ },
247
+ 'playtest-script': {
248
+ en: 'Optional game-owned package script used for automated playtesting.',
249
+ ko: '자동 플레이테스트에 사용할 선택적 게임 소유 패키지 스크립트.',
250
+ },
251
+ 'report-dir': {
252
+ en: 'JSON and Markdown handoff report directory.',
253
+ ko: 'JSON 및 Markdown 인계 리포트 디렉터리.',
254
+ },
255
+ 'timeout-ms': {
256
+ en: 'Maximum duration for each acceptance command.',
257
+ ko: '각 인수 검증 명령의 최대 실행 시간.',
258
+ },
259
+ 'kit-path': {
260
+ en: 'Path to the mpgd-kit checkout.',
261
+ ko: 'mpgd-kit 체크아웃 경로.',
262
+ },
263
+ }),
264
+ args: {
265
+ game: {
266
+ type: 'positional',
267
+ required: true,
268
+ description: 'Game root containing package.json and mpgd.targets.json.',
269
+ },
270
+ targets: {
271
+ type: 'string',
272
+ required: false,
273
+ default: 'default',
274
+ description: 'Comma-separated target list, default, or all.',
275
+ },
276
+ profile: {
277
+ type: 'string',
278
+ required: false,
279
+ default: 'staging',
280
+ description: 'Target build profile.',
281
+ },
282
+ 'ait-variant': {
283
+ type: 'enum',
284
+ choices: supportedVariants,
285
+ required: false,
286
+ default: 'wrapper',
287
+ description: 'Variant for the ait target.',
288
+ },
289
+ 'ios-variant': {
290
+ type: 'enum',
291
+ choices: supportedVariants,
292
+ required: false,
293
+ description: 'Variant for the ios target.',
294
+ },
295
+ 'playtest-script': {
296
+ type: 'string',
297
+ required: false,
298
+ default: 'playtest',
299
+ description: 'Game-owned package script used for automated playtesting.',
300
+ },
301
+ 'report-dir': {
302
+ type: 'string',
303
+ required: false,
304
+ description: 'JSON and Markdown handoff report directory.',
305
+ },
306
+ 'timeout-ms': {
307
+ type: 'string',
308
+ required: false,
309
+ default: String(defaultGameAcceptanceCommandTimeoutMs),
310
+ description: 'Maximum duration for each acceptance command in milliseconds.',
311
+ },
312
+ 'kit-path': {
313
+ type: 'string',
314
+ required: false,
315
+ description: 'Path to the mpgd-kit checkout.',
316
+ },
317
+ 'skip-test': {
318
+ type: 'boolean',
319
+ required: false,
320
+ description: 'Skip the game-owned test package script.',
321
+ },
322
+ 'skip-graph': {
323
+ type: 'boolean',
324
+ required: false,
325
+ description: 'Skip the kit graph preflight.',
326
+ },
327
+ 'skip-playtest': {
328
+ type: 'boolean',
329
+ required: false,
330
+ description: 'Skip the game-owned playtest package script.',
331
+ },
332
+ 'skip-target-build': {
333
+ type: 'boolean',
334
+ required: false,
335
+ description: 'Skip the target build matrix.',
336
+ },
337
+ 'skip-target-smoke': {
338
+ type: 'boolean',
339
+ required: false,
340
+ description: 'Skip the target smoke matrix.',
341
+ },
342
+ },
343
+ run: (ctx) => {
344
+ const positionals = readLocalPositionals(ctx.positionals, ['game', 'accept']);
345
+ const gameRoot = path.resolve(readRequiredPositional(positionals, 0, 'game'));
346
+ const iosVariant = readOptionalString(ctx.values['ios-variant']);
347
+ const reportDir = readOptionalString(ctx.values['report-dir']);
348
+ const kitPath = readOptionalString(ctx.values['kit-path']);
349
+ const timeoutMs = readPositiveInteger(readOptionalString(ctx.values['timeout-ms'])
350
+ ?? String(defaultGameAcceptanceCommandTimeoutMs), '--timeout-ms');
351
+ acceptGame({
352
+ gameRoot,
353
+ targets: readOptionalString(ctx.values.targets) ?? 'default',
354
+ profile: readOptionalString(ctx.values.profile) ?? 'staging',
355
+ aitVariant: readOptionalString(ctx.values['ait-variant']) ?? 'wrapper',
356
+ ...(iosVariant === undefined ? {} : { iosVariant }),
357
+ playtestScript: readOptionalString(ctx.values['playtest-script']) ?? 'playtest',
358
+ ...(reportDir === undefined ? {} : { reportDir }),
359
+ ...(kitPath === undefined ? {} : { kitPath }),
360
+ timeoutMs,
361
+ skipTest: ctx.values['skip-test'] === true,
362
+ skipGraph: ctx.values['skip-graph'] === true,
363
+ skipPlaytest: ctx.values['skip-playtest'] === true,
364
+ skipTargetBuild: ctx.values['skip-target-build'] === true,
365
+ skipTargetSmoke: ctx.values['skip-target-smoke'] === true,
366
+ });
367
+ },
368
+ }),
369
+ icons: defineI18n({
370
+ name: 'icons',
371
+ description: 'Generate and verify target app icons from the game brand source.',
372
+ resource: commandResource({
373
+ en: 'Generate and verify target app icons from the game brand source.',
374
+ ko: '게임 브랜드 원본에서 타깃별 앱 아이콘을 생성하고 검증합니다.',
375
+ }),
376
+ subCommands: Object.fromEntries(['generate', 'verify', 'inspect'].map((action) => [
377
+ action,
378
+ defineI18n({
379
+ name: action,
380
+ description: `${action} target app icon assets.`,
381
+ resource: commandResource({
382
+ en: `${action} target app icon assets.`,
383
+ ko: `타깃 앱 아이콘 자산을 ${action}합니다.`,
384
+ }),
385
+ args: {
386
+ game: {
387
+ type: 'positional',
388
+ required: true,
389
+ description: 'Game root containing mpgd.game.json and mpgd.targets.json.',
390
+ },
391
+ targets: {
392
+ type: 'string',
393
+ required: false,
394
+ description: 'Comma-separated target names. Defaults to every configured target.',
395
+ },
396
+ profile: {
397
+ type: 'string',
398
+ required: false,
399
+ default: action === 'verify' ? 'production' : 'development',
400
+ description: 'Validation profile (development or production).',
401
+ },
402
+ 'kit-path': {
403
+ type: 'string',
404
+ required: false,
405
+ description: 'Path to the mpgd-kit checkout.',
406
+ },
407
+ },
408
+ run: (ctx) => {
409
+ const positionals = readLocalPositionals(ctx.positionals, ['game', 'icons', action]);
410
+ const gameRoot = path.resolve(readRequiredPositional(positionals, 0, 'game'));
411
+ const env = createTargetCommandEnv({
412
+ 'targets-file': path.join(gameRoot, 'mpgd.targets.json'),
413
+ 'kit-path': ctx.values['kit-path'],
414
+ });
415
+ const targets = readOptionalString(ctx.values.targets) ?? '';
416
+ const profile = readOptionalString(ctx.values.profile)
417
+ ?? (action === 'verify' ? 'production' : 'development');
418
+ runPnpm([`game:icons:${action}`, '--', targets, profile], env);
419
+ },
420
+ }),
421
+ ])),
422
+ run: () => {
423
+ console.log('Use "mpgd game icons generate <game>", verify, or inspect.');
424
+ },
425
+ }),
216
426
  },
217
427
  run: () => {
218
- console.log('Use "mpgd game create <directory>".');
428
+ console.log('Use "mpgd game create <directory>", "mpgd game accept <game>", or "mpgd game icons generate <game>".');
219
429
  },
220
430
  });
221
431
  const defaultLegalDir = 'legal';
222
432
  const defaultLegalOutDir = 'artifacts/legal-site';
223
433
  const legalPageSlugs = ['privacy', 'support', 'terms'];
224
434
  const legalPageFileList = legalPageSlugs.map((slug) => `${slug}.html`).join(', ');
435
+ function acceptGame(input) {
436
+ if (input.skipTargetBuild && !input.skipTargetSmoke) {
437
+ throw new Error('Use --skip-target-smoke when --skip-target-build is set.');
438
+ }
439
+ const gameRoot = assertGameAcceptanceRoot(input.gameRoot);
440
+ const gamePackage = readPackageJsonOrUndefined(gameRoot, { strictParse: true });
441
+ if (gamePackage === undefined) {
442
+ throw new Error(`Missing game package.json: ${gameRoot}`);
443
+ }
444
+ const targetsFile = path.join(gameRoot, 'mpgd.targets.json');
445
+ if (!existsSync(targetsFile)) {
446
+ throw new Error(`Missing game target config: ${targetsFile}`);
447
+ }
448
+ const kitPath = resolveKitPathForTarget({
449
+ ...(input.kitPath === undefined ? {} : { 'kit-path': input.kitPath }),
450
+ });
451
+ const packageManager = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
452
+ const steps = [
453
+ packageScriptAcceptanceStep({
454
+ id: acceptanceStepIds.check,
455
+ label: 'Game check',
456
+ script: 'check',
457
+ gameRoot,
458
+ packageManager,
459
+ scripts: gamePackage.scripts,
460
+ required: true,
461
+ }),
462
+ packageScriptAcceptanceStep({
463
+ id: acceptanceStepIds.test,
464
+ label: 'Game tests',
465
+ script: 'test',
466
+ gameRoot,
467
+ packageManager,
468
+ scripts: gamePackage.scripts,
469
+ required: false,
470
+ skipped: input.skipTest,
471
+ }),
472
+ packageScriptAcceptanceStep({
473
+ id: acceptanceStepIds.build,
474
+ label: 'Game build',
475
+ script: 'build',
476
+ gameRoot,
477
+ packageManager,
478
+ scripts: gamePackage.scripts,
479
+ required: true,
480
+ }),
481
+ input.skipGraph
482
+ ? skippedAcceptanceStep(acceptanceStepIds.graph, 'TypeScript graph preflight', 'Disabled by --skip-graph.')
483
+ : {
484
+ id: acceptanceStepIds.graph,
485
+ label: 'TypeScript graph preflight',
486
+ command: packageManager,
487
+ args: ['graph:preflight'],
488
+ cwd: kitPath,
489
+ },
490
+ packageScriptAcceptanceStep({
491
+ id: acceptanceStepIds.playtest,
492
+ label: 'Automated playtest',
493
+ script: input.playtestScript,
494
+ gameRoot,
495
+ packageManager,
496
+ scripts: gamePackage.scripts,
497
+ required: false,
498
+ skipped: input.skipPlaytest,
499
+ }),
500
+ targetMatrixAcceptanceStep({
501
+ action: 'build-all',
502
+ id: acceptanceStepIds.targetBuild,
503
+ label: 'Target build matrix',
504
+ kitPath,
505
+ packageManager,
506
+ targetsFile,
507
+ targets: input.targets,
508
+ profile: input.profile,
509
+ aitVariant: input.aitVariant,
510
+ ...(input.iosVariant === undefined ? {} : { iosVariant: input.iosVariant }),
511
+ skipped: input.skipTargetBuild,
512
+ }),
513
+ targetMatrixAcceptanceStep({
514
+ action: 'smoke-all',
515
+ id: acceptanceStepIds.targetSmoke,
516
+ label: 'Target smoke matrix',
517
+ kitPath,
518
+ packageManager,
519
+ targetsFile,
520
+ targets: input.targets,
521
+ profile: input.profile,
522
+ aitVariant: input.aitVariant,
523
+ ...(input.iosVariant === undefined ? {} : { iosVariant: input.iosVariant }),
524
+ skipped: input.skipTargetSmoke,
525
+ }),
526
+ ];
527
+ const reportDir = path.resolve(gameRoot, input.reportDir ?? 'artifacts/acceptance');
528
+ const releaseManifestFile = resolveGameAcceptanceReleaseManifestFile(gameRoot);
529
+ const result = runGameAcceptance({
530
+ gameRoot,
531
+ reportDir,
532
+ ...(input.skipTargetBuild
533
+ ? {}
534
+ : { releaseManifestFile }),
535
+ options: {
536
+ targets: input.targets,
537
+ profile: input.profile,
538
+ aitVariant: input.aitVariant,
539
+ iosVariant: input.iosVariant ?? null,
540
+ playtestScript: input.playtestScript,
541
+ skipTest: input.skipTest,
542
+ skipGraph: input.skipGraph,
543
+ skipPlaytest: input.skipPlaytest,
544
+ skipTargetBuild: input.skipTargetBuild,
545
+ skipTargetSmoke: input.skipTargetSmoke,
546
+ timeoutMs: String(input.timeoutMs),
547
+ },
548
+ steps,
549
+ commandTimeoutMs: input.timeoutMs,
550
+ env: {
551
+ ...process.env,
552
+ MPGD_KIT_PATH: kitPath,
553
+ MPGD_RELEASE_MANIFEST_FILE: releaseManifestFile,
554
+ },
555
+ });
556
+ console.log(`[mpgd:accept] JSON report: ${result.jsonFile}`);
557
+ console.log(`[mpgd:accept] Markdown report: ${result.markdownFile}`);
558
+ if (result.report.status === 'failed') {
559
+ throw new Error(`Game acceptance failed. Report: ${result.markdownFile}`);
560
+ }
561
+ console.log('[mpgd:accept] passed');
562
+ }
563
+ function assertGameAcceptanceRoot(gameRoot) {
564
+ const resolved = path.resolve(gameRoot);
565
+ if (!existsSync(resolved)) {
566
+ throw new Error(`Game root does not exist: ${resolved}`);
567
+ }
568
+ return realpathSync(resolved);
569
+ }
570
+ function packageScriptAcceptanceStep(input) {
571
+ if (input.skipped === true) {
572
+ return skippedAcceptanceStep(input.id, input.label, 'Disabled by command option.');
573
+ }
574
+ if (input.scripts?.[input.script] === undefined && !input.required) {
575
+ return skippedAcceptanceStep(input.id, input.label, `Optional package script "${input.script}" is not configured.`);
576
+ }
577
+ return {
578
+ id: input.id,
579
+ label: input.label,
580
+ command: input.packageManager,
581
+ args: ['run', input.script],
582
+ cwd: input.gameRoot,
583
+ };
584
+ }
585
+ function targetMatrixAcceptanceStep(input) {
586
+ if (input.skipped) {
587
+ return skippedAcceptanceStep(input.id, input.label, 'Disabled by command option.');
588
+ }
589
+ const args = [
590
+ 'mpgd',
591
+ 'target',
592
+ input.action,
593
+ '--targets-file',
594
+ input.targetsFile,
595
+ '--kit-path',
596
+ input.kitPath,
597
+ '--targets',
598
+ input.targets,
599
+ ];
600
+ if (input.action === 'build-all') {
601
+ args.push('--profile', input.profile, '--ait-variant', input.aitVariant);
602
+ if (input.iosVariant !== undefined) {
603
+ args.push('--ios-variant', input.iosVariant);
604
+ }
605
+ }
606
+ return {
607
+ id: input.id,
608
+ label: input.label,
609
+ command: input.packageManager,
610
+ args,
611
+ cwd: input.kitPath,
612
+ };
613
+ }
614
+ function skippedAcceptanceStep(id, label, skipReason) {
615
+ return { id, label, skipReason };
616
+ }
617
+ function readPositiveInteger(value, label) {
618
+ const parsed = Number(value);
619
+ if (!Number.isInteger(parsed) || parsed <= 0) {
620
+ throw new Error(`${label} must be a positive integer.`);
621
+ }
622
+ return parsed;
623
+ }
225
624
  const legalCommand = defineI18n({
226
625
  name: 'legal',
227
626
  description: 'Build static legal/support pages for store and release evidence.',
@@ -738,6 +1137,7 @@ function createGameApp(input) {
738
1137
  ' mpgd target build-all',
739
1138
  `--targets-file ${path.join(appDir, 'mpgd.targets.json')}`,
740
1139
  `--targets ${recommendedMatrixTargets}`,
1140
+ '--profile staging',
741
1141
  '--ait-variant wrapper',
742
1142
  '--kit-path <path-to-mpgd-kit>',
743
1143
  ].join(' '));
@@ -949,6 +1349,7 @@ function toTemplatePath(value) {
949
1349
  }
950
1350
  function createTargetCommandEnv(values) {
951
1351
  const targetsFile = path.resolve(readOptionalString(values['targets-file']) ?? 'mpgd.targets.json');
1352
+ const gameRoot = path.dirname(targetsFile);
952
1353
  const kitPath = resolveKitPathForTarget(values);
953
1354
  const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
954
1355
  const preparedTargetsFile = preparePlatformTargetsFile({
@@ -960,10 +1361,75 @@ function createTargetCommandEnv(values) {
960
1361
  });
961
1362
  return {
962
1363
  ...process.env,
1364
+ ...createGameOwnedReleaseEnv(gameRoot, process.env),
963
1365
  MPGD_KIT_PATH: kitPath,
964
1366
  MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
965
1367
  };
966
1368
  }
1369
+ function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
1370
+ const monetizationFiles = resolveGameOwnedMonetizationFiles(gameRoot, baseEnv);
1371
+ const sourceGitSha = readOptionalString(baseEnv.MPGD_SOURCE_GIT_SHA)
1372
+ ?? readSourceGitSha(gameRoot);
1373
+ const appVersion = readOptionalString(baseEnv.APP_VERSION)
1374
+ ?? readGamePackageVersion(gameRoot);
1375
+ return {
1376
+ ...monetizationFiles,
1377
+ MPGD_SOURCE_GIT_SHA: sourceGitSha,
1378
+ ...(appVersion === undefined ? {} : { APP_VERSION: appVersion }),
1379
+ };
1380
+ }
1381
+ function resolveGameOwnedMonetizationFiles(gameRoot, baseEnv) {
1382
+ const configuredCatalog = readConfiguredPath(baseEnv.MPGD_PRODUCT_CATALOG_FILE);
1383
+ const configuredPlacements = readConfiguredPath(baseEnv.MPGD_AD_PLACEMENTS_FILE);
1384
+ if ((configuredCatalog === undefined) !== (configuredPlacements === undefined)) {
1385
+ throw new Error('MPGD_PRODUCT_CATALOG_FILE and MPGD_AD_PLACEMENTS_FILE must be configured together.');
1386
+ }
1387
+ if (configuredCatalog !== undefined && configuredPlacements !== undefined) {
1388
+ return {
1389
+ MPGD_PRODUCT_CATALOG_FILE: path.resolve(gameRoot, configuredCatalog),
1390
+ MPGD_AD_PLACEMENTS_FILE: path.resolve(gameRoot, configuredPlacements),
1391
+ };
1392
+ }
1393
+ const catalogFile = path.join(gameRoot, 'mpgd.catalog.json');
1394
+ const placementsFile = path.join(gameRoot, 'mpgd.ad-placements.json');
1395
+ const hasCatalog = existsSync(catalogFile);
1396
+ const hasPlacements = existsSync(placementsFile);
1397
+ if (hasCatalog !== hasPlacements) {
1398
+ throw new Error(`Game target builds must provide both mpgd.catalog.json and mpgd.ad-placements.json: ${gameRoot}`);
1399
+ }
1400
+ if (!hasCatalog) {
1401
+ return {};
1402
+ }
1403
+ return {
1404
+ MPGD_PRODUCT_CATALOG_FILE: catalogFile,
1405
+ MPGD_AD_PLACEMENTS_FILE: placementsFile,
1406
+ };
1407
+ }
1408
+ function readConfiguredPath(value) {
1409
+ const normalized = value?.trim();
1410
+ return normalized === undefined || normalized.length === 0 ? undefined : normalized;
1411
+ }
1412
+ function readSourceGitSha(gameRoot) {
1413
+ const result = spawnSync('git', ['-C', gameRoot, 'rev-parse', 'HEAD'], {
1414
+ encoding: 'utf8',
1415
+ stdio: ['ignore', 'pipe', 'ignore'],
1416
+ });
1417
+ if (result.error !== undefined || result.status !== 0) {
1418
+ return 'uncommitted';
1419
+ }
1420
+ return result.stdout.trim() || 'uncommitted';
1421
+ }
1422
+ function readGamePackageVersion(gameRoot) {
1423
+ const packageFile = path.join(gameRoot, 'package.json');
1424
+ if (!existsSync(packageFile)) {
1425
+ return undefined;
1426
+ }
1427
+ const packageJson = readJsonForCli(packageFile);
1428
+ if (typeof packageJson !== 'object' || packageJson === null || Array.isArray(packageJson)) {
1429
+ return undefined;
1430
+ }
1431
+ return readOptionalString(packageJson.version);
1432
+ }
967
1433
  function preparePlatformTargetsFile(input) {
968
1434
  const parsed = readJsonForCli(input.targetsFile);
969
1435
  const gameRoot = path.dirname(input.targetsFile);
@@ -1022,8 +1488,9 @@ function replaceTargetTokens(value, context) {
1022
1488
  function runTargetCommand(input) {
1023
1489
  const target = normalizeBuildTarget(input.target);
1024
1490
  const env = withTargetVariantEnv(target, input.variant, input.env);
1491
+ const profile = input.profile ?? 'production';
1025
1492
  const args = input.action === 'build'
1026
- ? ['build:target', target, input.profile ?? 'production']
1493
+ ? ['build:target', target, profile]
1027
1494
  : ['smoke:target', target];
1028
1495
  console.log(`[mpgd] ${input.action} ${target}`);
1029
1496
  runPnpm(args, env);
@@ -0,0 +1,8 @@
1
+ export interface ProductionTargetReadinessInput {
2
+ readonly target: string;
3
+ readonly profile: string;
4
+ readonly targetsFile: string;
5
+ readonly gameRoot: string;
6
+ readonly gameServicesUrl?: string;
7
+ }
8
+ export declare function assertProductionTargetReadiness(input: ProductionTargetReadinessInput): void;