@mpgd/cli 0.35.0 → 0.36.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 (49) hide show
  1. package/dist/android-bundle-signer.d.ts +2 -0
  2. package/dist/android-bundle-signer.js +38 -0
  3. package/dist/android-signing-session.d.ts +22 -0
  4. package/dist/android-signing-session.js +198 -0
  5. package/dist/artifact-inspection-environment.d.ts +2 -0
  6. package/dist/artifact-inspection-environment.js +31 -0
  7. package/dist/asc-pin.json +26 -0
  8. package/dist/capacitor-shell-starter.d.ts +1 -0
  9. package/dist/capacitor-shell-starter.js +9 -6
  10. package/dist/deploy-planning.d.ts +64 -0
  11. package/dist/deploy-planning.js +471 -0
  12. package/dist/deploy-process.d.ts +28 -0
  13. package/dist/deploy-process.js +197 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.js +409 -10
  16. package/dist/ios-ipa-inspection.js +151 -0
  17. package/dist/ios-keychain-import.swift +92 -0
  18. package/dist/ios-plist.d.ts +2 -0
  19. package/dist/ios-plist.js +75 -0
  20. package/dist/ios-signing-profile.d.ts +12 -0
  21. package/dist/ios-signing-profile.js +60 -0
  22. package/dist/ios-signing-session.d.ts +22 -0
  23. package/dist/ios-signing-session.js +256 -0
  24. package/dist/kit-upgrade-target-readiness.d.ts +8 -0
  25. package/dist/kit-upgrade-target-readiness.js +119 -0
  26. package/dist/kit-upgrade.d.ts +2 -0
  27. package/dist/kit-upgrade.js +3 -0
  28. package/dist/native-build-info.json +5 -0
  29. package/dist/native-build-target.js +12032 -0
  30. package/dist/native-deploy-run.d.ts +35 -0
  31. package/dist/native-deploy-run.js +462 -0
  32. package/dist/native-deploy-submission.d.ts +40 -0
  33. package/dist/native-deploy-submission.js +244 -0
  34. package/dist/native-deploy-workflow.d.ts +11 -0
  35. package/dist/native-deploy-workflow.js +328 -0
  36. package/dist/play-internal-submission.d.ts +32 -0
  37. package/dist/play-internal-submission.js +247 -0
  38. package/dist/play-publisher-port.d.ts +30 -0
  39. package/dist/play-publisher-port.js +1 -0
  40. package/dist/play-sdk-adapter.js +78 -0
  41. package/dist/release-state.d.ts +140 -0
  42. package/dist/release-state.js +1046 -0
  43. package/dist/release-workspace.d.ts +61 -0
  44. package/dist/release-workspace.js +504 -0
  45. package/dist/testflight-submission.d.ts +49 -0
  46. package/dist/testflight-submission.js +462 -0
  47. package/package.json +18 -13
  48. package/templates/phaser-game/README.md +23 -8
  49. package/templates/phaser-game/src/main.ts +34 -33
package/dist/index.js CHANGED
@@ -6,9 +6,12 @@ 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 { assertPlatformVersionLedger } from '@mpgd/target-config';
9
10
  import { buildAssetPacks } from './asset-pack-build.js';
10
11
  import { verifyAssetPackDelivery } from './asset-pack-verify.js';
11
12
  import { materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
13
+ import { doctorNativeDeployment, initializeDeployConfig, parseDeployTargets, planNativeDeployment, readNativeDeploymentPlan, writeNativeDeploymentPlan, } from './deploy-planning.js';
14
+ import { initializeNativeDeployWorkflow } from './native-deploy-workflow.js';
12
15
  import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
13
16
  import { escapeMarkdownInline as escapeEvidenceMarkdownInline, writeEvidenceReportFiles, } from './evidence-io.js';
14
17
  import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
@@ -20,9 +23,20 @@ import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStore
20
23
  import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
21
24
  import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
22
25
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
23
- import { targetConfigExtensionsFileEnv } from './target-config-env.js';
26
+ import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
27
+ import { readStoreCredential, runNativeDeployment } from './native-deploy-run.js';
28
+ import { submitRecordedNativeTarget } from './native-deploy-submission.js';
29
+ import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
24
30
  export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
25
31
  export { applyCapacitorShellStarter, materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
32
+ export { ReleaseProcessError, runReleaseProcess, } from './deploy-process.js';
33
+ export { installPinnedReleaseDependencies, pinNativeDeploymentPlan, preparePinnedReleaseWorkspace, runPinnedNativeBuild, withPinnedReleaseWorkspace, } from './release-workspace.js';
34
+ export { withAndroidUploadSigningSession, } from './android-signing-session.js';
35
+ export { checkpointNativeSubmission, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, recordNativeReleaseBuild, reserveNativeRelease, } from './release-state.js';
36
+ export { runNativeDeployment, readStoreCredential, } from './native-deploy-run.js';
37
+ export { submitRecordedNativeTarget, } from './native-deploy-submission.js';
38
+ export { PlaySubmissionUncertainError, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
39
+ export { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
26
40
  export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
27
41
  export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
28
42
  export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, } from './hosted-pwa-deployment.js';
@@ -156,6 +170,7 @@ export async function runMpgdCli(args) {
156
170
  subCommands: {
157
171
  assets: assetsCommand,
158
172
  game: gameCommand,
173
+ deploy: deployCommand,
159
174
  legal: legalCommand,
160
175
  target: targetCommand,
161
176
  kit: kitCommand,
@@ -208,10 +223,275 @@ const entryCommand = defineI18n({
208
223
  ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
209
224
  }),
210
225
  run: () => {
211
- console.info('Use a sub-command: assets, game, legal, target, kit.');
226
+ console.info('Use a sub-command: assets, game, deploy, legal, target, kit.');
212
227
  console.info('Run "pnpm mpgd --help" for available commands.');
213
228
  },
214
229
  });
230
+ const deployCommand = defineI18n({
231
+ name: 'deploy',
232
+ description: 'Configure and inspect game-owned native test deployments.',
233
+ resource: commandResource({
234
+ en: 'Configure and inspect game-owned native test deployments.',
235
+ ko: '게임 소유 네이티브 테스트 배포를 설정하고 점검합니다.',
236
+ }),
237
+ subCommands: {
238
+ init: defineI18n({
239
+ name: 'init',
240
+ description: 'Create game-owned deployment profile references.',
241
+ resource: commandResource({
242
+ en: 'Create game-owned deployment profile references.',
243
+ ko: '게임 소유 배포 프로필 참조를 생성합니다.',
244
+ }),
245
+ args: {
246
+ game: {
247
+ type: 'string',
248
+ required: false,
249
+ description: 'Game project directory containing mpgd.targets.json.',
250
+ },
251
+ },
252
+ run: (ctx) => {
253
+ const game = readOptionalString(ctx.values.game) ?? '.';
254
+ console.info(`Created deployment config: ${initializeDeployConfig(game)}`);
255
+ },
256
+ }),
257
+ workflow: defineI18n({
258
+ name: 'workflow',
259
+ description: 'Scaffold game-owned native deployment CI.',
260
+ resource: commandResource({
261
+ en: 'Scaffold game-owned native deployment CI.',
262
+ ko: '게임 소유 네이티브 배포 CI를 생성합니다.',
263
+ }),
264
+ subCommands: {
265
+ init: defineI18n({
266
+ name: 'init',
267
+ description: 'Create a protected-branch native deployment workflow.',
268
+ resource: commandResource({
269
+ en: 'Create a protected-branch native deployment workflow.',
270
+ ko: '보호 브랜치용 네이티브 배포 워크플로우를 생성합니다.',
271
+ }),
272
+ args: {
273
+ game: { type: 'string', required: false, description: 'Game project directory.' },
274
+ profile: { type: 'string', required: false, description: 'Deployment profile name.' },
275
+ targets: { type: 'string', required: false, description: 'android,ios selection.' },
276
+ 'release-branch': {
277
+ type: 'string', required: true,
278
+ description: 'Protected branch allowed to dispatch signed deployments.',
279
+ },
280
+ 'game-id': { type: 'string', required: false, description: 'Release ledger game ID.' },
281
+ 'approval-environment': {
282
+ type: 'string', required: false,
283
+ description: 'Protected GitHub Environment name.',
284
+ },
285
+ },
286
+ run: (ctx) => {
287
+ const targets = parseDeployTargets(readOptionalString(ctx.values.targets));
288
+ const gameId = readOptionalString(ctx.values['game-id']);
289
+ const approvalEnvironment = readOptionalString(ctx.values['approval-environment']);
290
+ const file = initializeNativeDeployWorkflow({
291
+ game: readOptionalString(ctx.values.game) ?? '.',
292
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
293
+ releaseBranch: readRequiredCliOption(ctx.values['release-branch'], '--release-branch'),
294
+ ...(targets === undefined ? {} : { targets }),
295
+ ...(gameId === undefined ? {} : { gameId }),
296
+ ...(approvalEnvironment === undefined ? {} : { approvalEnvironment }),
297
+ });
298
+ console.info(`Created native deployment workflow: ${file}`);
299
+ },
300
+ }),
301
+ },
302
+ }),
303
+ doctor: defineI18n({
304
+ name: 'doctor',
305
+ description: 'Check local native deployment configuration and toolchains.',
306
+ resource: commandResource({
307
+ en: 'Check local native deployment configuration and toolchains.',
308
+ ko: '로컬 네이티브 배포 설정과 도구 체인을 점검합니다.',
309
+ }),
310
+ args: {
311
+ game: {
312
+ type: 'string',
313
+ required: false,
314
+ description: 'Game project directory.',
315
+ },
316
+ profile: {
317
+ type: 'string',
318
+ required: false,
319
+ description: 'Deployment profile name; defaults to beta.',
320
+ },
321
+ targets: {
322
+ type: 'string',
323
+ required: false,
324
+ description: 'Comma-separated android,ios target selection.',
325
+ },
326
+ },
327
+ run: (ctx) => {
328
+ const targets = parseDeployTargets(readOptionalString(ctx.values.targets));
329
+ const result = doctorNativeDeployment({
330
+ game: readOptionalString(ctx.values.game) ?? '.',
331
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
332
+ ...(targets === undefined ? {} : { targets }),
333
+ });
334
+ for (const check of result.checks) {
335
+ console.info(`${check.status}: ${check.name} — ${check.detail}`);
336
+ }
337
+ if (!result.healthy) {
338
+ throw new Error('Native deployment doctor found missing requirements.');
339
+ }
340
+ },
341
+ }),
342
+ plan: defineI18n({
343
+ name: 'plan',
344
+ description: 'Write a read-only native deployment plan without reserving versions.',
345
+ resource: commandResource({
346
+ en: 'Write a read-only native deployment plan without reserving versions.',
347
+ ko: '버전 예약 없이 읽기 전용 네이티브 배포 계획을 작성합니다.',
348
+ }),
349
+ args: {
350
+ game: {
351
+ type: 'string',
352
+ required: false,
353
+ description: 'Game project directory.',
354
+ },
355
+ profile: {
356
+ type: 'string',
357
+ required: false,
358
+ description: 'Deployment profile name; defaults to beta.',
359
+ },
360
+ targets: {
361
+ type: 'string',
362
+ required: false,
363
+ description: 'Comma-separated android,ios target selection.',
364
+ },
365
+ out: {
366
+ type: 'string',
367
+ required: true,
368
+ description: 'New plan JSON path; existing files are not overwritten.',
369
+ },
370
+ },
371
+ run: (ctx) => {
372
+ const targets = parseDeployTargets(readOptionalString(ctx.values.targets));
373
+ const plan = planNativeDeployment({
374
+ game: readOptionalString(ctx.values.game) ?? '.',
375
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
376
+ ...(targets === undefined ? {} : { targets }),
377
+ });
378
+ const out = readRequiredCliOption(ctx.values.out, '--out');
379
+ writeNativeDeploymentPlan(out, plan);
380
+ console.info(`Wrote read-only deployment plan: ${path.resolve(out)}`);
381
+ },
382
+ }),
383
+ status: defineI18n({
384
+ name: 'status',
385
+ description: 'Read one game-owned native release and its store checkpoints.',
386
+ resource: commandResource({
387
+ en: 'Read one game-owned native release and its store checkpoints.',
388
+ ko: '게임 소유 네이티브 릴리스와 스토어 체크포인트를 조회합니다.',
389
+ }),
390
+ args: {
391
+ game: { type: 'string', required: false, description: 'Game project directory.' },
392
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
393
+ release: { type: 'string', required: true, description: 'Explicit release ID.' },
394
+ },
395
+ run: async (ctx) => {
396
+ const status = await readNativeReleaseStatusForCli({
397
+ gameRoot: path.resolve(readOptionalString(ctx.values.game) ?? '.'),
398
+ gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
399
+ releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
400
+ });
401
+ console.info(JSON.stringify(status, null, 2));
402
+ },
403
+ }),
404
+ submit: defineI18n({
405
+ name: 'submit',
406
+ description: 'Resume store submission of one already verified native build.',
407
+ resource: commandResource({
408
+ en: 'Resume store submission of one already verified native build.',
409
+ ko: '이미 검증한 네이티브 빌드의 스토어 제출을 재개합니다.',
410
+ }),
411
+ args: {
412
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON.' },
413
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
414
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
415
+ release: { type: 'string', required: true, description: 'Explicit release ID.' },
416
+ target: { type: 'string', required: true, description: 'android or ios.' },
417
+ approve: { type: 'boolean', required: false, description: 'Approve internal test submission.' },
418
+ },
419
+ run: async (ctx) => {
420
+ const game = readOptionalString(ctx.values.game);
421
+ const plan = readNativeDeploymentPlan(readRequiredCliOption(ctx.values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
422
+ const target = readRequiredCliOption(ctx.values.target, '--target');
423
+ if (target !== 'android' && target !== 'ios') {
424
+ throw new Error('--target must be android or ios.');
425
+ }
426
+ const result = await submitRecordedNativeTarget({
427
+ plan,
428
+ gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
429
+ releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
430
+ credential: readStoreCredential(plan, target, process.env),
431
+ approved: isDeployApproved(plan, ctx.values.approve === true),
432
+ });
433
+ console.info(JSON.stringify(result, null, 2));
434
+ },
435
+ }),
436
+ run: defineI18n({
437
+ name: 'run',
438
+ description: 'Reserve, build, verify and submit native internal-test releases.',
439
+ resource: commandResource({
440
+ en: 'Reserve, build, verify and submit native internal-test releases.',
441
+ ko: '네이티브 내부 테스트 릴리스를 예약·빌드·검증·제출합니다.',
442
+ }),
443
+ args: {
444
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON.' },
445
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
446
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
447
+ 'game-version': { type: 'string', required: true, description: 'Game SemVer version.' },
448
+ release: { type: 'string', required: true, description: 'Explicit release ID.' },
449
+ 'initial-ledger': {
450
+ type: 'string', required: false,
451
+ description: 'Initial version ledger JSON; only for the first release.',
452
+ },
453
+ approve: { type: 'boolean', required: false, description: 'Approve internal test submission.' },
454
+ },
455
+ run: async (ctx) => {
456
+ const game = readOptionalString(ctx.values.game);
457
+ const plan = readNativeDeploymentPlan(readRequiredCliOption(ctx.values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
458
+ const infoFile = path.join(packageRoot, 'dist/native-build-info.json');
459
+ const info = readJsonForCli(infoFile, 'native builder metadata');
460
+ assertJsonObject(info, 'native builder package metadata');
461
+ if (info.packageVersion !== cliVersion || typeof info.kitGitSha !== 'string'
462
+ || !/^[0-9a-f]{40}$/u.test(info.kitGitSha) || info.kitDirty !== false) {
463
+ throw new Error('Installed CLI has no clean, matching native Kit build identity.');
464
+ }
465
+ const initialLedgerFile = readOptionalString(ctx.values['initial-ledger']);
466
+ const recordedBuildMarker = process.env.MPGD_RECORDED_BUILD_MARKER;
467
+ if (recordedBuildMarker !== undefined
468
+ && (process.env.RUNNER_TEMP === undefined
469
+ || !path.isAbsolute(process.env.RUNNER_TEMP)
470
+ || !path.isAbsolute(recordedBuildMarker)
471
+ || path.dirname(recordedBuildMarker) !== path.resolve(process.env.RUNNER_TEMP))) {
472
+ throw new Error('The recorded-build marker must be directly inside RUNNER_TEMP.');
473
+ }
474
+ const status = await runNativeDeployment({
475
+ plan,
476
+ gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
477
+ gameVersion: readRequiredCliOption(ctx.values['game-version'], '--game-version'),
478
+ releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
479
+ kit: { packageVersion: cliVersion, gitSha: info.kitGitSha },
480
+ ...(initialLedgerFile === undefined ? {} : {
481
+ initialLedger: assertPlatformVersionLedger(readJsonForCli(path.resolve(initialLedgerFile), 'initial version ledger')),
482
+ }),
483
+ approved: isDeployApproved(plan, ctx.values.approve === true),
484
+ ...(recordedBuildMarker === undefined ? {} : {
485
+ onBuildRecorded: (target) => {
486
+ writeFileSync(recordedBuildMarker, target, { flag: 'wx', mode: 0o600 });
487
+ },
488
+ }),
489
+ });
490
+ console.info(JSON.stringify(status, null, 2));
491
+ },
492
+ }),
493
+ },
494
+ });
215
495
  const gameCommand = defineI18n({
216
496
  name: 'game',
217
497
  description: 'Create standalone Phaser game starters.',
@@ -1340,7 +1620,7 @@ const targetCommand = defineI18n({
1340
1620
  action: 'build',
1341
1621
  target,
1342
1622
  profile,
1343
- env: createTargetCommandEnv(ctx.values),
1623
+ env: createTargetCommandEnv(ctx.values, { nativeBuildTarget: target }),
1344
1624
  ...(variant === undefined ? {} : { variant }),
1345
1625
  });
1346
1626
  },
@@ -2097,6 +2377,9 @@ const kitCommand = defineI18n({
2097
2377
  for (const note of plan.notes) {
2098
2378
  console.info('Note: ' + note);
2099
2379
  }
2380
+ for (const advisory of plan.targetAdvisories) {
2381
+ console.info('Target advisory [' + advisory.target + ']: ' + advisory.message);
2382
+ }
2100
2383
  for (const blocker of plan.blockers) {
2101
2384
  console.error('Blocked: ' + blocker);
2102
2385
  }
@@ -2644,10 +2927,11 @@ function toJavaScriptStringLiteral(value) {
2644
2927
  function toTemplatePath(value) {
2645
2928
  return value.split(path.sep).join('/');
2646
2929
  }
2647
- function createTargetCommandEnv(values) {
2930
+ function createTargetCommandEnv(values, options = {}) {
2648
2931
  const targetsFile = path.resolve(readOptionalString(values['targets-file']) ?? 'mpgd.targets.json');
2649
2932
  const gameRoot = path.dirname(targetsFile);
2650
- const kitPath = resolveKitPathForTarget(values);
2933
+ const standaloneNative = resolveStandaloneNativeBuild(targetsFile, options.nativeBuildTarget, values);
2934
+ const kitPath = standaloneNative === undefined ? resolveKitPathForTarget(values) : gameRoot;
2651
2935
  const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
2652
2936
  const targetConfigExtensionsFile = resolveTargetConfigExtensionsFile(gameRoot, readOptionalString(values['target-config-extensions-file']));
2653
2937
  const preparedTargetsFile = preparePlatformTargetsFile({
@@ -2657,15 +2941,101 @@ function createTargetCommandEnv(values) {
2657
2941
  ? {}
2658
2942
  : { outputFile: path.resolve(resolvedTargetsFile) }),
2659
2943
  });
2660
- return {
2944
+ const env = {
2661
2945
  ...process.env,
2662
2946
  ...createGameOwnedReleaseEnv(gameRoot, process.env),
2663
- MPGD_KIT_PATH: kitPath,
2664
2947
  MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
2665
2948
  ...(targetConfigExtensionsFile === undefined
2666
2949
  ? {}
2667
2950
  : { [targetConfigExtensionsFileEnv]: targetConfigExtensionsFile }),
2668
2951
  };
2952
+ if (standaloneNative === undefined) {
2953
+ env.MPGD_KIT_PATH = kitPath;
2954
+ delete env.MPGD_NATIVE_PACKAGED_BUILD;
2955
+ delete env.MPGD_PACKAGED_KIT_GIT_SHA;
2956
+ }
2957
+ else {
2958
+ delete env.MPGD_KIT_PATH;
2959
+ env.MPGD_NATIVE_PACKAGED_BUILD = '1';
2960
+ env.MPGD_PACKAGED_KIT_GIT_SHA = standaloneNative.kitGitSha;
2961
+ env.MPGD_SKIP_BUILD_TARGET_PREFLIGHT = '1';
2962
+ env[targetConfigMatrixFileEnv] = standaloneNative.targetMatrixFile;
2963
+ }
2964
+ return env;
2965
+ }
2966
+ function resolveStandaloneNativeBuild(targetsFile, requestedTarget, values) {
2967
+ if ((requestedTarget !== 'android' && requestedTarget !== 'ios')
2968
+ || readOptionalString(values['kit-path']) !== undefined
2969
+ || readOptionalString(process.env.MPGD_KIT_PATH) !== undefined) {
2970
+ return undefined;
2971
+ }
2972
+ const raw = readJsonForCli(targetsFile);
2973
+ assertJsonObject(raw, `targets file ${targetsFile}`);
2974
+ assertJsonObject(raw.targets, `targets in ${targetsFile}`);
2975
+ const selected = raw.targets[requestedTarget];
2976
+ const expectedKind = requestedTarget === 'android' ? 'capacitor-android' : 'capacitor-ios';
2977
+ if (typeof selected !== 'object' || selected === null || Array.isArray(selected)
2978
+ || selected.kind !== expectedKind) {
2979
+ return undefined;
2980
+ }
2981
+ if (JSON.stringify(selected).includes('${MPGD_KIT_PATH}')) {
2982
+ if (detectedKitRoot !== undefined) {
2983
+ return undefined;
2984
+ }
2985
+ throw new Error(`Native target ${requestedTarget} still references the Kit checkout. Run mpgd target init capacitor to create a game-owned shell.`);
2986
+ }
2987
+ const gameRoot = path.dirname(targetsFile);
2988
+ const gameRootReal = realpathSync(gameRoot);
2989
+ for (const field of ['shellApp', 'webDir']) {
2990
+ const configuredPath = selected[field];
2991
+ if (typeof configuredPath !== 'string') {
2992
+ return undefined;
2993
+ }
2994
+ const candidate = path.resolve(gameRoot, configuredPath
2995
+ .replaceAll('${MPGD_GAME_ROOT}', gameRoot)
2996
+ .replaceAll('${MPGD_GAME_APP_ROOT}', gameRoot));
2997
+ let existingAncestor = candidate;
2998
+ while (!existsSync(existingAncestor)) {
2999
+ const parent = path.dirname(existingAncestor);
3000
+ if (parent === existingAncestor) {
3001
+ throw new Error(`Cannot resolve native target ${requestedTarget} ${field}: ${configuredPath}`);
3002
+ }
3003
+ existingAncestor = parent;
3004
+ }
3005
+ const canonicalCandidate = path.resolve(realpathSync(existingAncestor), path.relative(existingAncestor, candidate));
3006
+ const relativeToGame = path.relative(gameRootReal, canonicalCandidate);
3007
+ if (relativeToGame === '' || relativeToGame === '..'
3008
+ || relativeToGame.startsWith(`..${path.sep}`)
3009
+ || path.isAbsolute(relativeToGame)) {
3010
+ if (detectedKitRoot !== undefined) {
3011
+ return undefined;
3012
+ }
3013
+ throw new Error(`Native target ${requestedTarget} ${field} must stay inside its game project. Run mpgd target init capacitor to create a game-owned shell.`);
3014
+ }
3015
+ }
3016
+ const script = path.join(packageRoot, 'dist/native-build-target.js');
3017
+ const infoFile = path.join(packageRoot, 'dist/native-build-info.json');
3018
+ if (!existsSync(script) || !existsSync(infoFile)) {
3019
+ if (detectedKitRoot !== undefined) {
3020
+ return undefined;
3021
+ }
3022
+ throw new Error('The installed @mpgd/cli package is missing its native builder artifacts.');
3023
+ }
3024
+ const info = readJsonForCli(infoFile);
3025
+ assertJsonObject(info, 'native builder package metadata');
3026
+ if (info.packageVersion !== cliVersion
3027
+ || typeof info.kitGitSha !== 'string'
3028
+ || !/^[0-9a-f]{40}$/u.test(info.kitGitSha)
3029
+ || typeof info.kitDirty !== 'boolean') {
3030
+ throw new Error('The installed native builder metadata does not match @mpgd/cli.');
3031
+ }
3032
+ if (info.kitDirty) {
3033
+ throw new Error('The installed native builder was packaged from a dirty Kit worktree and cannot provide reliable kitGitSha provenance.');
3034
+ }
3035
+ return {
3036
+ kitGitSha: info.kitGitSha,
3037
+ targetMatrixFile: fileURLToPath(import.meta.resolve('@mpgd/target-config/targets.json')),
3038
+ };
2669
3039
  }
2670
3040
  function resolveTargetConfigExtensionsFile(gameRoot, configuredFile) {
2671
3041
  const inheritedFile = readConfiguredPath(process.env[targetConfigExtensionsFileEnv]);
@@ -2755,19 +3125,19 @@ function preparePlatformTargetsFile(input) {
2755
3125
  writeFileSync(outputFile, `${JSON.stringify(rendered, null, 2)}\n`);
2756
3126
  return outputFile;
2757
3127
  }
2758
- function readJsonForCli(file) {
3128
+ function readJsonForCli(file, label = 'targets file') {
2759
3129
  let raw;
2760
3130
  try {
2761
3131
  raw = readFileSync(file, 'utf8');
2762
3132
  }
2763
3133
  catch (error) {
2764
- throw new Error(`Failed to read targets file ${file}: ${formatError(error)}`);
3134
+ throw new Error(`Failed to read ${label} ${file}: ${formatError(error)}`);
2765
3135
  }
2766
3136
  try {
2767
3137
  return JSON.parse(raw);
2768
3138
  }
2769
3139
  catch (error) {
2770
- throw new Error(`Failed to parse targets file ${file}: ${formatError(error)}`);
3140
+ throw new Error(`Failed to parse ${label} ${file}: ${formatError(error)}`);
2771
3141
  }
2772
3142
  }
2773
3143
  function formatError(error) {
@@ -2799,12 +3169,38 @@ function runTargetCommand(input) {
2799
3169
  const target = normalizeBuildTarget(input.target, input.env);
2800
3170
  const env = withTargetVariantEnv(target, input.variant, input.env);
2801
3171
  const profile = input.profile ?? 'production';
3172
+ if (input.action === 'build' && env.MPGD_NATIVE_PACKAGED_BUILD === '1') {
3173
+ if (target !== 'android' && target !== 'ios') {
3174
+ throw new Error(`Packaged native builder cannot build target ${target}.`);
3175
+ }
3176
+ console.info(`[mpgd] build ${target} from installed CLI`);
3177
+ runPackagedNativeBuild(target, profile, env);
3178
+ return;
3179
+ }
2802
3180
  const args = input.action === 'build'
2803
3181
  ? ['build:target', target, profile]
2804
3182
  : ['smoke:target', target];
2805
3183
  console.info(`[mpgd] ${input.action} ${target}`);
2806
3184
  runPnpm(args, env);
2807
3185
  }
3186
+ function runPackagedNativeBuild(target, profile, env) {
3187
+ const targetsFile = env.MPGD_PLATFORM_TARGETS_FILE;
3188
+ if (targetsFile === undefined) {
3189
+ throw new Error('Packaged native build is missing its target config file.');
3190
+ }
3191
+ const script = path.join(packageRoot, 'dist/native-build-target.js');
3192
+ const result = spawnSync(process.execPath, [script, target, profile], {
3193
+ cwd: path.dirname(targetsFile),
3194
+ env,
3195
+ stdio: 'inherit',
3196
+ });
3197
+ if (result.error !== undefined) {
3198
+ throw result.error;
3199
+ }
3200
+ if (result.status !== 0) {
3201
+ throw new Error(`Packaged ${target} build failed with exit code ${result.status}.`);
3202
+ }
3203
+ }
2808
3204
  function runTargetMatrix(input) {
2809
3205
  for (const target of input.targets) {
2810
3206
  const variant = variantForTarget(target, input);
@@ -3090,6 +3486,9 @@ function renderHostedPwaVerificationMarkdown(verification) {
3090
3486
  '',
3091
3487
  ].join('\n');
3092
3488
  }
3489
+ function isDeployApproved(plan, approveFlag) {
3490
+ return plan.approval === 'preapproved-internal-test' || approveFlag;
3491
+ }
3093
3492
  function readRequiredCliOption(value, label) {
3094
3493
  const read = readOptionalString(value);
3095
3494
  if (read === undefined) {