@mpgd/cli 0.31.0 → 0.33.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 (36) hide show
  1. package/dist/asset-pack-build.d.ts +32 -0
  2. package/dist/asset-pack-build.js +411 -0
  3. package/dist/asset-pack-verify.d.ts +76 -0
  4. package/dist/asset-pack-verify.js +922 -0
  5. package/dist/asset-pack-zip.d.ts +19 -0
  6. package/dist/asset-pack-zip.js +81 -0
  7. package/dist/bin.js +6 -1
  8. package/dist/cloudflare-pages-static.js +1 -1
  9. package/dist/evidence-io.js +1 -1
  10. package/dist/game-acceptance.js +2 -2
  11. package/dist/gameplay-e2e.js +5 -5
  12. package/dist/hosted-pwa-deployment.js +2 -4
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.js +296 -5
  15. package/dist/microsoft-store-package-acceptance.js +1 -1
  16. package/dist/microsoft-store-package-generation-download.js +3 -3
  17. package/dist/microsoft-store-package-generation-input.js +1 -1
  18. package/dist/microsoft-store-package-generation-zip.js +1 -1
  19. package/dist/microsoft-store-submission.js +4 -4
  20. package/dist/offline-playtest.js +5 -5
  21. package/package.json +16 -16
  22. package/templates/phaser-game/apps/target-ait/package.json +4 -5
  23. package/templates/phaser-game/apps/target-ait/vite.config.ts +2 -7
  24. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
  25. package/templates/phaser-game/apps/target-devvit/package.json +2 -2
  26. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +6 -12
  27. package/templates/phaser-game/package.json +3 -3
  28. package/templates/phaser-game/pnpm-workspace.yaml +0 -4
  29. package/templates/phaser-game/src/entry.ts +1 -4
  30. package/templates/phaser-game/src/gameEntry.ts +1 -4
  31. package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +1 -4
  32. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -4
  33. package/templates/phaser-game/src/platform/devvitEntrypoint.ts +1 -4
  34. package/templates/phaser-game/tsconfig.json +1 -0
  35. package/templates/phaser-game/vite.runtime-target-config.ts +2 -2
  36. package/templates/phaser-game/vite.shared.ts +1 -3
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Deterministic ZIP writer for pack delivery archives. The builder targets
3
+ * this limited format only: STORE and DEFLATE entries, no ZIP64, no split
4
+ * archives, no encryption, no directory or symlink entries, no comments.
5
+ * Fixed metadata: DOS timestamp 1980-01-01 00:00:00, UNIX permissions 0644,
6
+ * UTF-8 name flag, no extra fields. Given the same entries in the same order
7
+ * and the same Node/zlib build, the produced bytes are identical.
8
+ */
9
+ export interface DeterministicZipEntry {
10
+ /** Normalized relative entry path; validated by the pack format module. */
11
+ readonly path: string;
12
+ /** Uncompressed entry bytes. */
13
+ readonly data: Buffer;
14
+ readonly method: 'store' | 'deflate';
15
+ /** Precomputed stored bytes for the entry method, when the builder already
16
+ * produced them (for example while choosing the method). */
17
+ readonly stored?: Buffer | undefined;
18
+ }
19
+ export declare function createDeterministicZip(entries: readonly DeterministicZipEntry[]): Buffer;
@@ -0,0 +1,81 @@
1
+ import { crc32, deflateRawSync } from 'node:zlib';
2
+ const DOS_TIME = 0;
3
+ const DOS_DATE = 0x0021;
4
+ const VERSION_NEEDED = 20;
5
+ const VERSION_MADE_BY = 0x0300;
6
+ const UTF8_FLAG = 0x0800;
7
+ const EXTERNAL_ATTRIBUTES = (0o100644 << 16) >>> 0;
8
+ const MAX_ENTRIES = 0xffff;
9
+ const MAX_ENTRY_BYTES = 0xffffffff;
10
+ export function createDeterministicZip(entries) {
11
+ if (entries.length === 0 || entries.length > MAX_ENTRIES) {
12
+ throw new Error(`Deterministic ZIP requires 1..${MAX_ENTRIES} entries`);
13
+ }
14
+ const localChunks = [];
15
+ const centralChunks = [];
16
+ let offset = 0;
17
+ for (const entry of entries) {
18
+ const name = Buffer.from(entry.path, 'utf8');
19
+ if (name.length > MAX_ENTRIES) {
20
+ throw new Error(`Deterministic ZIP entry name exceeds 65535 bytes: ${entry.path}`);
21
+ }
22
+ if (entry.data.length > MAX_ENTRY_BYTES) {
23
+ throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
24
+ }
25
+ const compressed = entry.method === 'deflate'
26
+ ? (entry.stored ?? deflateRawSync(entry.data, { level: 9 }))
27
+ : entry.data;
28
+ if (compressed.length > MAX_ENTRY_BYTES) {
29
+ throw new Error(`Deterministic ZIP entry exceeds 4 GiB: ${entry.path}`);
30
+ }
31
+ const methodCode = entry.method === 'deflate' ? 8 : 0;
32
+ const digest = crc32(entry.data) >>> 0;
33
+ const local = Buffer.alloc(30);
34
+ local.writeUInt32LE(0x04034b50, 0);
35
+ local.writeUInt16LE(VERSION_NEEDED, 4);
36
+ local.writeUInt16LE(UTF8_FLAG, 6);
37
+ local.writeUInt16LE(methodCode, 8);
38
+ local.writeUInt16LE(DOS_TIME, 10);
39
+ local.writeUInt16LE(DOS_DATE, 12);
40
+ local.writeUInt32LE(digest, 14);
41
+ local.writeUInt32LE(compressed.length, 18);
42
+ local.writeUInt32LE(entry.data.length, 22);
43
+ local.writeUInt16LE(name.length, 26);
44
+ local.writeUInt16LE(0, 28);
45
+ localChunks.push(local, name, compressed);
46
+ const central = Buffer.alloc(46);
47
+ central.writeUInt32LE(0x02014b50, 0);
48
+ central.writeUInt16LE(VERSION_MADE_BY, 4);
49
+ central.writeUInt16LE(VERSION_NEEDED, 6);
50
+ central.writeUInt16LE(UTF8_FLAG, 8);
51
+ central.writeUInt16LE(methodCode, 10);
52
+ central.writeUInt16LE(DOS_TIME, 12);
53
+ central.writeUInt16LE(DOS_DATE, 14);
54
+ central.writeUInt32LE(digest, 16);
55
+ central.writeUInt32LE(compressed.length, 20);
56
+ central.writeUInt32LE(entry.data.length, 24);
57
+ central.writeUInt16LE(name.length, 28);
58
+ central.writeUInt16LE(0, 30);
59
+ central.writeUInt16LE(0, 32);
60
+ central.writeUInt16LE(0, 34);
61
+ central.writeUInt16LE(0, 36);
62
+ central.writeUInt32LE(EXTERNAL_ATTRIBUTES, 38);
63
+ central.writeUInt32LE(offset, 42);
64
+ centralChunks.push(central, name);
65
+ offset += local.length + name.length + compressed.length;
66
+ }
67
+ const centralDirectory = Buffer.concat(centralChunks);
68
+ if (offset > MAX_ENTRY_BYTES || centralDirectory.length > MAX_ENTRY_BYTES) {
69
+ throw new Error('Deterministic ZIP archive exceeds 4 GiB');
70
+ }
71
+ const end = Buffer.alloc(22);
72
+ end.writeUInt32LE(0x06054b50, 0);
73
+ end.writeUInt16LE(0, 4);
74
+ end.writeUInt16LE(0, 6);
75
+ end.writeUInt16LE(entries.length, 8);
76
+ end.writeUInt16LE(entries.length, 10);
77
+ end.writeUInt32LE(centralDirectory.length, 12);
78
+ end.writeUInt32LE(offset, 16);
79
+ end.writeUInt16LE(0, 20);
80
+ return Buffer.concat([...localChunks, centralDirectory, end]);
81
+ }
package/dist/bin.js CHANGED
@@ -1,3 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { readCliArgs, runMpgdCli } from './index.js';
2
+ import { mpgdCliNeedsForcedExit, readCliArgs, runMpgdCli } from './index.js';
3
3
  await runMpgdCli(readCliArgs());
4
+ if (mpgdCliNeedsForcedExit()) {
5
+ // Stalled threadpool filesystem work cannot be cancelled; only the
6
+ // standalone binary may terminate the process to outrun it.
7
+ process.exit(process.exitCode ?? 1);
8
+ }
@@ -9,7 +9,7 @@
9
9
  * being silently ignored.
10
10
  */
11
11
  const maximumHeaderBlocks = 100;
12
- const maximumLineLength = 2000;
12
+ const maximumLineLength = 2_000;
13
13
  const supportedRedirectCodes = new Set([301, 302, 303, 307, 308]);
14
14
  export function parseCloudflarePagesHeaders(input) {
15
15
  const lines = input.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
@@ -38,7 +38,7 @@ export function escapeMarkdownInline(value) {
38
38
  .replaceAll('\n', ' ');
39
39
  }
40
40
  export function formatDuration(durationMs) {
41
- return durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
41
+ return durationMs < 1_000 ? `${durationMs}ms` : `${(durationMs / 1_000).toFixed(1)}s`;
42
42
  }
43
43
  export function formatError(error) {
44
44
  return error instanceof Error ? error.message : String(error);
@@ -3,9 +3,9 @@ import { existsSync, mkdirSync, rmSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { escapeMarkdownInline, escapeMarkdownTable, formatDuration, formatError, readBoundedUtf8File, relativeOrAbsolute, writeEvidenceReportFiles, } from './evidence-io.js';
5
5
  import { collectGameplayE2EPathEvidence, maximumGameplayE2EStates, readGameplayE2EPlan, resolveGameplayE2EPathInsideGameRoot, } from './gameplay-e2e.js';
6
- export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 * 1000;
6
+ export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 * 1_000;
7
7
  const defaultGameAcceptanceReleaseManifestFile = 'artifacts/release-manifest.json';
8
- export const maximumGameplayE2EReportBytes = 1024 * 1024;
8
+ export const maximumGameplayE2EReportBytes = 1_024 * 1_024;
9
9
  export function resolveGameAcceptanceReleaseManifestFile(gameRoot, env = process.env) {
10
10
  const configuredFile = env.MPGD_RELEASE_MANIFEST_FILE;
11
11
  return path.resolve(gameRoot, configuredFile === undefined || configuredFile.length === 0
@@ -7,13 +7,13 @@ const gameplayStateIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
7
7
  const gameplayKeyControlCharacterPattern = /[\u0000-\u001f\u007f]/u;
8
8
  export const maximumGameplayE2EStates = 50;
9
9
  const maximumActionsPerState = 100;
10
- const maximumWaitMs = 60000;
11
- const maximumBackgroundMs = 5 * 60000;
12
- const maximumHashReadChunkBytes = 64 * 1024;
10
+ const maximumWaitMs = 60_000;
11
+ const maximumBackgroundMs = 5 * 60_000;
12
+ const maximumHashReadChunkBytes = 64 * 1_024;
13
13
  const defaultGameplayE2EHashLimits = {
14
14
  maximumDepth: 128,
15
- maximumEntries: 100000,
16
- maximumTotalFileBytes: 4 * 1024 * 1024 * 1024,
15
+ maximumEntries: 100_000,
16
+ maximumTotalFileBytes: 4 * 1_024 * 1_024 * 1_024,
17
17
  };
18
18
  class GameplayE2EAggregatedError extends Error {
19
19
  errors;
@@ -966,7 +966,7 @@ function decodeCssEscapes(value) {
966
966
  return plain ?? '';
967
967
  }
968
968
  const codePoint = Number.parseInt(hex, 16);
969
- return codePoint > 1114111 || Number.isNaN(codePoint)
969
+ return codePoint > 0x10_ffff || Number.isNaN(codePoint)
970
970
  ? '\uFFFD'
971
971
  : String.fromCodePoint(codePoint);
972
972
  });
@@ -1161,9 +1161,7 @@ function verifyCloudflarePagesHeaders(deploymentRoot, sourceFiles, deploymentFil
1161
1161
  if (sourcePaths.has(file.slice(1))) {
1162
1162
  requirements.push({
1163
1163
  requestPath: file,
1164
- expected: noStoreCacheControlFiles.has(file)
1165
- ? noStoreCacheControl
1166
- : freshCacheControl,
1164
+ expected: noStoreCacheControlFiles.has(file) ? noStoreCacheControl : freshCacheControl,
1167
1165
  label: file.slice(1),
1168
1166
  });
1169
1167
  }
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcce
8
8
  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';
9
9
  export { microsoftStoreSubmissionSchemaVersion, parseMicrosoftStoreSubmissionConfig, renderMicrosoftStoreSubmissionMarkdown, runMicrosoftStoreSubmissionPreflight, type MicrosoftStoreSubmissionConfig, type MicrosoftStoreSubmissionCommerce, type MicrosoftStoreSubmissionCommerceProduct, type MicrosoftStoreSubmissionEvidence, type RunMicrosoftStoreSubmissionPreflightInput, } from './microsoft-store-submission.js';
10
10
  export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';
11
+ export declare function mpgdCliNeedsForcedExit(): boolean;
11
12
  export declare function runMpgdCli(args: readonly string[]): Promise<void>;
12
13
  export declare function runCreateGameCli(args: readonly string[]): Promise<void>;
13
14
  export declare function readCliArgs(): readonly string[];
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ 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 { buildAssetPacks } from './asset-pack-build.js';
10
+ import { verifyAssetPackDelivery } from './asset-pack-verify.js';
9
11
  import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
10
12
  import { escapeMarkdownInline as escapeEvidenceMarkdownInline, writeEvidenceReportFiles, } from './evidence-io.js';
11
13
  import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
@@ -85,11 +87,71 @@ const acceptanceStepIds = {
85
87
  targetSmoke: 'target-smoke',
86
88
  gameplayE2E: 'gameplay-e2e',
87
89
  };
90
+ /** Whether the last run reported a deadline failure that leaves
91
+ * uncancellable filesystem work pending: the standalone binary forces its
92
+ * exit (see bin.ts); in-process library callers handle it themselves. */
93
+ let forcedExitPending = false;
94
+ /** A blocked pipe must not prevent the standalone binary from reaching its
95
+ * forced-exit path after a deadline report. The callback may remain queued
96
+ * behind a stalled reader, so the drain is best effort and bounded. */
97
+ const CLI_OUTPUT_DRAIN_TIMEOUT_MS = 250;
98
+ const drainOutput = (stream) => new Promise((resolve) => {
99
+ let settled = false;
100
+ const finish = () => {
101
+ if (settled) {
102
+ return;
103
+ }
104
+ settled = true;
105
+ resolve();
106
+ };
107
+ const timer = setTimeout(finish, CLI_OUTPUT_DRAIN_TIMEOUT_MS);
108
+ try {
109
+ stream.write('', () => {
110
+ clearTimeout(timer);
111
+ finish();
112
+ });
113
+ }
114
+ catch {
115
+ clearTimeout(timer);
116
+ finish();
117
+ }
118
+ });
119
+ export function mpgdCliNeedsForcedExit() {
120
+ return forcedExitPending;
121
+ }
88
122
  export async function runMpgdCli(args) {
123
+ forcedExitPending = false;
89
124
  await cli([...args], entryCommand, {
90
125
  name: 'mpgd',
91
126
  version: cliVersion,
127
+ // Machine-readable mode contract: `assets verify-delivery --json`
128
+ // owns stdout entirely — exactly one JSON document, parseable with
129
+ // JSON.parse(stdout) and nothing else. The framework's banner is
130
+ // suppressed for that command, and framework-level argument errors
131
+ // (bad value types, missing required arguments — rejected before the
132
+ // command handler runs) move to stderr so stdout never carries a
133
+ // partial document. Every other command keeps the default rendering.
134
+ renderHeader: async (ctx) => {
135
+ const values = ctx.values;
136
+ if (ctx.name === 'verify-delivery' && values.json === true) {
137
+ return '';
138
+ }
139
+ const title = ctx.env.description || ctx.env.name || '';
140
+ return title
141
+ ? `${title} (${ctx.env.name || ''}${ctx.env.version ? ` v${ctx.env.version}` : ''})`
142
+ : title;
143
+ },
144
+ renderValidationErrors: async (ctx, error) => {
145
+ const messages = error.errors.map((entry) => String(entry.message)).join('\n');
146
+ const values = ctx.values;
147
+ if (ctx.name === 'verify-delivery' && values.json === true) {
148
+ process.stderr.write(`${messages}\n`);
149
+ return '';
150
+ }
151
+ return messages;
152
+ },
92
153
  subCommands: {
154
+ assets: assetsCommand,
93
155
  game: gameCommand,
94
156
  legal: legalCommand,
95
157
  target: targetCommand,
@@ -143,7 +205,7 @@ const entryCommand = defineI18n({
143
205
  ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
144
206
  }),
145
207
  run: () => {
146
- console.info('Use a sub-command: game, target, kit.');
208
+ console.info('Use a sub-command: assets, game, legal, target, kit.');
147
209
  console.info('Run "pnpm mpgd --help" for available commands.');
148
210
  },
149
211
  });
@@ -774,6 +836,226 @@ function readPositiveInteger(value, label) {
774
836
  }
775
837
  return parsed;
776
838
  }
839
+ function assetPackVerifyArgs() {
840
+ return {
841
+ manifest: {
842
+ type: 'string',
843
+ required: true,
844
+ description: 'Path to the delivery manifest JSON document.',
845
+ },
846
+ root: {
847
+ type: 'string',
848
+ required: true,
849
+ description: 'Deployment root the manifest artifact paths resolve against.',
850
+ },
851
+ json: {
852
+ type: 'boolean',
853
+ required: false,
854
+ description: 'Print a single machine-readable JSON report to stdout.',
855
+ },
856
+ 'max-object-bytes': {
857
+ type: 'number',
858
+ required: false,
859
+ description: 'Optional static-host limit: largest allowed deployed object in bytes.',
860
+ },
861
+ 'max-files': {
862
+ type: 'number',
863
+ required: false,
864
+ description: 'Optional static-host limit: total regular files allowed under the root.',
865
+ },
866
+ 'max-total-bytes': {
867
+ type: 'number',
868
+ required: false,
869
+ description: 'Optional static-host limit: total deployed bytes allowed under the root.',
870
+ },
871
+ 'max-archive-bytes': {
872
+ type: 'number',
873
+ required: false,
874
+ description: 'Optional cap: largest zip archive read for verification, in bytes.',
875
+ },
876
+ 'max-entry-bytes': {
877
+ type: 'number',
878
+ required: false,
879
+ description: 'Optional cap: largest expanded zip entry allowed, in bytes.',
880
+ },
881
+ 'max-expanded-bytes': {
882
+ type: 'number',
883
+ required: false,
884
+ description: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
885
+ },
886
+ };
887
+ }
888
+ function assetPackBuildArgs() {
889
+ return {
890
+ config: {
891
+ type: 'string',
892
+ required: true,
893
+ description: 'Path to the asset pack build config JSON file.',
894
+ },
895
+ out: {
896
+ type: 'string',
897
+ required: true,
898
+ description: 'Output directory for delivery artifacts, outside the source root.',
899
+ },
900
+ };
901
+ }
902
+ const assetsCommand = defineI18n({
903
+ name: 'assets',
904
+ description: 'Build deterministic asset pack delivery artifacts.',
905
+ resource: commandResource({
906
+ en: 'Build deterministic asset pack delivery artifacts.',
907
+ ko: '결정적인 에셋 팩 전달 산출물을 빌드합니다.',
908
+ }),
909
+ subCommands: {
910
+ 'build-packs': defineI18n({
911
+ name: 'build-packs',
912
+ description: 'Build files or ZIP delivery artifacts for configured packs.',
913
+ resource: commandResource({
914
+ en: 'Build files or ZIP delivery artifacts for configured packs.',
915
+ ko: '설정된 팩의 files 또는 ZIP 전달 산출물을 빌드합니다.',
916
+ }, {
917
+ config: {
918
+ en: 'Path to the asset pack build config JSON file.',
919
+ ko: '에셋 팩 빌드 설정 JSON 파일 경로.',
920
+ },
921
+ out: {
922
+ en: 'Output directory for delivery artifacts, outside the source root.',
923
+ ko: '전달 산출물 출력 디렉터리. 소스 루트 밖이어야 합니다.',
924
+ },
925
+ }),
926
+ args: assetPackBuildArgs(),
927
+ run: (ctx) => {
928
+ const report = buildAssetPacks({
929
+ configPath: ctx.values.config,
930
+ outDir: ctx.values.out,
931
+ });
932
+ for (const output of report.outputs) {
933
+ console.info(`${output.action === 'written' ? 'wrote' : 'unchanged'} ${output.path} `
934
+ + `(${output.bytes} bytes, sha256:${output.sha256})`);
935
+ }
936
+ for (const archive of report.archives) {
937
+ console.info(`archive ${archive.path}: ${archive.entryCount} entries `
938
+ + `(${archive.storeEntries} store, ${archive.deflateEntries} deflate), `
939
+ + `sources ${archive.sourceBytes} bytes, archive ${archive.archiveBytes} bytes, `
940
+ + `sha256:${archive.sha256}`);
941
+ }
942
+ console.info(`Asset pack delivery manifest: ${report.manifestPath}`);
943
+ },
944
+ }),
945
+ 'verify-delivery': defineI18n({
946
+ name: 'verify-delivery',
947
+ description: 'Verify delivery artifacts read-only against the manifest.',
948
+ resource: commandResource({
949
+ en: 'Verify delivery artifacts read-only against the manifest.',
950
+ ko: '전달 산출물을 manifest 기준으로 읽기 전용으로 검증합니다.',
951
+ }, {
952
+ manifest: {
953
+ en: 'Path to the delivery manifest JSON document.',
954
+ ko: '전달 manifest JSON 문서 경로.',
955
+ },
956
+ root: {
957
+ en: 'Deployment root the manifest artifact paths resolve against.',
958
+ ko: 'manifest 산출물 경로의 기준이 되는 배포 루트 디렉터리.',
959
+ },
960
+ json: {
961
+ en: 'Print a single machine-readable JSON report to stdout.',
962
+ ko: '기계 판독 가능한 JSON 보고서 하나를 stdout에 출력합니다.',
963
+ },
964
+ 'max-object-bytes': {
965
+ en: 'Optional static-host limit: largest allowed deployed object in bytes.',
966
+ ko: '선택적 정적 호스트 한도: 허용되는 최대 배포 object 바이트 수.',
967
+ },
968
+ 'max-files': {
969
+ en: 'Optional static-host limit: total regular files allowed under the root.',
970
+ ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 일반 파일 총 개수.',
971
+ },
972
+ 'max-total-bytes': {
973
+ en: 'Optional static-host limit: total deployed bytes allowed under the root.',
974
+ ko: '선택적 정적 호스트 한도: 루트 아래 허용되는 총 배포 바이트 수.',
975
+ },
976
+ 'max-archive-bytes': {
977
+ en: 'Optional cap: largest zip archive read for verification, in bytes.',
978
+ ko: '선택적 상한: 검증 시 읽는 zip 아카이브의 최대 바이트 수.',
979
+ },
980
+ 'max-entry-bytes': {
981
+ en: 'Optional cap: largest expanded zip entry allowed, in bytes.',
982
+ ko: '선택적 상한: 허용되는 확장(zip 해제) 엔트리 최대 바이트 수.',
983
+ },
984
+ 'max-expanded-bytes': {
985
+ en: 'Optional cap: total expanded bytes allowed per archive, in bytes.',
986
+ ko: '선택적 상한: 아카이브별 허용되는 총 확장 바이트 수.',
987
+ },
988
+ }),
989
+ args: assetPackVerifyArgs(),
990
+ run: async (ctx) => {
991
+ const hostLimits = {
992
+ ...(ctx.values['max-object-bytes'] === undefined
993
+ ? {}
994
+ : { maxObjectBytes: ctx.values['max-object-bytes'] }),
995
+ ...(ctx.values['max-files'] === undefined
996
+ ? {}
997
+ : { maxFiles: ctx.values['max-files'] }),
998
+ ...(ctx.values['max-total-bytes'] === undefined
999
+ ? {}
1000
+ : { maxTotalBytes: ctx.values['max-total-bytes'] }),
1001
+ };
1002
+ const report = await verifyAssetPackDelivery({
1003
+ manifestPath: ctx.values.manifest,
1004
+ root: ctx.values.root,
1005
+ ...(Object.keys(hostLimits).length > 0 ? { hostLimits } : {}),
1006
+ ...(ctx.values['max-archive-bytes'] === undefined
1007
+ ? {}
1008
+ : { maxArchiveBytes: ctx.values['max-archive-bytes'] }),
1009
+ ...(ctx.values['max-entry-bytes'] === undefined
1010
+ ? {}
1011
+ : { maxEntryBytes: ctx.values['max-entry-bytes'] }),
1012
+ ...(ctx.values['max-expanded-bytes'] === undefined
1013
+ ? {}
1014
+ : { maxExpandedBytes: ctx.values['max-expanded-bytes'] }),
1015
+ });
1016
+ if (ctx.values.json === true) {
1017
+ console.info(JSON.stringify(report, null, 2));
1018
+ }
1019
+ else {
1020
+ for (const archive of report.archives) {
1021
+ console.info(`verified archive ${archive.packId}: ${archive.entries} entries, ${archive.expandedBytes} expanded bytes`);
1022
+ }
1023
+ console.info(`manifest ${report.manifest.sha256.slice(0, 12)}… (${report.manifest.bytes} bytes, ${report.manifest.packs} packs)`);
1024
+ console.info(`referenced objects: ${report.referenced.files} files, ${report.referenced.bytes} bytes`);
1025
+ if (report.inventory !== undefined) {
1026
+ console.info(`root inventory: ${report.inventory.files} files, ${report.inventory.bytes} bytes${report.inventory.truncated ? ' (truncated)' : ''}`);
1027
+ }
1028
+ for (const failure of report.failures) {
1029
+ console.error(`[${failure.stage}/${failure.code}] ${failure.message}`);
1030
+ }
1031
+ if (report.ok) {
1032
+ console.info('Delivery verification passed.');
1033
+ }
1034
+ else {
1035
+ console.error('Delivery verification failed; see the failures above.');
1036
+ }
1037
+ }
1038
+ if (report.failures.some((failure) => failure.code === 'deadline')) {
1039
+ // Piped console output is asynchronous; drain both streams so a
1040
+ // large report is not truncated by the binary's forced exit.
1041
+ await Promise.all([drainOutput(process.stdout), drainOutput(process.stderr)]);
1042
+ // Stalled filesystem requests cannot be cancelled once the
1043
+ // deadline fires; the standalone binary forces its exit so a
1044
+ // pending threadpool call cannot keep the process (and its
1045
+ // leaked handle) alive past the reported budget. In-process
1046
+ // callers of runMpgdCli observe mpgdCliNeedsForcedExit instead.
1047
+ forcedExitPending = true;
1048
+ }
1049
+ if (!report.ok) {
1050
+ process.exitCode = 1;
1051
+ }
1052
+ },
1053
+ }),
1054
+ },
1055
+ run: () => {
1056
+ console.info('Use "mpgd assets build-packs" or "mpgd assets verify-delivery".');
1057
+ },
1058
+ });
777
1059
  const legalCommand = defineI18n({
778
1060
  name: 'legal',
779
1061
  description: 'Build static legal/support pages for store and release evidence.',
@@ -1910,9 +2192,7 @@ function createGameApp(input) {
1910
2192
  relativePath: file.relativePath,
1911
2193
  content: file.content,
1912
2194
  });
1913
- return prepared === undefined
1914
- ? []
1915
- : [{ ...file, content: renderTemplate(prepared, context) }];
2195
+ return prepared === undefined ? [] : [{ ...file, content: renderTemplate(prepared, context) }];
1916
2196
  });
1917
2197
  if (input.dryRun) {
1918
2198
  console.info(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
@@ -2003,8 +2283,17 @@ function createTemplateContext(input) {
2003
2283
  if (input.workspace && kitPath === undefined) {
2004
2284
  throw new Error('Could not detect an mpgd-kit checkout for --workspace. Pass --kit-path or set MPGD_KIT_PATH.');
2005
2285
  }
2006
- const workspaceRoot = toTemplatePath(path.relative(input.appDir, kitPath ?? input.appDir) || '.');
2286
+ const relativeKitPath = path.relative(input.appDir, kitPath ?? input.appDir);
2287
+ if (input.workspace && path.isAbsolute(relativeKitPath)) {
2288
+ throw new Error('--workspace requires the game and mpgd-kit to share a filesystem root.');
2289
+ }
2290
+ const workspaceRoot = toTemplatePath(relativeKitPath || '.');
2007
2291
  const workspacePrefix = workspaceRoot;
2292
+ // The normalized relative path's parent hops reach the common source root.
2293
+ // Do not inherit the kit's narrower root when the game is a sibling checkout.
2294
+ const tsconfigRootDir = input.workspace
2295
+ ? workspacePrefix.split('/').filter((segment) => segment === '..').join('/') || '.'
2296
+ : '.';
2008
2297
  const mpgdDependencyVersionReplacements = resolveMpgdDependencyVersionReplacements({
2009
2298
  ...(input.dependencyVersion === undefined
2010
2299
  ? {}
@@ -2025,6 +2314,7 @@ function createTemplateContext(input) {
2025
2314
  tsconfigExtendsLine: input.workspace
2026
2315
  ? ` "extends": "${workspacePrefix}/tsconfig.base.json",`
2027
2316
  : '',
2317
+ tsconfigRootDir,
2028
2318
  tsconfigWorkspaceIncludes: input.workspace
2029
2319
  ? [
2030
2320
  `,\n "${workspacePrefix}/packages/**/*.ts"`,
@@ -2142,6 +2432,7 @@ function renderTemplate(content, context) {
2142
2432
  .replaceAll('__PACKAGE_NAME__', context.packageName)
2143
2433
  .replaceAll('__MPGD_DEPENDENCY_VERSION__', context.defaultDependencyVersion)
2144
2434
  .replaceAll('__TSCONFIG_EXTENDS_LINE__', context.tsconfigExtendsLine)
2435
+ .replaceAll('__TSCONFIG_ROOT_DIR__', context.tsconfigRootDir)
2145
2436
  .replaceAll('__TSCONFIG_WORKSPACE_INCLUDES__', context.tsconfigWorkspaceIncludes)
2146
2437
  .replaceAll('__TSCONFIG_WORKSPACE_EXCLUDES__', context.tsconfigWorkspaceExcludes)
2147
2438
  .replaceAll('__WORKSPACE_I18N_BUILD_PREFIX__', context.workspaceI18nBuildPrefix)
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
7
7
  export const microsoftStorePackageAcceptanceSchemaVersion = 1;
8
8
  const supportedPackageExtensions = ['.appx', '.appxbundle', '.msix', '.msixbundle'];
9
- const maximumWindowsPackageCommandDurationMs = 60 * 60 * 1000;
9
+ const maximumWindowsPackageCommandDurationMs = 60 * 60 * 1_000;
10
10
  const maximumWindowsPackageCommandOutputBytes = 16 * 1024 * 1024;
11
11
  const maximumWindowsPackageCommandDiagnosticCharacters = 8 * 1024;
12
12
  const xmlNamedEntities = {
@@ -11,9 +11,9 @@ import { assertMicrosoftStorePackageZip } from './microsoft-store-package-genera
11
11
  const maximumManifestBytes = 1024 * 1024;
12
12
  const maximumManifestIconBytes = 2 * 1024 * 1024;
13
13
  const maximumArchiveBytes = 512 * 1024 * 1024;
14
- const manifestRequestTimeoutMs = 30000;
15
- const manifestIconRequestTimeoutMs = 30000;
16
- const packageRequestTimeoutMs = 10 * 60 * 1000;
14
+ const manifestRequestTimeoutMs = 30_000;
15
+ const manifestIconRequestTimeoutMs = 30_000;
16
+ const packageRequestTimeoutMs = 10 * 60 * 1_000;
17
17
  const blockedIpv4Addresses = createBlockedIpv4Addresses();
18
18
  const blockedIpv6Addresses = createBlockedIpv6Addresses();
19
19
  const allowedIpv6GlobalUnicastAddresses = createAllowedIpv6GlobalUnicastAddresses();
@@ -261,7 +261,7 @@ function requireStorePackageVersion(input, label) {
261
261
  }
262
262
  const numbers = parts.map(Number);
263
263
  if (numbers[0] === 0
264
- || numbers.some((part) => part > 65535)
264
+ || numbers.some((part) => part > 65_535)
265
265
  || (numbers[3] ?? 0) !== 0) {
266
266
  throw new Error(`${label} must start from 1, keep every part at or below 65535, and end in .0.`);
267
267
  }
@@ -10,7 +10,7 @@ export function assertMicrosoftStorePackageZip(file, sizeBytes) {
10
10
  if (header.readUInt32LE(0) !== 0x04034b50) {
11
11
  throw new Error('PWABuilder package archive must start with a ZIP local file header.');
12
12
  }
13
- const tailLength = Math.min(sizeBytes, 65557);
13
+ const tailLength = Math.min(sizeBytes, 65_557);
14
14
  const tail = Buffer.allocUnsafe(tailLength);
15
15
  readExactly(descriptor, tail, sizeBytes - tailLength);
16
16
  const endOffset = findEndOfCentralDirectory(tail);
@@ -7,7 +7,7 @@ import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsol
7
7
  import { isMicrosoftStoreSupportedListingLocale } from './microsoft-store-supported-locales.js';
8
8
  export const microsoftStoreSubmissionSchemaVersion = 1;
9
9
  // Microsoft Store listing and package identity limits documented by Microsoft Learn.
10
- const maximumStoreDescriptionCharacters = 10000;
10
+ const maximumStoreDescriptionCharacters = 10_000;
11
11
  const maximumStoreDesktopScreenshots = 10;
12
12
  const maximumStoreManifestIcons = 32;
13
13
  const maximumStoreScreenshotBytes = 50 * 1024 * 1024;
@@ -907,20 +907,20 @@ function pngPassLength(size, start, step) {
907
907
  return size <= start ? 0 : Math.ceil((size - start) / step);
908
908
  }
909
909
  function crc32Png(parts) {
910
- let crc = 4294967295;
910
+ let crc = 0xffff_ffff;
911
911
  for (const part of parts) {
912
912
  for (const byte of part) {
913
913
  crc = (crc >>> 8) ^ (pngCrc32Table[(crc ^ byte) & 0xff] ?? 0);
914
914
  }
915
915
  }
916
- return (crc ^ 4294967295) >>> 0;
916
+ return (crc ^ 0xffff_ffff) >>> 0;
917
917
  }
918
918
  function createPngCrc32Table() {
919
919
  const table = new Uint32Array(256);
920
920
  for (let value = 0; value < table.length; value += 1) {
921
921
  let crc = value;
922
922
  for (let bit = 0; bit < 8; bit += 1) {
923
- crc = (crc >>> 1) ^ (crc & 1 ? 3988292384 : 0);
923
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb8_8320 : 0);
924
924
  }
925
925
  table[value] = crc >>> 0;
926
926
  }