@mpgd/cli 0.47.0 → 0.49.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 (59) hide show
  1. package/dist/command-launcher.d.ts +13 -0
  2. package/dist/command-launcher.js +24 -0
  3. package/dist/devvit-output-collisions.d.ts +49 -0
  4. package/dist/devvit-output-collisions.js +120 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +186 -10
  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 +2134 -351
  10. package/dist/tooling-migration.d.ts +24 -0
  11. package/dist/tooling-migration.js +497 -0
  12. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  13. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  14. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  15. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  16. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  17. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  18. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  19. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  20. package/dist/windows-host/README.md +118 -0
  21. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  22. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  23. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  24. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  25. package/dist/windows-native-tools.d.ts +4 -0
  26. package/dist/windows-native-tools.js +21 -0
  27. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  28. package/package.json +30 -18
  29. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  30. package/templates/lite-overlay/AGENTS.md +32 -0
  31. package/templates/lite-overlay/README.md +37 -0
  32. package/templates/lite-overlay/agent/acceptance.md +27 -0
  33. package/templates/lite-overlay/agent/brief.md +17 -0
  34. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  35. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  36. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  37. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  38. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  39. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  40. package/templates/phaser-game/AGENTS.md +5 -0
  41. package/templates/phaser-game/README.md +5 -3
  42. package/templates/phaser-game/apps/target-ait/package.json +4 -4
  43. package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
  44. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
  45. package/templates/phaser-game/apps/target-devvit/package.json +2 -2
  46. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  47. package/templates/phaser-game/apps/target-devvit/vite.config.ts +2 -0
  48. package/templates/phaser-game/package.json +8 -8
  49. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  50. package/templates/phaser-game/src/env.d.ts +2 -0
  51. package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
  52. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
  53. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
  54. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
  55. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
  56. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
  57. package/templates/phaser-game/tools/{run-game-acceptance.mjs → run-game-acceptance.ts} +1 -8
  58. package/templates/phaser-game/tsconfig.tools.json +12 -0
  59. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -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
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * One output path written by more than one source in a Devvit web build.
3
+ *
4
+ * `public` means a file from a public directory was silently replaced by a
5
+ * Vite-emitted file of the same name (Vite copies `publicDir` first and then
6
+ * writes the bundle over it). `case` means two output files differ only by
7
+ * letter case. The build plugin checks bundle names before writing so this
8
+ * also works on case-insensitive filesystems.
9
+ */
10
+ export interface DevvitOutputCollision {
11
+ readonly kind: 'public' | 'case';
12
+ readonly path: string;
13
+ readonly sources: readonly string[];
14
+ }
15
+ /** Validate logical output names before the filesystem can collapse them. */
16
+ export declare function assertDevvitOutputNames(bundleNames: readonly string[], publicNames: readonly string[], outDir: string): void;
17
+ /** Shared Vite 7/8 guard for production and staging watch builds. */
18
+ export declare function devvitOutputCollisionPlugin(): {
19
+ name: string;
20
+ apply: "build";
21
+ enforce: "post";
22
+ configResolved(config: {
23
+ publicDir: string;
24
+ }): void;
25
+ generateBundle: {
26
+ order: "post";
27
+ handler(this: {
28
+ environment: {
29
+ config: {
30
+ build: {
31
+ copyPublicDir: boolean;
32
+ outDir: string;
33
+ };
34
+ };
35
+ };
36
+ }, options: {
37
+ dir?: string | undefined;
38
+ }, bundle: Record<string, {
39
+ fileName: string;
40
+ }>): void;
41
+ };
42
+ };
43
+ /**
44
+ * Finds output files of a Devvit client build that were overwritten or that
45
+ * collide by case. `publicDirs` are the directories Vite copied verbatim.
46
+ */
47
+ export declare function findDevvitOutputCollisions(outDir: string, publicDirs: readonly string[]): DevvitOutputCollision[];
48
+ /** Throws a single readable error listing every collision. */
49
+ export declare function assertNoDevvitOutputCollisions(outDir: string, publicDirs: readonly string[]): void;
@@ -0,0 +1,120 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ /** Validate logical output names before the filesystem can collapse them. */
4
+ export function assertDevvitOutputNames(bundleNames, publicNames, outDir) {
5
+ const collisions = [];
6
+ const names = new Map();
7
+ for (const name of [...publicNames, ...bundleNames]) {
8
+ const previous = names.get(name.toLowerCase());
9
+ if (previous !== undefined) {
10
+ collisions.push({
11
+ kind: previous === name ? 'public' : 'case',
12
+ path: name,
13
+ sources: [previous, name],
14
+ });
15
+ }
16
+ names.set(name.toLowerCase(), name);
17
+ }
18
+ throwCollisions(collisions, outDir);
19
+ }
20
+ /** Shared Vite 7/8 guard for production and staging watch builds. */
21
+ export function devvitOutputCollisionPlugin() {
22
+ let publicDir = '';
23
+ return {
24
+ name: 'mpgd-devvit-output-collisions',
25
+ apply: 'build',
26
+ enforce: 'post',
27
+ configResolved(config) {
28
+ publicDir = config.publicDir;
29
+ },
30
+ generateBundle: {
31
+ order: 'post',
32
+ handler(options, bundle) {
33
+ const publicNames = this.environment.config.build.copyPublicDir
34
+ && publicDir !== '' && existsSync(publicDir) ? listFiles(publicDir) : [];
35
+ assertDevvitOutputNames(Object.values(bundle).map((output) => output.fileName), publicNames, options.dir ?? this.environment.config.build.outDir);
36
+ },
37
+ },
38
+ };
39
+ }
40
+ function listFiles(root) {
41
+ const out = [];
42
+ const pending = [root];
43
+ while (pending.length > 0) {
44
+ const directory = pending.pop();
45
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
46
+ const absolute = join(directory, entry.name);
47
+ // Follow symlinks: a linked directory is traversed, a linked file listed.
48
+ const stats = entry.isSymbolicLink() ? statSync(absolute, { throwIfNoEntry: false }) : entry;
49
+ if (stats === undefined) {
50
+ continue;
51
+ }
52
+ if (stats.isDirectory()) {
53
+ pending.push(absolute);
54
+ }
55
+ else if (stats.isFile()) {
56
+ out.push(relative(root, absolute).split(sep).join('/'));
57
+ }
58
+ }
59
+ }
60
+ return out.sort();
61
+ }
62
+ /**
63
+ * Finds output files of a Devvit client build that were overwritten or that
64
+ * collide by case. `publicDirs` are the directories Vite copied verbatim.
65
+ */
66
+ export function findDevvitOutputCollisions(outDir, publicDirs) {
67
+ if (!existsSync(outDir)) {
68
+ return [];
69
+ }
70
+ const outputs = listFiles(outDir);
71
+ const collisions = [];
72
+ for (const publicDir of publicDirs) {
73
+ if (!existsSync(publicDir)) {
74
+ continue;
75
+ }
76
+ for (const file of listFiles(publicDir)) {
77
+ const output = join(outDir, file);
78
+ if (!existsSync(output)) {
79
+ continue;
80
+ }
81
+ if (!readFileSync(output).equals(readFileSync(join(publicDir, file)))) {
82
+ collisions.push({
83
+ kind: 'public',
84
+ path: file,
85
+ sources: [join(publicDir, file), 'vite bundle output'],
86
+ });
87
+ }
88
+ }
89
+ }
90
+ const byLowerCase = new Map();
91
+ for (const file of outputs) {
92
+ const key = file.toLowerCase();
93
+ byLowerCase.set(key, [...(byLowerCase.get(key) ?? []), file]);
94
+ }
95
+ for (const files of byLowerCase.values()) {
96
+ if (files.length > 1) {
97
+ collisions.push({ kind: 'case', path: files[0], sources: files });
98
+ }
99
+ }
100
+ return collisions;
101
+ }
102
+ /** Throws a single readable error listing every collision. */
103
+ export function assertNoDevvitOutputCollisions(outDir, publicDirs) {
104
+ const collisions = findDevvitOutputCollisions(outDir, publicDirs);
105
+ throwCollisions(collisions, outDir);
106
+ }
107
+ function throwCollisions(collisions, outDir) {
108
+ if (collisions.length === 0) {
109
+ return;
110
+ }
111
+ const lines = collisions.map((collision) => collision.kind === 'public'
112
+ ? ` - ${collision.path}: public and bundle files use the same output name`
113
+ : ` - ${collision.sources.join(', ')}: names differ only by case`);
114
+ throw new Error([
115
+ `Devvit build output has ${collisions.length} conflicting file name(s) in ${outDir}:`,
116
+ ...lines,
117
+ 'Rename the public file or the entry/chunk (Devvit client output uses unhashed [name].js names),'
118
+ + ' then rebuild.',
119
+ ].join('\n'));
120
+ }
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';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import i18n, { defineI18n } from '@gunshi/plugin-i18n';
7
7
  import resources from '@gunshi/resources';
8
8
  import { cli } from 'gunshi';
9
9
  import { assertPlatformVersionLedger } from '@mpgd/target-config';
10
+ import { resolveNativeCommandLaunch } from './command-launcher.js';
10
11
  import { buildAssetPacks } from './asset-pack-build.js';
11
12
  import { verifyAssetPackDelivery } from './asset-pack-verify.js';
12
13
  import { buildAssetPackTarget } from './asset-pack-target.js';
@@ -24,6 +25,7 @@ import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageA
24
25
  import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
25
26
  import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
26
27
  import { applyKitUpgrade, planKitUpgrade } from './kit-upgrade.js';
28
+ import { applyToolingMigration, planToolingMigration } from './tooling-migration.js';
27
29
  import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submission.js';
28
30
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
29
31
  import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
@@ -47,6 +49,7 @@ export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupporte
47
49
  export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, } from './offline-playtest.js';
48
50
  export { assertMicrosoftStorePwaCacheTransition, assertMicrosoftStorePwaPrecacheUrl, assertMicrosoftStorePwaReleaseEvidence, inspectMicrosoftStorePwaBrowserCacheTransition, microsoftStorePwaCacheSchema, readMicrosoftStorePwaBrowserReleaseEvidence, requestMicrosoftStorePwaBrowserUpdate, resolveMicrosoftStorePwaScopedCacheName, } from './microsoft-store-pwa-e2e.js';
49
51
  export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
52
+ export { getMicrosoftStoreNativeAcceptanceModulePath, getMicrosoftStoreNativeHostSourceDirectory, } from './windows-native-tools.js';
50
53
  export { createMicrosoftStorePackageGenerationRuntime, microsoftStorePackageGeneratorEndpoint, microsoftStorePackageGeneratorSourceRevision, microsoftStorePackageGenerationSchemaVersion, renderMicrosoftStorePackageGenerationMarkdown, runMicrosoftStorePackageGeneration, } from './microsoft-store-package-generation.js';
51
54
  export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, } from './microsoft-store-submission.js';
52
55
  export { assertProductionTargetReadiness, } from './production-target-readiness.js';
@@ -54,6 +57,18 @@ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
54
57
  const packageRoot = resolvePackageRoot(sourceDir);
55
58
  const detectedKitRoot = resolveDefaultKitRoot();
56
59
  const gameTemplateDir = path.resolve(packageRoot, 'templates/phaser-game');
60
+ const liteTemplateOverlayDir = path.resolve(packageRoot, 'templates/lite-overlay');
61
+ const supportedGameTemplates = ['phaser', 'lite'];
62
+ /** Lite renderers implemented today. dom/svg are reserved extension points in @mpgd/lite-shell. */
63
+ const supportedLiteRenderers = ['canvas'];
64
+ /** Phaser-only base template files that the lite template does not emit. */
65
+ const liteTemplateRemovedFiles = new Set([
66
+ 'src/scenes/BootScene.ts',
67
+ 'src/scenes/LobbyScene.ts',
68
+ 'src/scenes/PlayScene.ts',
69
+ 'src/assets/manifest.ts',
70
+ 'src/assets/marker.svg',
71
+ ]);
57
72
  const cliVersion = readPackageVersion(packageRoot);
58
73
  const recommendedMatrixTargets = 'web,microsoft-store,verse8,ait,reddit';
59
74
  const recommendedMatrixTargetsWithoutMicrosoftStore = recommendedMatrixTargets
@@ -80,6 +95,7 @@ const mpgdTemplateDependencyPackages = [
80
95
  { name: '@mpgd/game-services', packageDir: 'packages/game-services' },
81
96
  { name: '@mpgd/game-runtime', packageDir: 'packages/game-runtime' },
82
97
  { name: '@mpgd/i18n', packageDir: 'packages/i18n' },
98
+ { name: '@mpgd/lite-shell', packageDir: 'packages/lite-shell' },
83
99
  { name: '@mpgd/phaser-assets', packageDir: 'packages/phaser-assets' },
84
100
  { name: '@mpgd/platform', packageDir: 'packages/platform' },
85
101
  { name: '@mpgd/target-config', packageDir: 'packages/target-config' },
@@ -162,7 +178,7 @@ export async function runMpgdCli(args) {
162
178
  // partial document. Every other command keeps the default rendering.
163
179
  renderHeader: async (ctx) => {
164
180
  const values = ctx.values;
165
- if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
181
+ if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade' || ctx.name === 'migrate-tools') && values.json === true) {
166
182
  return '';
167
183
  }
168
184
  const title = ctx.env.description || ctx.env.name || '';
@@ -173,7 +189,7 @@ export async function runMpgdCli(args) {
173
189
  renderValidationErrors: async (ctx, error) => {
174
190
  const messages = error.errors.map((entry) => String(entry.message)).join('\n');
175
191
  const values = ctx.values;
176
- if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade') && values.json === true) {
192
+ if ((ctx.name === 'verify-delivery' || ctx.name === 'stage-target' || ctx.name === 'publish-s3' || ctx.name === 'upgrade' || ctx.name === 'migrate-tools') && values.json === true) {
177
193
  process.stderr.write(`${messages}\n`);
178
194
  return '';
179
195
  }
@@ -551,8 +567,26 @@ const gameCommand = defineI18n({
551
567
  en: 'Include the Microsoft Store PWA target and release workflow.',
552
568
  ko: 'Microsoft Store PWA 타깃과 출시 워크플로를 포함합니다.',
553
569
  },
570
+ template: {
571
+ en: 'Game template: phaser (default) or lite (no Phaser, @mpgd/lite-shell).',
572
+ ko: '게임 템플릿: phaser(기본) 또는 lite(Phaser 없음, @mpgd/lite-shell).',
573
+ },
574
+ renderer: {
575
+ en: 'Lite template renderer. Only canvas is implemented today.',
576
+ ko: 'lite 템플릿 렌더러. 현재는 canvas만 지원합니다.',
577
+ },
554
578
  }),
555
579
  args: {
580
+ template: {
581
+ type: 'string',
582
+ required: false,
583
+ description: 'Game template: phaser (default) or lite.',
584
+ },
585
+ renderer: {
586
+ type: 'string',
587
+ required: false,
588
+ description: 'Lite template renderer: canvas (default).',
589
+ },
556
590
  directory: {
557
591
  type: 'positional',
558
592
  required: true,
@@ -603,7 +637,10 @@ const gameCommand = defineI18n({
603
637
  ? 'workspace:*'
604
638
  : readOptionalString(ctx.values['dependency-version']);
605
639
  const kitPath = readGameCreateKitPath(ctx.values, ctx.values.workspace === true);
640
+ const { template, renderer } = readGameCreateTemplate(readOptionalString(ctx.values.template), readOptionalString(ctx.values.renderer));
606
641
  createGameApp({
642
+ template,
643
+ ...(renderer === undefined ? {} : { renderer }),
607
644
  directory,
608
645
  ...(title === undefined ? {} : { title }),
609
646
  ...(packageName === undefined ? {} : { packageName }),
@@ -2540,6 +2577,58 @@ const kitCommand = defineI18n({
2540
2577
  }
2541
2578
  },
2542
2579
  }),
2580
+ 'migrate-tools': defineI18n({
2581
+ name: 'migrate-tools',
2582
+ description: 'Plan or apply the TypeScript tools migration for an existing generated game.',
2583
+ resource: commandResource({
2584
+ base: {
2585
+ en: 'Plan or apply the TypeScript tools migration for an existing generated game.',
2586
+ ko: '기존 생성 게임의 TypeScript 도구 이전을 계획하거나 적용합니다.',
2587
+ },
2588
+ args: {
2589
+ game: { en: 'Existing generated game directory.', ko: '기존 생성 게임 디렉터리.' },
2590
+ apply: { en: 'Apply the reviewed local migration plan.', ko: '검토한 로컬 이전 계획을 적용합니다.' },
2591
+ json: { en: 'Print the complete plan as JSON.', ko: '전체 계획을 JSON으로 출력합니다.' },
2592
+ },
2593
+ }),
2594
+ args: {
2595
+ game: { type: 'string', required: false, default: '.', description: 'Game directory.' },
2596
+ apply: { type: 'boolean', required: false, description: 'Apply the local plan.' },
2597
+ json: { type: 'boolean', required: false, description: 'Print JSON.' },
2598
+ },
2599
+ run: (ctx) => {
2600
+ const plan = planToolingMigration(readOptionalString(ctx.values.game) ?? '.', gameTemplateDir);
2601
+ const result = ctx.values.apply === true && plan.blockers.length === 0
2602
+ ? applyToolingMigration(plan) : undefined;
2603
+ if (ctx.values.json === true) {
2604
+ process.stdout.write(JSON.stringify({ plan, result: result ?? null }, null, 2) + '\n');
2605
+ }
2606
+ else {
2607
+ console.info('TypeScript tooling plan for ' + plan.gameRoot);
2608
+ for (const edit of plan.edits) {
2609
+ console.info(' ' + (edit.after === null ? 'Remove ' : 'Write ') + edit.file);
2610
+ }
2611
+ for (const note of plan.notes) {
2612
+ console.info('Note: ' + note);
2613
+ }
2614
+ for (const blocker of plan.blockers) {
2615
+ console.error('Blocked: ' + blocker);
2616
+ }
2617
+ if (result !== undefined) {
2618
+ console.info('Applied ' + result.length + ' file change(s).');
2619
+ }
2620
+ else if (!plan.edits.length && !plan.blockers.length) {
2621
+ console.info('TypeScript tooling is already current.');
2622
+ }
2623
+ else if (!plan.blockers.length) {
2624
+ console.info('Run again with --apply after reviewing the plan.');
2625
+ }
2626
+ }
2627
+ if (plan.blockers.length) {
2628
+ process.exitCode = 1;
2629
+ }
2630
+ },
2631
+ }),
2543
2632
  doctor: defineI18n({
2544
2633
  name: 'doctor',
2545
2634
  description: 'Print CLI, kit, template, and target-wrapper status.',
@@ -2558,7 +2647,7 @@ const kitCommand = defineI18n({
2558
2647
  }),
2559
2648
  },
2560
2649
  run: () => {
2561
- console.info('Use "mpgd kit doctor" or "mpgd kit upgrade --game <directory> [--apply]".');
2650
+ console.info('Use "mpgd kit doctor", "mpgd kit upgrade --game <directory> [--apply]", or "mpgd kit migrate-tools --game <directory> [--apply]".');
2562
2651
  },
2563
2652
  });
2564
2653
  function commandResource(description, args = {}) {
@@ -2755,9 +2844,88 @@ function normalizeLegalHtml(content, sourceFile) {
2755
2844
  }
2756
2845
  return content.endsWith('\n') ? content : `${content}\n`;
2757
2846
  }
2847
+ function readGameCreateTemplate(template, renderer) {
2848
+ const resolvedTemplate = template ?? 'phaser';
2849
+ if (!supportedGameTemplates.includes(resolvedTemplate)) {
2850
+ throw new Error(`Unsupported --template ${resolvedTemplate}; expected one of: ${supportedGameTemplates.join(', ')}.`);
2851
+ }
2852
+ if (resolvedTemplate !== 'lite') {
2853
+ if (renderer !== undefined) {
2854
+ throw new Error('--renderer is only available with --template lite.');
2855
+ }
2856
+ return { template: resolvedTemplate };
2857
+ }
2858
+ const resolvedRenderer = renderer ?? 'canvas';
2859
+ if (!supportedLiteRenderers.includes(resolvedRenderer)) {
2860
+ throw new Error(`Unsupported --renderer ${resolvedRenderer}; implemented lite renderers: ${supportedLiteRenderers.join(', ')}. `
2861
+ + 'dom and svg are planned LiteRenderer implementations.');
2862
+ }
2863
+ return { template: 'lite', renderer: resolvedRenderer };
2864
+ }
2865
+ /** Apply the lite overlay to base template files: drop Phaser-only files and swap renderers. */
2866
+ function applyLiteTemplateOverlay(baseFiles, overlayFiles) {
2867
+ const overlay = new Map(overlayFiles.map((file) => [file.relativePath, file]));
2868
+ const merged = baseFiles.flatMap((file) => {
2869
+ if (liteTemplateRemovedFiles.has(file.relativePath)) {
2870
+ return [];
2871
+ }
2872
+ const replacement = overlay.get(file.relativePath);
2873
+ overlay.delete(file.relativePath);
2874
+ if (replacement !== undefined) {
2875
+ return [replacement];
2876
+ }
2877
+ if (file.relativePath === 'package.json') {
2878
+ return [{ ...file, content: liteTemplatePackageJson(file.content) }];
2879
+ }
2880
+ if (file.relativePath === 'agent/game-manifest.json') {
2881
+ return [{ ...file, content: liteTemplateGameManifest(file.content) }];
2882
+ }
2883
+ if (file.relativePath === 'mpgd.targets.json') {
2884
+ return [{ ...file, content: liteTemplateTargets(file.content) }];
2885
+ }
2886
+ return [file];
2887
+ });
2888
+ return [...merged, ...overlay.values()];
2889
+ }
2890
+ function liteTemplateTargets(content) {
2891
+ const value = JSON.parse(content);
2892
+ const reddit = value.targets.reddit;
2893
+ if (reddit !== undefined) {
2894
+ reddit.inlineEntry = 'index.html';
2895
+ reddit.inlineBudget = {
2896
+ maxGzipKb: 40,
2897
+ forbidModules: ['phaser', 'matter-js'],
2898
+ deferredChunks: ['main.js', 'matter.js', 'assets/main-*.js', 'assets/matter-*.js'],
2899
+ };
2900
+ }
2901
+ return `${JSON.stringify(value, null, 2)}\n`;
2902
+ }
2903
+ function liteTemplatePackageJson(content) {
2904
+ const value = JSON.parse(content);
2905
+ value.description = (value.description ?? '').replace('mpgd Phaser game', 'mpgd lite (Canvas2D) game');
2906
+ const dependencies = { ...value.dependencies };
2907
+ delete dependencies.phaser;
2908
+ delete dependencies['@mpgd/phaser-assets'];
2909
+ dependencies['@mpgd/lite-shell'] = '__MPGD_DEPENDENCY_VERSION_LITE_SHELL__';
2910
+ value.dependencies = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
2911
+ return `${JSON.stringify(value, null, 2)}\n`;
2912
+ }
2913
+ function liteTemplateGameManifest(content) {
2914
+ const value = JSON.parse(content);
2915
+ if (value.game !== undefined) {
2916
+ value.game.engine = 'lite-canvas';
2917
+ }
2918
+ value.blocks = (value.blocks ?? []).map((block) => block.id === 'assets.manifest.phaser-loader'
2919
+ ? { id: 'render.lite-shell.canvas', owner: 'src/lite/tapGame.ts' }
2920
+ : block);
2921
+ return `${JSON.stringify(value, null, 2)}\n`;
2922
+ }
2758
2923
  function createGameApp(input) {
2759
2924
  const appDir = resolveAppDirectory(input.directory);
2760
2925
  const gameName = path.basename(appDir);
2926
+ if (input.template === 'lite' && !input.workspace) {
2927
+ throw new Error('The lite template requires --workspace until @mpgd/lite-shell is published.');
2928
+ }
2761
2929
  assertValidGameName(gameName);
2762
2930
  const packageName = input.packageName ?? gameName;
2763
2931
  assertValidPackageName(packageName);
@@ -2777,7 +2945,11 @@ function createGameApp(input) {
2777
2945
  ...(input.title === undefined ? {} : { title: input.title }),
2778
2946
  microsoftStore: input.microsoftStore,
2779
2947
  });
2780
- const files = collectTemplateFiles(gameTemplateDir).flatMap((file) => {
2948
+ const baseTemplateFiles = collectTemplateFiles(gameTemplateDir);
2949
+ const selectedTemplateFiles = input.template === 'lite'
2950
+ ? applyLiteTemplateOverlay(baseTemplateFiles, collectTemplateFiles(liteTemplateOverlayDir))
2951
+ : baseTemplateFiles;
2952
+ const files = selectedTemplateFiles.flatMap((file) => {
2781
2953
  const prepared = prepareBaseGameTemplateFile({
2782
2954
  relativePath: file.relativePath,
2783
2955
  content: file.content,
@@ -2923,6 +3095,9 @@ function createTemplateContext(input) {
2923
3095
  workspaceI18nBuildPrefix: input.workspace
2924
3096
  ? `pnpm --dir ${workspacePrefix} i18n:build && `
2925
3097
  : '',
3098
+ devvitOutputPluginImport: input.workspace
3099
+ ? toJavaScriptStringLiteral(`./${toTemplatePath(path.relative(path.join(input.appDir, 'apps/target-devvit'), path.resolve(kitPath ?? input.appDir, 'packages/cli/src/devvit-output-collisions')))}`)
3100
+ : "'@mpgd/cli/devvit'",
2926
3101
  defaultKitPath: kitPath === undefined ? defaultMpgdKitPath : workspacePrefix,
2927
3102
  recommendedMatrixTargets: input.microsoftStore
2928
3103
  ? recommendedMatrixTargets
@@ -3014,6 +3189,7 @@ function walkTemplateDir(templateDir, relativeDir, files) {
3014
3189
  }
3015
3190
  function renderTemplate(content, context) {
3016
3191
  let rendered = content
3192
+ .replaceAll("from '@mpgd/cli/devvit'", `from ${context.devvitOutputPluginImport}`)
3017
3193
  .replaceAll('__GAME_NAME__', context.gameName)
3018
3194
  .replaceAll('__DEVVIT_APP_NAME__', context.devvitAppName)
3019
3195
  .replaceAll('__GAME_TITLE_TS_LITERAL__', context.gameTitleTsLiteral)
@@ -3037,9 +3213,10 @@ function renderTemplate(content, context) {
3037
3213
  return rendered;
3038
3214
  }
3039
3215
  function templateOutputPath(relativePath) {
3040
- return path.basename(relativePath) === 'gitignore'
3041
- ? path.join(path.dirname(relativePath), '.gitignore')
3042
- : relativePath;
3216
+ const normalized = relativePath.replace(/\\/gu, '/');
3217
+ return path.posix.basename(normalized) === 'gitignore'
3218
+ ? path.posix.join(path.posix.dirname(normalized), '.gitignore')
3219
+ : normalized;
3043
3220
  }
3044
3221
  function toPascalCase(value) {
3045
3222
  return value
@@ -3506,12 +3683,11 @@ function runPnpm(args, commandEnv) {
3506
3683
  if (kitPath === undefined || kitPath.length === 0) {
3507
3684
  throw new Error('Missing MPGD_KIT_PATH. Pass --kit-path or set MPGD_KIT_PATH.');
3508
3685
  }
3509
- const command = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
3510
- const result = spawnSync(command, [...args], {
3686
+ const launch = resolveNativeCommandLaunch({ command: 'pnpm', args, environment: commandEnv });
3687
+ const result = spawnSync(launch.command, [...launch.args], {
3511
3688
  cwd: kitPath,
3512
3689
  env: commandEnv,
3513
3690
  stdio: 'inherit',
3514
- shell: process.platform === 'win32',
3515
3691
  });
3516
3692
  if (result.error !== undefined) {
3517
3693
  throw result.error;
@@ -3,6 +3,7 @@ import { closeSync, existsSync, fstatSync, lstatSync, openSync, readFileSync, re
3
3
  import { isIP } from 'node:net';
4
4
  import path from 'node:path';
5
5
  import { inflateSync } from 'node:zlib';
6
+ import { normalizeRootRelativeRoute } from '@mpgd/platform/routes';
6
7
  import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
7
8
  import { isMicrosoftStoreSupportedListingLocale } from './microsoft-store-supported-locales.js';
8
9
  export const microsoftStoreSubmissionSchemaVersion = 1;
@@ -445,14 +446,18 @@ function parseListings(input) {
445
446
  }];
446
447
  }));
447
448
  }
449
+ /**
450
+ * An artifact-relative URL: `.` and then a root-relative route already in the
451
+ * form `normalizeRootRelativeRoute` from `@mpgd/platform/routes` returns, the
452
+ * rule share links and notification routes follow, so no client reads it as
453
+ * another host, even once it percent-decodes the path. The manifest ships in
454
+ * the artifact and the Store resolves these against the manifest's own URL,
455
+ * so an absolute URL is never needed.
456
+ */
448
457
  function requireManifestUrl(input, label) {
449
458
  const value = requireNonEmptyString(input, label);
450
- const base = new URL('https://mpgd.invalid/');
451
- const url = new URL(value, base);
452
- if (!value.startsWith('./')
453
- || url.origin !== base.origin
454
- || url.pathname.startsWith('//')
455
- || value !== `.${url.pathname}${url.search}${url.hash}`) {
459
+ const route = value.startsWith('./') ? value.slice(1) : undefined;
460
+ if (route === undefined || normalizeRootRelativeRoute(route) !== route) {
456
461
  throw new Error(`${label} must be an artifact-relative URL beginning with ./`);
457
462
  }
458
463
  return value;
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "a54c0341749bb93a97ddd927205de960294c6511",
2
+ "kitGitSha": "88c1eb8846cc699d328d94d9c374b088c123e4c1",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.47.0"
4
+ "packageVersion": "0.49.0"
5
5
  }