@mpgd/cli 0.46.1 → 0.48.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 (62) hide show
  1. package/dist/capacitor-shell-starter.d.ts +57 -1
  2. package/dist/capacitor-shell-starter.js +311 -34
  3. package/dist/command-launcher.d.ts +13 -0
  4. package/dist/command-launcher.js +24 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +149 -12
  7. package/dist/microsoft-store-submission.js +11 -6
  8. package/dist/native-build-info.json +2 -2
  9. package/dist/native-build-target.js +2413 -345
  10. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  11. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  12. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  13. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  14. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  15. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  16. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  17. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  18. package/dist/windows-host/README.md +118 -0
  19. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  20. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  21. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  22. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  23. package/dist/windows-native-tools.d.ts +4 -0
  24. package/dist/windows-native-tools.js +21 -0
  25. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  26. package/package.json +24 -14
  27. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  28. package/templates/lite-overlay/AGENTS.md +27 -0
  29. package/templates/lite-overlay/README.md +37 -0
  30. package/templates/lite-overlay/agent/acceptance.md +27 -0
  31. package/templates/lite-overlay/agent/brief.md +17 -0
  32. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  33. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  34. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  35. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  36. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  37. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  38. package/templates/phaser-game/README.md +5 -3
  39. package/templates/phaser-game/apps/target-ait/package.json +2 -2
  40. package/templates/phaser-game/apps/target-ait/src/main.ts +17 -0
  41. package/templates/phaser-game/apps/target-ait/src/runtime-config.types.ts +2 -0
  42. package/templates/phaser-game/apps/target-ait/vite.config.ts +27 -0
  43. package/templates/phaser-game/apps/target-devvit/package.json +2 -1
  44. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  45. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +35 -1
  46. package/templates/phaser-game/package.json +3 -1
  47. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  48. package/templates/phaser-game/src/env.d.ts +2 -0
  49. package/templates/phaser-game/src/main.ts +8 -1
  50. package/templates/phaser-game/src/platform/buildGateways/ait.ts +9 -0
  51. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +13 -1
  52. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +51 -2
  53. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +51 -2
  54. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +59 -26
  55. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +36 -1
  56. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +39 -2
  57. package/templates/phaser-game/src/platform/commerceAccountBinding.ts +79 -0
  58. package/templates/phaser-game/src/platform/commerceProducts.ts +69 -0
  59. package/templates/phaser-game/src/platform/gameServices.ts +22 -2
  60. package/templates/phaser-game/src/platform/monetizationJournal.ts +286 -0
  61. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +22 -0
  62. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -2,14 +2,61 @@ interface PlannedFile {
2
2
  readonly path: string;
3
3
  readonly content: string;
4
4
  }
5
+ /**
6
+ * A provider id that names a kit native store plugin. `--providers` installs
7
+ * the plugin package into the game-owned shell (where `cap add` or `cap sync`
8
+ * registers its native code), declares it in the game (where the Vite bundle
9
+ * imports it from the gateway file), and wires the gateway when the game still
10
+ * has the unwired template gateway; any other id stays selection metadata.
11
+ */
12
+ export interface CapacitorShellProviderPlugin {
13
+ readonly id: string;
14
+ readonly packageName: string;
15
+ /** The plugin's directory in a kit checkout, read when the game has not installed it yet. */
16
+ readonly packageDir: string;
17
+ /** The native platform, which is also the canonical `mpgd.targets.json` target name. */
18
+ readonly platform: 'android' | 'ios';
19
+ /** The game gateway file expected to register the plugin's service provider. */
20
+ readonly gatewayFile: string;
21
+ readonly gatewayFactory: string;
22
+ /**
23
+ * Gateway sources the initializer may replace with the template's wired
24
+ * gateway: the unwired gateway earlier templates generated. Any other
25
+ * source is game-owned and only checked.
26
+ */
27
+ readonly legacyGatewaySources: readonly string[];
28
+ }
29
+ export declare const capacitorShellProviderPlugins: readonly CapacitorShellProviderPlugin[];
30
+ /**
31
+ * Game modules the wired template gateways import. They are written next to a
32
+ * replaced legacy gateway when missing; a game-owned copy that differs from
33
+ * the template keeps the legacy gateway in place.
34
+ */
35
+ export declare const capacitorShellProviderSupportFiles: readonly string[];
36
+ /** A plugin the plan installs, with the exact version the shell and the game pin. */
37
+ export interface CapacitorShellPlannedProviderPlugin extends CapacitorShellProviderPlugin {
38
+ readonly version: string;
39
+ /** `installed`: the game's copy; `kit`: the version this CLI release ships with. */
40
+ readonly versionSource: 'installed' | 'kit';
41
+ }
5
42
  export interface CapacitorShellStarterInput {
6
43
  readonly gameRoot: string;
7
44
  readonly appId: string;
8
45
  readonly displayName: string;
9
46
  readonly iconSource?: string;
10
47
  readonly backendUrl?: string;
11
- /** Selection metadata only; a provider SDK is not installed or enabled here. */
48
+ /**
49
+ * Requested provider ids. The ids in `capacitorShellProviderPlugins`
50
+ * (`play-billing`, `storekit`) install their native plugin package into the
51
+ * shell and the game; other ids are recorded as selection metadata only.
52
+ */
12
53
  readonly providerIds?: readonly string[];
54
+ /**
55
+ * The CLI's game template. With it a legacy, unwired Android or iOS gateway
56
+ * is replaced by the template's store-provider wiring; without it the plan
57
+ * only checks the gateway and warns.
58
+ */
59
+ readonly templateRoot?: string;
13
60
  }
14
61
  export interface CapacitorShellStarterPlan {
15
62
  readonly gameRoot: string;
@@ -17,6 +64,15 @@ export interface CapacitorShellStarterPlan {
17
64
  readonly changedFiles: readonly string[];
18
65
  readonly nativePlatformsToAdd: readonly ('android' | 'ios')[];
19
66
  readonly files: readonly PlannedFile[];
67
+ /** Native store plugins the plan installs into the shell and declares in the game. */
68
+ readonly providerPlugins: readonly CapacitorShellPlannedProviderPlugin[];
69
+ /**
70
+ * Store wiring the plan could not write or confirm: a missing or game-owned
71
+ * gateway that does not register the plugin, or a store plugin the shell
72
+ * keeps although it is no longer requested. Printed; game source is never
73
+ * rewritten for these.
74
+ */
75
+ readonly warnings: readonly string[];
20
76
  }
21
77
  /**
22
78
  * Produce the complete game-owned shell change list before writing anything.
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { DOMParser } from '@xmldom/xmldom';
7
+ import { satisfies, valid } from 'semver';
7
8
  import { isNonPublicServiceHostname } from './production-target-readiness.js';
8
9
  import { assertAndroidSettingsAppProject, assertAndroidSettingsNoAppRemap, assertIosReleaseInfoPlistExpansion, assertIosReleasePackagingSettings, assertIosReleaseProductName, assertNativeShellIdentity, hasAndroidDisplayNameResourceOverride, hasAndroidManifestSourceSetOverride, hasAndroidResourceSourceSetOverride, hasGradleIdentityMutation, hasGradleTaskAction, maskGradleStrings, readIosAppTargetId, readIosReleaseInfoPlist, stripGradleComments, } from './native-shell-identity.js';
9
10
  const shellPath = 'apps/mobile-capacitor';
@@ -18,6 +19,50 @@ const javaKeywords = new Set(`
18
19
  synchronized this throw throws transient try void volatile while true false null record sealed
19
20
  permits var yield
20
21
  `.trim().split(/\s+/u));
22
+ const unwiredCapacitorGateway = (target) => [
23
+ "import { createCapacitorPlatformGateway } from '@mpgd/adapter-capacitor';",
24
+ "import type { PlatformGateway } from '@mpgd/platform';",
25
+ '',
26
+ "import type { RuntimeConfig } from '../runtimeDetector';",
27
+ '',
28
+ 'export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {',
29
+ ' return createCapacitorPlatformGateway({',
30
+ ` target: '${target}',`,
31
+ ' appVersion: runtime.appVersion,',
32
+ ' buildId: runtime.buildId,',
33
+ ' });',
34
+ '}',
35
+ '',
36
+ ].join('\n');
37
+ export const capacitorShellProviderPlugins = Object.freeze([
38
+ {
39
+ id: 'play-billing',
40
+ packageName: '@mpgd/capacitor-play-billing',
41
+ packageDir: 'native-plugins/capacitor-play-billing',
42
+ platform: 'android',
43
+ gatewayFile: 'src/platform/buildGateways/capacitorAndroid.ts',
44
+ gatewayFactory: 'createCapacitorPlayBillingServiceProvider',
45
+ legacyGatewaySources: [unwiredCapacitorGateway('android')],
46
+ },
47
+ {
48
+ id: 'storekit',
49
+ packageName: '@mpgd/capacitor-storekit',
50
+ packageDir: 'native-plugins/capacitor-storekit',
51
+ platform: 'ios',
52
+ gatewayFile: 'src/platform/buildGateways/capacitorIos.ts',
53
+ gatewayFactory: 'createCapacitorStoreKitServiceProvider',
54
+ legacyGatewaySources: [unwiredCapacitorGateway('ios')],
55
+ },
56
+ ]);
57
+ /**
58
+ * Game modules the wired template gateways import. They are written next to a
59
+ * replaced legacy gateway when missing; a game-owned copy that differs from
60
+ * the template keeps the legacy gateway in place.
61
+ */
62
+ export const capacitorShellProviderSupportFiles = Object.freeze([
63
+ 'src/platform/commerceProducts.ts',
64
+ 'src/platform/commerceAccountBinding.ts',
65
+ ]);
21
66
  /**
22
67
  * Produce the complete game-owned shell change list before writing anything.
23
68
  * Existing Android/iOS projects, signing files, and custom controllers are
@@ -107,6 +152,13 @@ export function planCapacitorShellStarter(input) {
107
152
  const cliDevelopmentDependencies = requireObject(cliPackage.devDependencies, 'CLI dev dependencies');
108
153
  const typescriptVersion = requireString(cliDevelopmentDependencies.typescript, 'CLI TypeScript version');
109
154
  const targetMap = requireObject(targets.targets, 'targets');
155
+ for (const plugin of capacitorShellProviderPlugins) {
156
+ if (providerIds.includes(plugin.id) && targetMap[plugin.platform] === undefined) {
157
+ throw new Error(`--providers ${plugin.id} installs the ${plugin.platform} store plugin into the native `
158
+ + `shell, but mpgd.targets.json has no ${plugin.platform} target. Add the `
159
+ + `capacitor-${plugin.platform} target before requesting store providers.`);
160
+ }
161
+ }
110
162
  for (const [targetName, kind, identityKey] of [
111
163
  ['android', 'capacitor-android', 'packageId'],
112
164
  ['ios', 'capacitor-ios', 'bundleId'],
@@ -174,11 +226,41 @@ export function planCapacitorShellStarter(input) {
174
226
  }
175
227
  const requestedProviderIds = input.providerIds === undefined
176
228
  && Array.isArray(existingManifest?.requestedProviderIds)
177
- ? existingManifest.requestedProviderIds : providerIds;
229
+ ? existingManifest.requestedProviderIds.filter((id) => typeof id === 'string')
230
+ : providerIds;
178
231
  const requestedBackendUrl = backendUrl ?? (typeof existingManifest?.backendUrl === 'string'
179
232
  ? existingManifest.backendUrl : undefined);
233
+ // Store plugins are pinned to one exact version in the shell and the game:
234
+ // the game's installed copy when it has one, otherwise the version this CLI
235
+ // release ships with, which the plan adds to the game as well. The Vite
236
+ // bundle imports the plugin from the gateway file, and its native code must
237
+ // match that JavaScript.
238
+ const providerPlugins = capacitorShellProviderPlugins
239
+ .filter((plugin) => requestedProviderIds.includes(plugin.id))
240
+ .map((plugin) => resolveProviderPlugin(gameRoot, gameDependencies, cliDevelopmentDependencies, plugin));
241
+ const warnings = [];
242
+ const gatewayFiles = planProviderGatewayWiring(gameRoot, input.templateRoot, providerPlugins, warnings);
243
+ if (packageContent !== undefined) {
244
+ const existingShellDependencies = requireObject(parseJsonObject(packageContent, 'existing shell package').dependencies, 'existing shell dependencies');
245
+ for (const plugin of capacitorShellProviderPlugins) {
246
+ if (existingShellDependencies[plugin.packageName] !== undefined
247
+ && !providerPlugins.some((planned) => planned.id === plugin.id)) {
248
+ warnings.push(`The shell still lists ${plugin.packageName} although ${plugin.id} is no longer `
249
+ + 'requested; the initializer never removes a store plugin, so remove it from the '
250
+ + 'shell package and resync if the game stopped selling there.');
251
+ }
252
+ }
253
+ }
254
+ const shellDependencies = {
255
+ '@capacitor/android': capacitorVersion,
256
+ '@capacitor/app': appVersion,
257
+ '@capacitor/core': capacitorVersion,
258
+ '@capacitor/ios': capacitorVersion,
259
+ '@mpgd/capacitor-game-services': pluginVersion,
260
+ ...Object.fromEntries(providerPlugins.map((plugin) => [plugin.packageName, plugin.version])),
261
+ };
180
262
  if (packageContent !== undefined) {
181
- assertCompatibleExistingPackage(packageContent, capacitorVersion, appVersion, pluginVersion, typescriptVersion);
263
+ assertCompatibleExistingPackage(packageContent, shellDependencies, capacitorVersion, typescriptVersion);
182
264
  }
183
265
  if (configContent !== undefined) {
184
266
  const safeConfig = requireStaticCapacitorConfig(configContent);
@@ -205,28 +287,24 @@ export function planCapacitorShellStarter(input) {
205
287
  { path: 'mpgd.targets.json', content: json(targets) },
206
288
  {
207
289
  path: `${shellPath}/package.json`,
208
- content: packageContent ?? json({
209
- name: `${gamePackageName}-native-shell`,
210
- private: true,
211
- version: '0.0.0',
212
- type: 'module',
213
- scripts: {
214
- cap: 'cap',
215
- 'sync:android': 'cap sync android',
216
- 'sync:ios': 'cap sync ios',
217
- },
218
- dependencies: {
219
- '@capacitor/android': capacitorVersion,
220
- '@capacitor/app': appVersion,
221
- '@capacitor/core': capacitorVersion,
222
- '@capacitor/ios': capacitorVersion,
223
- '@mpgd/capacitor-game-services': pluginVersion,
224
- },
225
- devDependencies: {
226
- '@capacitor/cli': capacitorVersion,
227
- typescript: typescriptVersion,
228
- },
229
- }),
290
+ content: packageContent === undefined
291
+ ? json({
292
+ name: `${gamePackageName}-native-shell`,
293
+ private: true,
294
+ version: '0.0.0',
295
+ type: 'module',
296
+ scripts: {
297
+ cap: 'cap',
298
+ 'sync:android': 'cap sync android',
299
+ 'sync:ios': 'cap sync ios',
300
+ },
301
+ dependencies: shellDependencies,
302
+ devDependencies: {
303
+ '@capacitor/cli': capacitorVersion,
304
+ typescript: typescriptVersion,
305
+ },
306
+ })
307
+ : withPackageDependencies(packageContent, providerPlugins),
230
308
  },
231
309
  {
232
310
  path: `${shellPath}/capacitor.config.ts`,
@@ -253,6 +331,18 @@ export function planCapacitorShellStarter(input) {
253
331
  appId: input.appId,
254
332
  displayName: input.displayName,
255
333
  requestedProviderIds,
334
+ ...(providerPlugins.length === 0
335
+ ? {}
336
+ : {
337
+ providers: Object.fromEntries(providerPlugins.map((plugin) => [
338
+ plugin.id,
339
+ {
340
+ package: plugin.packageName,
341
+ version: plugin.version,
342
+ platform: plugin.platform,
343
+ },
344
+ ])),
345
+ }),
256
346
  ...(requestedBackendUrl === undefined ? {} : { backendUrl: requestedBackendUrl }),
257
347
  }),
258
348
  },
@@ -269,8 +359,11 @@ export function planCapacitorShellStarter(input) {
269
359
  'Existing native files, signing settings, and custom',
270
360
  'ViewControllers are never replaced by mpgd target init capacitor.',
271
361
  '',
272
- 'requestedProviderIds is selection metadata only: it does not install an',
273
- 'SDK or report a feature as available. Register provider modules explicitly.',
362
+ 'requestedProviderIds is selection metadata, except play-billing and',
363
+ 'storekit: those install the kit store plugin into this shell and the game',
364
+ 'and register it with cap sync. A plugin never reports a feature as',
365
+ 'available on its own; the gateway registers the store provider, and the',
366
+ 'store answers configuration-required until the plugin and products exist.',
274
367
  'Production builds require a real game-owned backend when grants are enabled.',
275
368
  '',
276
369
  ].join('\n'),
@@ -318,6 +411,24 @@ export function planCapacitorShellStarter(input) {
318
411
  : current,
319
412
  });
320
413
  }
414
+ requestedFiles.push(...gatewayFiles);
415
+ // The game bundle imports the plugin from its gateway file, so the game
416
+ // declares it too, at the shell's exact version; the materializer's game
417
+ // install picks it up.
418
+ const missingGameDependencies = providerPlugins.filter((plugin) => gameDependencies[plugin.packageName] === undefined);
419
+ if (missingGameDependencies.length > 0) {
420
+ safeDestination(gameRoot, 'package.json');
421
+ requestedFiles.push({
422
+ path: 'package.json',
423
+ content: json({
424
+ ...gamePackage,
425
+ dependencies: {
426
+ ...gameDependencies,
427
+ ...Object.fromEntries(missingGameDependencies.map((plugin) => [plugin.packageName, plugin.version])),
428
+ },
429
+ }),
430
+ });
431
+ }
321
432
  const nativePlatformsToAdd = ['android', 'ios'].filter((platform) => {
322
433
  const nativeDirectory = path.join(gameRoot, shellPath, platform);
323
434
  if (!existsSync(nativeDirectory)) {
@@ -342,6 +453,8 @@ export function planCapacitorShellStarter(input) {
342
453
  files,
343
454
  changedFiles: files.map((file) => file.path),
344
455
  nativePlatformsToAdd,
456
+ providerPlugins,
457
+ warnings,
345
458
  };
346
459
  }
347
460
  export function assertCapacitorAppId(appId) {
@@ -472,6 +585,15 @@ export function materializeCapacitorShellStarter(plan, runner = { run: runComman
472
585
  }
473
586
  assertNativePlatformComplete(plan.gameRoot, platform, requireString(manifest.appId, 'shell app ID'), requireString(manifest.displayName, 'shell display name'));
474
587
  }
588
+ // `cap add` syncs a new project itself; a preserved project needs an explicit
589
+ // sync so the store plugin's native code joins the shell.
590
+ for (const platform of ['android', 'ios']) {
591
+ if (plan.providerPlugins.some((plugin) => plugin.platform === platform)
592
+ && !plan.nativePlatformsToAdd.includes(platform)
593
+ && existsSync(path.join(plan.gameRoot, shellPath, platform))) {
594
+ runner.run('pnpm', ['--dir', shellPath, 'cap', 'sync', platform], plan.gameRoot);
595
+ }
596
+ }
475
597
  const afterInstall = planCapacitorShellStarter(plan.input);
476
598
  if (afterInstall.changedFiles.length > 0 || afterInstall.nativePlatformsToAdd.length > 0) {
477
599
  throw new Error('Capacitor shell changed during installation; rerun the initializer.');
@@ -1915,19 +2037,24 @@ function runCommand(command, args, cwd) {
1915
2037
  throw new Error(`${command} ${args.join(' ')} failed. The shell files remain for a safe retry.`);
1916
2038
  }
1917
2039
  }
1918
- function assertCompatibleExistingPackage(source, capacitorVersion, appVersion, pluginVersion, typescriptVersion) {
2040
+ function assertCompatibleExistingPackage(source, shellDependencies, capacitorVersion, typescriptVersion) {
1919
2041
  const value = parseJsonObject(source, 'existing shell package');
1920
2042
  if (value.private !== true) {
1921
2043
  throw new Error('Existing Capacitor shell package must stay private.');
1922
2044
  }
1923
2045
  const dependencies = requireObject(value.dependencies, 'existing shell dependencies');
1924
- for (const [name, version] of Object.entries({
1925
- '@capacitor/android': capacitorVersion,
1926
- '@capacitor/app': appVersion,
1927
- '@capacitor/core': capacitorVersion,
1928
- '@capacitor/ios': capacitorVersion,
1929
- '@mpgd/capacitor-game-services': pluginVersion,
1930
- })) {
2046
+ const providerPackages = new Set(capacitorShellProviderPlugins.map((plugin) => plugin.packageName));
2047
+ for (const [name, version] of Object.entries(shellDependencies)) {
2048
+ // A store plugin the shell does not list yet is added by the plan. One it
2049
+ // lists at another version would compile native code that does not match
2050
+ // the game's JavaScript, so it is refused like any other pinned package.
2051
+ if (providerPackages.has(name) && dependencies[name] === undefined) {
2052
+ continue;
2053
+ }
2054
+ if (providerPackages.has(name) && dependencies[name] !== version) {
2055
+ throw new Error(`Existing shell ${name} is ${String(dependencies[name])} but the game uses ${version}; `
2056
+ + 'align the shell package with the game before rerunning.');
2057
+ }
1931
2058
  if (dependencies[name] !== version) {
1932
2059
  throw new Error(`Existing shell ${name} differs; refusing to rewrite dependencies.`);
1933
2060
  }
@@ -2209,6 +2336,156 @@ function assertNotSymlink(file) {
2209
2336
  }
2210
2337
  }
2211
2338
  }
2339
+ /**
2340
+ * Adds the requested store plugins to an existing shell package, keeping every
2341
+ * other field as the game wrote it. The content is unchanged when nothing is
2342
+ * missing, so an untouched shell stays out of the change list.
2343
+ */
2344
+ function withPackageDependencies(source, plugins) {
2345
+ const value = parseJsonObject(source, 'existing shell package');
2346
+ const dependencies = requireObject(value.dependencies, 'existing shell dependencies');
2347
+ const missing = plugins.filter((plugin) => dependencies[plugin.packageName] === undefined);
2348
+ if (missing.length === 0) {
2349
+ return source;
2350
+ }
2351
+ return json({
2352
+ ...value,
2353
+ dependencies: {
2354
+ ...dependencies,
2355
+ ...Object.fromEntries(missing.map((plugin) => [plugin.packageName, plugin.version])),
2356
+ },
2357
+ });
2358
+ }
2359
+ /**
2360
+ * Resolves one requested store plugin to the exact version the shell and the
2361
+ * game pin: the game's installed copy, or, when the game does not declare the
2362
+ * plugin yet, the version this CLI release ships with (its published dev
2363
+ * dependency, or the plugin package of the kit checkout it runs from). A
2364
+ * declared but uninstalled plugin, or one whose peers the game's installed
2365
+ * kit packages do not satisfy, is refused before anything is written.
2366
+ */
2367
+ function resolveProviderPlugin(gameRoot, gameDependencies, cliDevelopmentDependencies, plugin) {
2368
+ const installedFile = path.join(gameRoot, 'node_modules', plugin.packageName, 'package.json');
2369
+ let version;
2370
+ let versionSource;
2371
+ let peerDependencies;
2372
+ if (existsSync(installedFile)) {
2373
+ const installed = readJsonObject(installedFile);
2374
+ version = requireString(installed.version, `installed ${plugin.packageName} version`);
2375
+ versionSource = 'installed';
2376
+ peerDependencies = installed.peerDependencies;
2377
+ }
2378
+ else if (gameDependencies[plugin.packageName] !== undefined) {
2379
+ throw new Error(`The game declares ${plugin.packageName} but it is not installed; install game `
2380
+ + 'dependencies before initializing the shell.');
2381
+ }
2382
+ else {
2383
+ const kitPackageFile = fileURLToPath(new URL(`../../../${plugin.packageDir}/package.json`, import.meta.url));
2384
+ const kitPackage = existsSync(kitPackageFile) ? readJsonObject(kitPackageFile) : undefined;
2385
+ const published = cliDevelopmentDependencies[plugin.packageName];
2386
+ const known = typeof published === 'string' && valid(published) !== null
2387
+ ? published
2388
+ : kitPackage?.name === plugin.packageName
2389
+ ? kitPackage.version
2390
+ : undefined;
2391
+ if (typeof known !== 'string') {
2392
+ throw new Error(`Provider ${plugin.id} needs ${plugin.packageName}, and this CLI does not know which `
2393
+ + `version to install; add it to the game (pnpm add ${plugin.packageName}) and rerun.`);
2394
+ }
2395
+ version = known;
2396
+ versionSource = 'kit';
2397
+ peerDependencies = kitPackage?.peerDependencies;
2398
+ }
2399
+ if (valid(version) === null || version.includes('+')) {
2400
+ throw new Error(`${plugin.packageName} must resolve to a concrete version, not ${version}.`);
2401
+ }
2402
+ if (typeof peerDependencies === 'object' && peerDependencies !== null) {
2403
+ // Every declared peer the game has installed must satisfy the plugin's
2404
+ // range (Capacitor core, the bridge, the adapter, and the platform alike),
2405
+ // so an incompatible host is refused before the shell or gateway is written.
2406
+ for (const [peer, range] of Object.entries(peerDependencies)) {
2407
+ const peerFile = path.join(gameRoot, 'node_modules', peer, 'package.json');
2408
+ if (typeof range !== 'string' || !existsSync(peerFile)) {
2409
+ continue;
2410
+ }
2411
+ const peerVersion = readJsonObject(peerFile).version;
2412
+ if (typeof peerVersion === 'string' && valid(peerVersion) !== null
2413
+ && !satisfies(peerVersion, range, { includePrerelease: true })) {
2414
+ throw new Error(`Provider ${plugin.id} (${plugin.packageName}@${version}) needs ${peer} ${range}, but `
2415
+ + `the game has ${peerVersion}; upgrade the game's kit packages (mpgd kit upgrade) first.`);
2416
+ }
2417
+ }
2418
+ }
2419
+ return { ...plugin, version, versionSource };
2420
+ }
2421
+ /**
2422
+ * Plans the gateway wiring for the requested store plugins. A gateway that
2423
+ * already imports the plugin is left alone; a gateway that is still the
2424
+ * unwired template gateway is replaced by the template's wired gateway, with
2425
+ * the support modules it imports written when the game has none; every other
2426
+ * gateway is game-owned, so the plan only warns. Nothing here can enable a
2427
+ * purchase: the wired gateway registers the store provider, and grants still
2428
+ * come from the backend ledger through the reconciler.
2429
+ */
2430
+ function planProviderGatewayWiring(gameRoot, templateRoot, plugins, warnings) {
2431
+ const planned = new Map();
2432
+ for (const plugin of plugins) {
2433
+ const current = readExisting(safeDestination(gameRoot, plugin.gatewayFile));
2434
+ // Wired means the gateway calls the plugin's service-provider factory; a
2435
+ // type-only or unused import of the package does not register anything.
2436
+ if (current !== undefined && new RegExp(`\\b${plugin.gatewayFactory}\\s*\\(`, 'u').test(current)) {
2437
+ continue;
2438
+ }
2439
+ const register = `register ${plugin.gatewayFactory} from ${plugin.packageName} there so the `
2440
+ + 'shell plugin is used';
2441
+ if (current === undefined) {
2442
+ warnings.push(`${plugin.gatewayFile} is missing; ${register}.`);
2443
+ continue;
2444
+ }
2445
+ const legacy = plugin.legacyGatewaySources.includes(current.replace(/\r\n/gu, '\n'));
2446
+ if (!legacy || templateRoot === undefined) {
2447
+ warnings.push(`${plugin.gatewayFile} does not call ${plugin.gatewayFactory}; ${register}.`);
2448
+ continue;
2449
+ }
2450
+ const support = new Map();
2451
+ let kept;
2452
+ for (const file of capacitorShellProviderSupportFiles) {
2453
+ const existing = readExisting(safeDestination(gameRoot, file));
2454
+ const template = readTemplateFile(templateRoot, file);
2455
+ if (existing === undefined) {
2456
+ support.set(file, template);
2457
+ }
2458
+ else if (existing !== template) {
2459
+ kept = file;
2460
+ break;
2461
+ }
2462
+ }
2463
+ if (kept !== undefined) {
2464
+ warnings.push(`${plugin.gatewayFile} was not rewritten because the game-owned ${kept} differs from `
2465
+ + `the template; ${register}.`);
2466
+ continue;
2467
+ }
2468
+ planned.set(plugin.gatewayFile, readTemplateFile(templateRoot, plugin.gatewayFile));
2469
+ for (const [file, content] of support) {
2470
+ planned.set(file, content);
2471
+ }
2472
+ }
2473
+ return [...planned].map(([file, content]) => ({ path: file, content }));
2474
+ }
2475
+ /** A regular file of the CLI's game template, read without leaving the template root. */
2476
+ function readTemplateFile(templateRoot, relative) {
2477
+ const root = realpathSync(templateRoot);
2478
+ const file = path.resolve(root, relative);
2479
+ const within = path.relative(root, file);
2480
+ if (within === '' || within.startsWith('..') || path.isAbsolute(within)) {
2481
+ throw new Error(`Game template path escapes the template root: ${relative}`);
2482
+ }
2483
+ if (!existsSync(file) || !lstatSync(file).isFile()
2484
+ || path.relative(root, realpathSync(file)).startsWith('..')) {
2485
+ throw new Error(`Game template file is missing: ${relative}`);
2486
+ }
2487
+ return readFileSync(file, 'utf8');
2488
+ }
2212
2489
  function resolveInstalledPluginVersion(gameRoot, requirement) {
2213
2490
  const adapterRoot = path.dirname(realpathSync(path.join(gameRoot, 'node_modules/@mpgd/adapter-capacitor/package.json')));
2214
2491
  const installedPackage = path.join(adapterRoot, 'node_modules/@mpgd/capacitor-game-services/package.json');
@@ -0,0 +1,13 @@
1
+ export interface NativeCommandLaunch {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ readonly shell?: boolean;
5
+ }
6
+ /** Launch pnpm directly; Windows .cmd shims require a shell and lose argument boundaries. */
7
+ export declare function resolveNativeCommandLaunch(input: {
8
+ readonly command: string;
9
+ readonly args: readonly string[];
10
+ readonly environment: NodeJS.ProcessEnv;
11
+ readonly platform?: NodeJS.Platform;
12
+ readonly nodeExecutable?: string;
13
+ }): NativeCommandLaunch;
@@ -0,0 +1,24 @@
1
+ /** Launch pnpm directly; Windows .cmd shims require a shell and lose argument boundaries. */
2
+ export function resolveNativeCommandLaunch(input) {
3
+ if ((input.platform ?? process.platform) !== 'win32') {
4
+ return { command: input.command, args: input.args };
5
+ }
6
+ if (input.command === './gradlew') {
7
+ return { command: 'gradlew.bat', args: input.args, shell: true };
8
+ }
9
+ if (input.command !== 'pnpm') {
10
+ return { command: input.command, args: input.args };
11
+ }
12
+ const pnpmEntry = input.environment.npm_execpath;
13
+ const pnpmBase = pnpmEntry?.split(/[\\/]/u).at(-1) ?? '';
14
+ if (pnpmEntry === undefined || !/^pnpm(?:\.exe|\.[cm]?js)$/iu.test(pnpmBase)) {
15
+ throw new Error('Windows builds require pnpm exec mpgd so npm_execpath resolves the pnpm executable or JavaScript entrypoint.');
16
+ }
17
+ if (/\.exe$/iu.test(pnpmEntry)) {
18
+ return { command: pnpmEntry, args: input.args };
19
+ }
20
+ return {
21
+ command: input.nodeExecutable ?? process.execPath,
22
+ args: [pnpmEntry, ...input.args],
23
+ };
24
+ }
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupporte
15
15
  export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, type OfflinePlaytestEvidence, type RunOfflinePlaytestPackagingInput, type RunOfflinePlaytestPackagingResult, } from './offline-playtest.js';
16
16
  export { assertMicrosoftStorePwaCacheTransition, assertMicrosoftStorePwaPrecacheUrl, assertMicrosoftStorePwaReleaseEvidence, inspectMicrosoftStorePwaBrowserCacheTransition, microsoftStorePwaCacheSchema, readMicrosoftStorePwaBrowserReleaseEvidence, requestMicrosoftStorePwaBrowserUpdate, resolveMicrosoftStorePwaScopedCacheName, type AssertMicrosoftStorePwaCacheTransitionInput, type InspectMicrosoftStorePwaCacheTransitionInput, type MicrosoftStorePwaBrowserPage, type MicrosoftStorePwaCachedIndexObservation, type MicrosoftStorePwaCacheTransitionEvidence, type MicrosoftStorePwaCacheTransitionObservation, type MicrosoftStorePwaReleaseEvidence, } from './microsoft-store-pwa-e2e.js';
17
17
  export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, type CreateMicrosoftStorePackageAcceptanceRuntimeInput, type MicrosoftStorePackageAcceptanceEvidence, type MicrosoftStorePackageAcceptanceRuntime, type MicrosoftStorePackageIdentity, type RunMicrosoftStorePackageAcceptanceInput, } from './microsoft-store-package-acceptance.js';
18
+ export { getMicrosoftStoreNativeAcceptanceModulePath, getMicrosoftStoreNativeHostSourceDirectory, } from './windows-native-tools.js';
18
19
  export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, type CreateMicrosoftStorePackageGenerationRuntimeInput, type MicrosoftStoreAddressResolver, type MicrosoftStorePackageGenerationEvidence, type MicrosoftStorePackageGenerationRuntime, type MicrosoftStoreResolvedAddress, type RunMicrosoftStorePackageGenerationInput, } from './microsoft-store-package-generation.js';
19
20
  export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, type MicrosoftStoreSubmissionConfig, type MicrosoftStoreSubmissionCommerce, type MicrosoftStoreSubmissionCommerceProduct, type MicrosoftStoreSubmissionEvidence, type RunMicrosoftStoreSubmissionPreflightInput, } from './microsoft-store-submission.js';
20
21
  export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';