@mpgd/cli 0.34.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 +483 -13
  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 +41 -0
  27. package/dist/kit-upgrade.js +489 -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 +20 -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';
@@ -17,11 +20,23 @@ import { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, verifyHost
17
20
  import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
18
21
  import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
19
22
  import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
23
+ import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
20
24
  import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
21
25
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
22
- 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';
23
30
  export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
24
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';
25
40
  export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
26
41
  export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
27
42
  export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, } from './hosted-pwa-deployment.js';
@@ -135,7 +150,7 @@ export async function runMpgdCli(args) {
135
150
  // partial document. Every other command keeps the default rendering.
136
151
  renderHeader: async (ctx) => {
137
152
  const values = ctx.values;
138
- if (ctx.name === 'verify-delivery' && values.json === true) {
153
+ if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
139
154
  return '';
140
155
  }
141
156
  const title = ctx.env.description || ctx.env.name || '';
@@ -146,7 +161,7 @@ export async function runMpgdCli(args) {
146
161
  renderValidationErrors: async (ctx, error) => {
147
162
  const messages = error.errors.map((entry) => String(entry.message)).join('\n');
148
163
  const values = ctx.values;
149
- if (ctx.name === 'verify-delivery' && values.json === true) {
164
+ if ((ctx.name === 'verify-delivery' || ctx.name === 'upgrade') && values.json === true) {
150
165
  process.stderr.write(`${messages}\n`);
151
166
  return '';
152
167
  }
@@ -155,6 +170,7 @@ export async function runMpgdCli(args) {
155
170
  subCommands: {
156
171
  assets: assetsCommand,
157
172
  game: gameCommand,
173
+ deploy: deployCommand,
158
174
  legal: legalCommand,
159
175
  target: targetCommand,
160
176
  kit: kitCommand,
@@ -207,10 +223,275 @@ const entryCommand = defineI18n({
207
223
  ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
208
224
  }),
209
225
  run: () => {
210
- console.info('Use a sub-command: assets, game, legal, target, kit.');
226
+ console.info('Use a sub-command: assets, game, deploy, legal, target, kit.');
211
227
  console.info('Run "pnpm mpgd --help" for available commands.');
212
228
  },
213
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
+ });
214
495
  const gameCommand = defineI18n({
215
496
  name: 'game',
216
497
  description: 'Create standalone Phaser game starters.',
@@ -1339,7 +1620,7 @@ const targetCommand = defineI18n({
1339
1620
  action: 'build',
1340
1621
  target,
1341
1622
  profile,
1342
- env: createTargetCommandEnv(ctx.values),
1623
+ env: createTargetCommandEnv(ctx.values, { nativeBuildTarget: target }),
1343
1624
  ...(variant === undefined ? {} : { variant }),
1344
1625
  });
1345
1626
  },
@@ -2042,6 +2323,79 @@ const kitCommand = defineI18n({
2042
2323
  ko: '현재 mpgd-kit 체크아웃을 점검합니다.',
2043
2324
  }),
2044
2325
  subCommands: {
2326
+ upgrade: defineI18n({
2327
+ name: 'upgrade',
2328
+ description: 'Plan or apply the latest published Kit package versions for a game.',
2329
+ resource: commandResource({
2330
+ base: {
2331
+ en: 'Plan or apply the latest published Kit package versions for a game.',
2332
+ ko: '게임에 배포된 최신 Kit 패키지 버전 적용을 계획하거나 실행합니다.',
2333
+ },
2334
+ args: {
2335
+ game: {
2336
+ en: 'Game directory containing package.json and optional mpgd.targets.json.',
2337
+ ko: 'package.json과 선택적 mpgd.targets.json이 있는 게임 디렉터리.',
2338
+ },
2339
+ 'targets-file': {
2340
+ en: 'Optional game-owned target configuration file.',
2341
+ ko: '선택적 게임 소유 타깃 구성 파일.',
2342
+ },
2343
+ apply: {
2344
+ en: 'Write the planned manifests and pnpm lockfiles.',
2345
+ ko: '계획된 의존성 선언과 pnpm 락파일을 갱신합니다.',
2346
+ },
2347
+ json: {
2348
+ en: 'Write one machine-readable JSON document to stdout.',
2349
+ ko: '표준 출력에 기계 판독용 JSON 문서 하나를 출력합니다.',
2350
+ },
2351
+ },
2352
+ }),
2353
+ args: {
2354
+ game: { type: 'string', required: false, default: '.', description: 'Game directory.' },
2355
+ 'targets-file': { type: 'string', required: false, description: 'Target config file.' },
2356
+ apply: { type: 'boolean', required: false, description: 'Apply the upgrade plan.' },
2357
+ json: { type: 'boolean', required: false, description: 'Print JSON.' },
2358
+ },
2359
+ run: async (ctx) => {
2360
+ const game = readOptionalString(ctx.values.game) ?? '.';
2361
+ const plan = await planKitUpgrade(game, undefined, readOptionalString(ctx.values['targets-file']));
2362
+ const result = ctx.values.apply === true && plan.blockers.length === 0
2363
+ ? applyKitUpgrade(plan)
2364
+ : undefined;
2365
+ if (ctx.values.json === true) {
2366
+ process.stdout.write(JSON.stringify({ plan, result: result ?? null }, null, 2) + '\n');
2367
+ }
2368
+ else {
2369
+ console.info('Kit upgrade plan for ' + plan.gameRoot);
2370
+ for (const update of plan.updates) {
2371
+ console.info(' ' + path.relative(plan.gameRoot, update.manifest)
2372
+ + ': ' + update.packageName + ' ' + update.current + ' -> ' + update.next);
2373
+ }
2374
+ if (plan.updates.length === 0) {
2375
+ console.info(' Already current for published npm latest tags.');
2376
+ }
2377
+ for (const note of plan.notes) {
2378
+ console.info('Note: ' + note);
2379
+ }
2380
+ for (const advisory of plan.targetAdvisories) {
2381
+ console.info('Target advisory [' + advisory.target + ']: ' + advisory.message);
2382
+ }
2383
+ for (const blocker of plan.blockers) {
2384
+ console.error('Blocked: ' + blocker);
2385
+ }
2386
+ if (result !== undefined) {
2387
+ console.info('Updated ' + result.manifests.length + ' manifest(s) and '
2388
+ + result.lockfiles.length + ' lockfile(s). Run target checks before release.');
2389
+ }
2390
+ else if (plan.updates.length > 0 && plan.blockers.length === 0) {
2391
+ console.info('Run again with --apply to update manifests and pnpm lockfiles.');
2392
+ }
2393
+ }
2394
+ if (plan.blockers.length > 0) {
2395
+ process.exitCode = 1;
2396
+ }
2397
+ },
2398
+ }),
2045
2399
  doctor: defineI18n({
2046
2400
  name: 'doctor',
2047
2401
  description: 'Print CLI, kit, template, and target-wrapper status.',
@@ -2060,7 +2414,7 @@ const kitCommand = defineI18n({
2060
2414
  }),
2061
2415
  },
2062
2416
  run: () => {
2063
- console.info('Use "mpgd kit doctor".');
2417
+ console.info('Use "mpgd kit doctor" or "mpgd kit upgrade --game <directory> [--apply]".');
2064
2418
  },
2065
2419
  });
2066
2420
  function commandResource(description, args = {}) {
@@ -2573,10 +2927,11 @@ function toJavaScriptStringLiteral(value) {
2573
2927
  function toTemplatePath(value) {
2574
2928
  return value.split(path.sep).join('/');
2575
2929
  }
2576
- function createTargetCommandEnv(values) {
2930
+ function createTargetCommandEnv(values, options = {}) {
2577
2931
  const targetsFile = path.resolve(readOptionalString(values['targets-file']) ?? 'mpgd.targets.json');
2578
2932
  const gameRoot = path.dirname(targetsFile);
2579
- const kitPath = resolveKitPathForTarget(values);
2933
+ const standaloneNative = resolveStandaloneNativeBuild(targetsFile, options.nativeBuildTarget, values);
2934
+ const kitPath = standaloneNative === undefined ? resolveKitPathForTarget(values) : gameRoot;
2580
2935
  const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
2581
2936
  const targetConfigExtensionsFile = resolveTargetConfigExtensionsFile(gameRoot, readOptionalString(values['target-config-extensions-file']));
2582
2937
  const preparedTargetsFile = preparePlatformTargetsFile({
@@ -2586,15 +2941,101 @@ function createTargetCommandEnv(values) {
2586
2941
  ? {}
2587
2942
  : { outputFile: path.resolve(resolvedTargetsFile) }),
2588
2943
  });
2589
- return {
2944
+ const env = {
2590
2945
  ...process.env,
2591
2946
  ...createGameOwnedReleaseEnv(gameRoot, process.env),
2592
- MPGD_KIT_PATH: kitPath,
2593
2947
  MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
2594
2948
  ...(targetConfigExtensionsFile === undefined
2595
2949
  ? {}
2596
2950
  : { [targetConfigExtensionsFileEnv]: targetConfigExtensionsFile }),
2597
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
+ };
2598
3039
  }
2599
3040
  function resolveTargetConfigExtensionsFile(gameRoot, configuredFile) {
2600
3041
  const inheritedFile = readConfiguredPath(process.env[targetConfigExtensionsFileEnv]);
@@ -2684,19 +3125,19 @@ function preparePlatformTargetsFile(input) {
2684
3125
  writeFileSync(outputFile, `${JSON.stringify(rendered, null, 2)}\n`);
2685
3126
  return outputFile;
2686
3127
  }
2687
- function readJsonForCli(file) {
3128
+ function readJsonForCli(file, label = 'targets file') {
2688
3129
  let raw;
2689
3130
  try {
2690
3131
  raw = readFileSync(file, 'utf8');
2691
3132
  }
2692
3133
  catch (error) {
2693
- throw new Error(`Failed to read targets file ${file}: ${formatError(error)}`);
3134
+ throw new Error(`Failed to read ${label} ${file}: ${formatError(error)}`);
2694
3135
  }
2695
3136
  try {
2696
3137
  return JSON.parse(raw);
2697
3138
  }
2698
3139
  catch (error) {
2699
- throw new Error(`Failed to parse targets file ${file}: ${formatError(error)}`);
3140
+ throw new Error(`Failed to parse ${label} ${file}: ${formatError(error)}`);
2700
3141
  }
2701
3142
  }
2702
3143
  function formatError(error) {
@@ -2728,12 +3169,38 @@ function runTargetCommand(input) {
2728
3169
  const target = normalizeBuildTarget(input.target, input.env);
2729
3170
  const env = withTargetVariantEnv(target, input.variant, input.env);
2730
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
+ }
2731
3180
  const args = input.action === 'build'
2732
3181
  ? ['build:target', target, profile]
2733
3182
  : ['smoke:target', target];
2734
3183
  console.info(`[mpgd] ${input.action} ${target}`);
2735
3184
  runPnpm(args, env);
2736
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
+ }
2737
3204
  function runTargetMatrix(input) {
2738
3205
  for (const target of input.targets) {
2739
3206
  const variant = variantForTarget(target, input);
@@ -3019,6 +3486,9 @@ function renderHostedPwaVerificationMarkdown(verification) {
3019
3486
  '',
3020
3487
  ].join('\n');
3021
3488
  }
3489
+ function isDeployApproved(plan, approveFlag) {
3490
+ return plan.approval === 'preapproved-internal-test' || approveFlag;
3491
+ }
3022
3492
  function readRequiredCliOption(value, label) {
3023
3493
  const read = readOptionalString(value);
3024
3494
  if (read === undefined) {