@mpgd/cli 0.28.1 → 0.30.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.
- package/dist/cloudflare-pages-static.d.ts +53 -0
- package/dist/cloudflare-pages-static.js +236 -0
- package/dist/hosted-pwa-deployment.d.ts +42 -0
- package/dist/hosted-pwa-deployment.js +1264 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +387 -2
- package/dist/microsoft-store-package-acceptance.js +19 -8
- package/dist/microsoft-store-pwa-release.d.ts +58 -0
- package/dist/microsoft-store-pwa-release.js +343 -0
- package/dist/offline-playtest.js +20 -4
- package/package.json +7 -7
- package/templates/phaser-game/agent/game-manifest.json +4 -0
- package/templates/phaser-game/mpgd.game.json +3 -0
- package/templates/phaser-game/src/entry.ts +10 -0
- package/templates/phaser-game/src/gameEntry.ts +10 -0
- package/templates/phaser-game/src/platform/textSelection.ts +121 -0
- package/templates/phaser-game/vite.shared.ts +14 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, type GameAcceptanceCommandResult, type GameAcceptanceCommandRunner, type GameAcceptanceReport, type GameAcceptanceStatus, type GameAcceptanceStep, type GameAcceptanceStepResult, type GameAcceptanceStepStatus, type RunGameAcceptanceInput, type RunGameAcceptanceResult, } from './game-acceptance.js';
|
|
2
2
|
export { createBrowserGameplayE2EDriver, type BrowserGameplayE2EInspectInput, type BrowserGameplayE2EPage, type BrowserGameplayE2EScreenshotOptions, type BrowserGameplayE2EViewport, type CreateBrowserGameplayE2EDriverInput, } from './browser-gameplay-e2e.js';
|
|
3
3
|
export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, type GameplayE2EAction, type GameplayE2EActionResult, type GameplayE2EDriver, type GameplayE2EHashLimits, type GameplayE2EInputAction, type GameplayE2EObservation, type GameplayE2EPauseResumeAction, type GameplayE2EPathEvidence, type GameplayE2EPlan, type GameplayE2EReport, type GameplayE2EResultStatus, type GameplayE2EState, type GameplayE2EStateResult, type GameplayE2EStatus, type RunGameplayE2EInput, type RunGameplayE2EResult, } from './gameplay-e2e.js';
|
|
4
|
+
export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, type CloudflarePagesDeploymentProfile, type HostedPwaDeploymentHost, type HostedPwaDeploymentVerification, type VerifyHostedPwaDeploymentInput, } from './hosted-pwa-deployment.js';
|
|
4
5
|
export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, type OfflinePlaytestEvidence, type RunOfflinePlaytestPackagingInput, type RunOfflinePlaytestPackagingResult, } from './offline-playtest.js';
|
|
5
6
|
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';
|
|
6
7
|
export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, type CreateMicrosoftStorePackageAcceptanceRuntimeInput, type MicrosoftStorePackageAcceptanceEvidence, type MicrosoftStorePackageAcceptanceRuntime, type MicrosoftStorePackageIdentity, type RunMicrosoftStorePackageAcceptanceInput, } from './microsoft-store-package-acceptance.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import completion from '@gunshi/plugin-completion';
|
|
@@ -7,8 +7,10 @@ import i18n, { defineI18n } from '@gunshi/plugin-i18n';
|
|
|
7
7
|
import resources from '@gunshi/resources';
|
|
8
8
|
import { cli } from 'gunshi';
|
|
9
9
|
import { normalizeConfiguredBuildTargets, normalizeBuildTarget as normalizeConfiguredTargetName, supportedBuildTargets, } from './build-targets.js';
|
|
10
|
+
import { escapeMarkdownInline as escapeEvidenceMarkdownInline, writeEvidenceReportFiles, } from './evidence-io.js';
|
|
10
11
|
import { defaultGameAcceptanceCommandTimeoutMs, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, } from './game-acceptance.js';
|
|
11
12
|
import { resolveGameplayE2EReportFile } from './gameplay-e2e.js';
|
|
13
|
+
import { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, verifyHostedPwaDeployment, } from './hosted-pwa-deployment.js';
|
|
12
14
|
import { createMicrosoftStorePackageAcceptanceRuntime, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
13
15
|
import { runMicrosoftStorePackageGeneration } from './microsoft-store-package-generation.js';
|
|
14
16
|
import { defaultMpgdKitPath, initializeMicrosoftStoreStarter, listMicrosoftStoreInitializerTemplateFiles, prepareBaseGameTemplateFile, resolveMicrosoftStoreAdapterDependencyVersion, } from './microsoft-store-starter.js';
|
|
@@ -18,6 +20,7 @@ import { targetConfigExtensionsFileEnv } from './target-config-env.js';
|
|
|
18
20
|
export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
|
|
19
21
|
export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
|
|
20
22
|
export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
|
|
23
|
+
export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, } from './hosted-pwa-deployment.js';
|
|
21
24
|
export { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, offlinePlaytestSchemaVersion, runOfflinePlaytestPackaging, } from './offline-playtest.js';
|
|
22
25
|
export { assertMicrosoftStorePwaCacheTransition, assertMicrosoftStorePwaPrecacheUrl, assertMicrosoftStorePwaReleaseEvidence, inspectMicrosoftStorePwaBrowserCacheTransition, microsoftStorePwaCacheSchema, readMicrosoftStorePwaBrowserReleaseEvidence, requestMicrosoftStorePwaBrowserUpdate, resolveMicrosoftStorePwaScopedCacheName, } from './microsoft-store-pwa-e2e.js';
|
|
23
26
|
export { createMicrosoftStorePackageAcceptanceRuntime, microsoftStorePackageAcceptanceSchemaVersion, parseMicrosoftStorePackageIdentity, parseWindowsAppCertificationResult, renderMicrosoftStorePackageAcceptanceMarkdown, runMicrosoftStorePackageAcceptance, } from './microsoft-store-package-acceptance.js';
|
|
@@ -140,6 +143,8 @@ const entryCommand = defineI18n({
|
|
|
140
143
|
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
141
144
|
}),
|
|
142
145
|
run: () => {
|
|
146
|
+
console.info('Use a sub-command: game, target, kit.');
|
|
147
|
+
console.info('Run "pnpm mpgd --help" for available commands.');
|
|
143
148
|
},
|
|
144
149
|
});
|
|
145
150
|
const gameCommand = defineI18n({
|
|
@@ -475,6 +480,9 @@ const gameCommand = defineI18n({
|
|
|
475
480
|
outputDir,
|
|
476
481
|
maximumBytes,
|
|
477
482
|
});
|
|
483
|
+
console.info(`Offline playtest (test only): ${result.entryFile}`);
|
|
484
|
+
console.info(`Evidence: ${result.evidenceFile}`);
|
|
485
|
+
console.info('Not a release target, deployment artifact, PWA, or store-submission package.');
|
|
478
486
|
},
|
|
479
487
|
}),
|
|
480
488
|
icons: defineI18n({
|
|
@@ -531,10 +539,12 @@ const gameCommand = defineI18n({
|
|
|
531
539
|
}),
|
|
532
540
|
])),
|
|
533
541
|
run: () => {
|
|
542
|
+
console.info('Use "mpgd game icons generate <game>", verify, or inspect.');
|
|
534
543
|
},
|
|
535
544
|
}),
|
|
536
545
|
},
|
|
537
546
|
run: () => {
|
|
547
|
+
console.info('Use "mpgd game create <directory>", "mpgd game accept <game>", "mpgd game offline-playtest <game>", or "mpgd game icons generate <game>".');
|
|
538
548
|
},
|
|
539
549
|
});
|
|
540
550
|
const defaultLegalDir = 'legal';
|
|
@@ -696,9 +706,12 @@ function acceptGame(input) {
|
|
|
696
706
|
MPGD_GAMEPLAY_E2E_REPORT_FILE: gameplayE2EReportFile,
|
|
697
707
|
},
|
|
698
708
|
});
|
|
709
|
+
console.info(`[mpgd:accept] JSON report: ${result.jsonFile}`);
|
|
710
|
+
console.info(`[mpgd:accept] Markdown report: ${result.markdownFile}`);
|
|
699
711
|
if (result.report.status === 'failed') {
|
|
700
712
|
throw new Error(`Game acceptance failed. Report: ${result.markdownFile}`);
|
|
701
713
|
}
|
|
714
|
+
console.info('[mpgd:accept] passed');
|
|
702
715
|
}
|
|
703
716
|
function assertGameAcceptanceRoot(gameRoot) {
|
|
704
717
|
const resolved = path.resolve(gameRoot);
|
|
@@ -782,6 +795,7 @@ const legalCommand = defineI18n({
|
|
|
782
795
|
...resolveLegalCommandOptions(ctx.values),
|
|
783
796
|
check: false,
|
|
784
797
|
});
|
|
798
|
+
console.info(`Built legal site: ${result.outDir}`);
|
|
785
799
|
},
|
|
786
800
|
}),
|
|
787
801
|
check: defineI18n({
|
|
@@ -797,10 +811,12 @@ const legalCommand = defineI18n({
|
|
|
797
811
|
...resolveLegalCommandOptions(ctx.values),
|
|
798
812
|
check: true,
|
|
799
813
|
});
|
|
814
|
+
console.info(`Legal site checked: ${result.outDir}`);
|
|
800
815
|
},
|
|
801
816
|
}),
|
|
802
817
|
},
|
|
803
818
|
run: () => {
|
|
819
|
+
console.info('Use "mpgd legal build" or "mpgd legal check".');
|
|
804
820
|
},
|
|
805
821
|
});
|
|
806
822
|
const targetCommand = defineI18n({
|
|
@@ -870,9 +886,10 @@ const targetCommand = defineI18n({
|
|
|
870
886
|
const resolvedKitPath = configuredKitPath === undefined
|
|
871
887
|
? detectedKitRoot
|
|
872
888
|
: assertKitRoot(path.resolve(configuredKitPath));
|
|
889
|
+
const canonicalGameRoot = existsSync(gameRoot) ? realpathSync(gameRoot) : gameRoot;
|
|
873
890
|
const defaultKitPath = resolvedKitPath === undefined
|
|
874
891
|
? defaultMpgdKitPath
|
|
875
|
-
: toTemplatePath(path.relative(
|
|
892
|
+
: toTemplatePath(path.relative(canonicalGameRoot, resolvedKitPath) || '.');
|
|
876
893
|
const dryRun = ctx.values['dry-run'] === true;
|
|
877
894
|
const adapterDependencyVersion = resolveMicrosoftStoreAdapterDependencyVersion(requireMpgdDependencyVersion(resolveMpgdDependencyVersionReplacements({
|
|
878
895
|
workspace: false,
|
|
@@ -885,7 +902,9 @@ const targetCommand = defineI18n({
|
|
|
885
902
|
adapterDependencyVersion,
|
|
886
903
|
dryRun,
|
|
887
904
|
});
|
|
905
|
+
console.info(`${dryRun ? 'Would update' : 'Updated'} Microsoft Store starter workflow: ${result.changedFiles.length} file(s).`);
|
|
888
906
|
for (const file of result.changedFiles) {
|
|
907
|
+
console.info(`- ${file}`);
|
|
889
908
|
}
|
|
890
909
|
},
|
|
891
910
|
}),
|
|
@@ -989,6 +1008,241 @@ const targetCommand = defineI18n({
|
|
|
989
1008
|
});
|
|
990
1009
|
},
|
|
991
1010
|
}),
|
|
1011
|
+
'verify-deployment': defineI18n({
|
|
1012
|
+
name: 'verify-deployment',
|
|
1013
|
+
description: 'Verify that a hosted deployment directory serves a verified PWA artifact unchanged.',
|
|
1014
|
+
resource: commandResource({
|
|
1015
|
+
en: 'Verify that a hosted deployment directory serves a verified PWA artifact unchanged.',
|
|
1016
|
+
ko: '호스트 배포 디렉터리가 검증된 PWA 산출물을 변조 없이 서비스하는지 검증합니다.',
|
|
1017
|
+
}, {
|
|
1018
|
+
target: {
|
|
1019
|
+
en: 'Target that produced the source PWA artifact (microsoft-store).',
|
|
1020
|
+
ko: '원본 PWA 산출물을 생성한 타깃(microsoft-store).',
|
|
1021
|
+
},
|
|
1022
|
+
'deployment-root': {
|
|
1023
|
+
en: 'Host deployment directory to verify.',
|
|
1024
|
+
ko: '검증할 호스트 배포 디렉터리.',
|
|
1025
|
+
},
|
|
1026
|
+
'source-artifact-root': {
|
|
1027
|
+
en: 'Directory of the verified source PWA artifact.',
|
|
1028
|
+
ko: '검증된 원본 PWA 산출물 디렉터리.',
|
|
1029
|
+
},
|
|
1030
|
+
host: {
|
|
1031
|
+
en: `Hosting profile (${hostedPwaDeploymentHosts.join(', ')}).`,
|
|
1032
|
+
ko: `호스팅 프로필(${hostedPwaDeploymentHosts.join(', ')}).`,
|
|
1033
|
+
},
|
|
1034
|
+
profile: {
|
|
1035
|
+
en: `Host routing profile (${cloudflarePagesDeploymentProfiles.join(', ')}).`,
|
|
1036
|
+
ko: `호스트 라우팅 프로필(${cloudflarePagesDeploymentProfiles.join(', ')}).`,
|
|
1037
|
+
},
|
|
1038
|
+
'report-dir': {
|
|
1039
|
+
en: 'Directory for verification evidence files (defaults to the working directory).',
|
|
1040
|
+
ko: '검증 결과 파일을 기록할 디렉터리(기본값은 작업 디렉터리).',
|
|
1041
|
+
},
|
|
1042
|
+
}),
|
|
1043
|
+
args: {
|
|
1044
|
+
target: {
|
|
1045
|
+
type: 'positional',
|
|
1046
|
+
required: true,
|
|
1047
|
+
description: 'Target that produced the source PWA artifact (microsoft-store)',
|
|
1048
|
+
},
|
|
1049
|
+
'deployment-root': {
|
|
1050
|
+
type: 'string',
|
|
1051
|
+
required: true,
|
|
1052
|
+
description: 'Host deployment directory to verify.',
|
|
1053
|
+
},
|
|
1054
|
+
'source-artifact-root': {
|
|
1055
|
+
type: 'string',
|
|
1056
|
+
required: true,
|
|
1057
|
+
description: 'Directory of the verified source PWA artifact.',
|
|
1058
|
+
},
|
|
1059
|
+
host: {
|
|
1060
|
+
type: 'string',
|
|
1061
|
+
required: false,
|
|
1062
|
+
default: 'cloudflare-pages',
|
|
1063
|
+
description: `Hosting profile (${hostedPwaDeploymentHosts.join(', ')}).`,
|
|
1064
|
+
},
|
|
1065
|
+
profile: {
|
|
1066
|
+
type: 'string',
|
|
1067
|
+
required: false,
|
|
1068
|
+
default: 'api-only',
|
|
1069
|
+
description: `Host routing profile (${cloudflarePagesDeploymentProfiles.join(', ')}).`,
|
|
1070
|
+
},
|
|
1071
|
+
'report-dir': {
|
|
1072
|
+
type: 'string',
|
|
1073
|
+
required: false,
|
|
1074
|
+
description: 'Directory for verification evidence files (defaults to the working directory).',
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
run: (ctx) => {
|
|
1078
|
+
const positionals = readLocalPositionals(ctx.positionals, [
|
|
1079
|
+
'target',
|
|
1080
|
+
'verify-deployment',
|
|
1081
|
+
]);
|
|
1082
|
+
const target = normalizeConfiguredTargetName(readRequiredPositional(positionals, 0, 'target'));
|
|
1083
|
+
if (target !== 'microsoft-store') {
|
|
1084
|
+
throw new Error(`Hosted deployment verification is not available for target: ${target}`);
|
|
1085
|
+
}
|
|
1086
|
+
const deploymentRoot = readRequiredCliOption(ctx.values['deployment-root'], '--deployment-root');
|
|
1087
|
+
const sourceArtifactRoot = readRequiredCliOption(ctx.values['source-artifact-root'], '--source-artifact-root');
|
|
1088
|
+
const host = readOptionalString(ctx.values.host) ?? 'cloudflare-pages';
|
|
1089
|
+
const profile = readOptionalString(ctx.values.profile) ?? 'api-only';
|
|
1090
|
+
if (!hostedPwaDeploymentHosts.includes(host)) {
|
|
1091
|
+
throw new Error(`Unsupported hosted PWA deployment host: ${host} `
|
|
1092
|
+
+ `(supported: ${hostedPwaDeploymentHosts.join(', ')}).`);
|
|
1093
|
+
}
|
|
1094
|
+
if (!cloudflarePagesDeploymentProfiles.includes(profile)) {
|
|
1095
|
+
throw new Error(`Unsupported ${host} deployment profile: ${profile} `
|
|
1096
|
+
+ `(supported: ${cloudflarePagesDeploymentProfiles.join(', ')}).`);
|
|
1097
|
+
}
|
|
1098
|
+
const reportDir = path.resolve(readOptionalString(ctx.values['report-dir']) ?? process.cwd());
|
|
1099
|
+
assertReportDirectoryOutsideVerifiedTrees(reportDir, sourceArtifactRoot, deploymentRoot);
|
|
1100
|
+
assertReportFilesSafeToWrite(reportDir);
|
|
1101
|
+
const verification = verifyHostedPwaDeployment({
|
|
1102
|
+
sourceArtifactRoot,
|
|
1103
|
+
deploymentRoot,
|
|
1104
|
+
host: host,
|
|
1105
|
+
profile: profile,
|
|
1106
|
+
});
|
|
1107
|
+
mkdirSync(reportDir, { recursive: true });
|
|
1108
|
+
writeEvidenceReportFiles({
|
|
1109
|
+
jsonFile: path.join(reportDir, 'hosted-pwa-verification.json'),
|
|
1110
|
+
markdownFile: path.join(reportDir, 'hosted-pwa-verification.md'),
|
|
1111
|
+
report: verification,
|
|
1112
|
+
markdown: renderHostedPwaVerificationMarkdown(verification),
|
|
1113
|
+
});
|
|
1114
|
+
console.info(`Hosted PWA deployment verified: ${verification.verifiedGameFileCount} game `
|
|
1115
|
+
+ `file(s) from revision ${verification.revision} under the `
|
|
1116
|
+
+ `${verification.host}/${verification.profile} profile `
|
|
1117
|
+
+ `(routes: ${verification.workerRoutes.include.join(', ')}).`);
|
|
1118
|
+
},
|
|
1119
|
+
}),
|
|
1120
|
+
'preview-versions': defineI18n({
|
|
1121
|
+
name: 'preview-versions',
|
|
1122
|
+
description: 'Preview candidate platform version numbers for a release ledger without reserving them.',
|
|
1123
|
+
resource: commandResource({
|
|
1124
|
+
en: 'Preview candidate platform version numbers for a release ledger without reserving them.',
|
|
1125
|
+
ko: '릴리스 원장에 대한 플랫폼 버전 번호 후보를 예약 없이 미리 확인합니다.',
|
|
1126
|
+
}, {
|
|
1127
|
+
'ledger-file': {
|
|
1128
|
+
en: 'Path to the platform version ledger JSON file (read-only).',
|
|
1129
|
+
ko: '플랫폼 버전 원장 JSON 파일 경로(읽기 전용).',
|
|
1130
|
+
},
|
|
1131
|
+
'plan-file': {
|
|
1132
|
+
en: 'Optional prepared release plan JSON to revalidate and extend.',
|
|
1133
|
+
ko: '재검증하고 확장할 선택적 준비된 릴리스 계획 JSON.',
|
|
1134
|
+
},
|
|
1135
|
+
'game-id': {
|
|
1136
|
+
en: 'Game identifier used in the release buildId.',
|
|
1137
|
+
ko: '릴리스 buildId에 사용할 게임 식별자.',
|
|
1138
|
+
},
|
|
1139
|
+
'game-version': {
|
|
1140
|
+
en: 'Final game SemVer for the release.',
|
|
1141
|
+
ko: '릴리스의 최종 게임 SemVer.',
|
|
1142
|
+
},
|
|
1143
|
+
'source-git-sha': {
|
|
1144
|
+
en: 'Full 40-character Git SHA of the game source.',
|
|
1145
|
+
ko: '게임 소스의 전체 40자 Git SHA.',
|
|
1146
|
+
},
|
|
1147
|
+
'kit-git-sha': {
|
|
1148
|
+
en: 'Full 40-character Git SHA of the mpgd-kit checkout.',
|
|
1149
|
+
ko: 'mpgd-kit 체크아웃의 전체 40자 Git SHA.',
|
|
1150
|
+
},
|
|
1151
|
+
'target-config-digest': {
|
|
1152
|
+
en: '64-character SHA-256 digest of the game-owned target configuration.',
|
|
1153
|
+
ko: '게임 소유 타깃 설정의 64자 SHA-256 다이제스트.',
|
|
1154
|
+
},
|
|
1155
|
+
targets: {
|
|
1156
|
+
en: 'Comma-separated targets, optionally "name:hosted-content-only" for microsoft-store.',
|
|
1157
|
+
ko: '쉼표로 구분된 타깃. microsoft-store는 "name:hosted-content-only" 지정 가능.',
|
|
1158
|
+
},
|
|
1159
|
+
json: {
|
|
1160
|
+
en: 'Emit the candidate allocation as JSON.',
|
|
1161
|
+
ko: '후보 할당을 JSON으로 출력합니다.',
|
|
1162
|
+
},
|
|
1163
|
+
'kit-path': {
|
|
1164
|
+
en: 'Path to the mpgd-kit checkout.',
|
|
1165
|
+
ko: 'mpgd-kit 체크아웃 경로.',
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
args: {
|
|
1169
|
+
'ledger-file': {
|
|
1170
|
+
type: 'string',
|
|
1171
|
+
required: true,
|
|
1172
|
+
description: 'Path to the platform version ledger JSON file (read-only).',
|
|
1173
|
+
},
|
|
1174
|
+
'plan-file': {
|
|
1175
|
+
type: 'string',
|
|
1176
|
+
required: false,
|
|
1177
|
+
description: 'Optional prepared release plan JSON to revalidate and extend.',
|
|
1178
|
+
},
|
|
1179
|
+
'game-id': {
|
|
1180
|
+
type: 'string',
|
|
1181
|
+
required: true,
|
|
1182
|
+
description: 'Game identifier used in the release buildId.',
|
|
1183
|
+
},
|
|
1184
|
+
'game-version': {
|
|
1185
|
+
type: 'string',
|
|
1186
|
+
required: true,
|
|
1187
|
+
description: 'Final game SemVer for the release.',
|
|
1188
|
+
},
|
|
1189
|
+
'source-git-sha': {
|
|
1190
|
+
type: 'string',
|
|
1191
|
+
required: true,
|
|
1192
|
+
description: 'Full 40-character Git SHA of the game source.',
|
|
1193
|
+
},
|
|
1194
|
+
'kit-git-sha': {
|
|
1195
|
+
type: 'string',
|
|
1196
|
+
required: true,
|
|
1197
|
+
description: 'Full 40-character Git SHA of the mpgd-kit checkout.',
|
|
1198
|
+
},
|
|
1199
|
+
'target-config-digest': {
|
|
1200
|
+
type: 'string',
|
|
1201
|
+
required: true,
|
|
1202
|
+
description: '64-character SHA-256 digest of the game-owned target configuration.',
|
|
1203
|
+
},
|
|
1204
|
+
targets: {
|
|
1205
|
+
type: 'string',
|
|
1206
|
+
required: true,
|
|
1207
|
+
description: 'Comma-separated targets, optionally "name:hosted-content-only" for microsoft-store.',
|
|
1208
|
+
},
|
|
1209
|
+
json: {
|
|
1210
|
+
type: 'boolean',
|
|
1211
|
+
required: false,
|
|
1212
|
+
description: 'Emit the candidate allocation as JSON.',
|
|
1213
|
+
},
|
|
1214
|
+
'kit-path': {
|
|
1215
|
+
type: 'string',
|
|
1216
|
+
required: false,
|
|
1217
|
+
description: 'Path to the mpgd-kit checkout.',
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
run: (ctx) => {
|
|
1221
|
+
const kitPath = resolveKitPathForTarget(ctx.values);
|
|
1222
|
+
const planFile = readOptionalString(ctx.values['plan-file']);
|
|
1223
|
+
const env = {
|
|
1224
|
+
...process.env,
|
|
1225
|
+
MPGD_KIT_PATH: kitPath,
|
|
1226
|
+
MPGD_PREVIEW_LEDGER_FILE: path.resolve(readRequiredCliOption(ctx.values['ledger-file'], '--ledger-file')),
|
|
1227
|
+
MPGD_PREVIEW_GAME_ID: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
|
|
1228
|
+
MPGD_PREVIEW_GAME_VERSION: readRequiredCliOption(ctx.values['game-version'], '--game-version'),
|
|
1229
|
+
MPGD_PREVIEW_SOURCE_GIT_SHA: readRequiredCliOption(ctx.values['source-git-sha'], '--source-git-sha'),
|
|
1230
|
+
MPGD_PREVIEW_KIT_GIT_SHA: readRequiredCliOption(ctx.values['kit-git-sha'], '--kit-git-sha'),
|
|
1231
|
+
MPGD_PREVIEW_TARGET_CONFIG_DIGEST: readRequiredCliOption(ctx.values['target-config-digest'], '--target-config-digest'),
|
|
1232
|
+
MPGD_PREVIEW_TARGETS: readRequiredCliOption(ctx.values.targets, '--targets'),
|
|
1233
|
+
// Always define the optional toggles so ambient values from the
|
|
1234
|
+
// caller's environment cannot override absent flags.
|
|
1235
|
+
MPGD_PREVIEW_PLAN_FILE: planFile === undefined ? '' : path.resolve(planFile),
|
|
1236
|
+
MPGD_PREVIEW_JSON: ctx.values.json === true ? '1' : '',
|
|
1237
|
+
};
|
|
1238
|
+
if (ctx.values.json !== true) {
|
|
1239
|
+
console.info('[mpgd] target preview-versions (read-only candidates, not reservations)');
|
|
1240
|
+
}
|
|
1241
|
+
// --silent keeps pnpm's script echo out of stdout so the JSON document
|
|
1242
|
+
// is machine-readable apart from the framework banner.
|
|
1243
|
+
runPnpm(['--silent', 'preview:platform-versions'], env);
|
|
1244
|
+
},
|
|
1245
|
+
}),
|
|
992
1246
|
'generate-package': defineI18n({
|
|
993
1247
|
name: 'generate-package',
|
|
994
1248
|
description: 'Generate a packaged target artifact with an external platform service.',
|
|
@@ -1108,6 +1362,7 @@ const targetCommand = defineI18n({
|
|
|
1108
1362
|
jsonFile: path.join(outputDir, 'package-generation.json'),
|
|
1109
1363
|
markdownFile: path.join(outputDir, 'package-generation.md'),
|
|
1110
1364
|
});
|
|
1365
|
+
console.info(`Microsoft Store package ZIP downloaded to ${evidence.archive.file}; package inspection remains required.`);
|
|
1111
1366
|
},
|
|
1112
1367
|
}),
|
|
1113
1368
|
preflight: defineI18n({
|
|
@@ -1186,6 +1441,7 @@ const targetCommand = defineI18n({
|
|
|
1186
1441
|
jsonFile: path.join(outputDir, 'submission-preflight.json'),
|
|
1187
1442
|
markdownFile: path.join(outputDir, 'submission-preflight.md'),
|
|
1188
1443
|
});
|
|
1444
|
+
console.info(`Microsoft Store submission preflight passed: ${Object.keys(evidence.listing.locales).length} listing locale(s), ${evidence.warnings.length} warning(s).`);
|
|
1189
1445
|
},
|
|
1190
1446
|
}),
|
|
1191
1447
|
'accept-package': defineI18n({
|
|
@@ -1300,6 +1556,7 @@ const targetCommand = defineI18n({
|
|
|
1300
1556
|
...(appCertExecutable === undefined ? {} : { appCertExecutable }),
|
|
1301
1557
|
...(makeAppxExecutable === undefined ? {} : { makeAppxExecutable }),
|
|
1302
1558
|
}));
|
|
1559
|
+
console.info(`Microsoft Store package acceptance passed: ${evidence.packages.length} package(s).`);
|
|
1303
1560
|
},
|
|
1304
1561
|
}),
|
|
1305
1562
|
'build-all': defineI18n({
|
|
@@ -1400,6 +1657,7 @@ const targetCommand = defineI18n({
|
|
|
1400
1657
|
}),
|
|
1401
1658
|
},
|
|
1402
1659
|
run: () => {
|
|
1660
|
+
console.info('Use "mpgd target init <target>", "mpgd target build <target>", "mpgd target smoke <target>", "mpgd target generate-package <target>", "mpgd target preflight <target>", or "mpgd target accept-package <target> --packages <paths>".');
|
|
1403
1661
|
},
|
|
1404
1662
|
});
|
|
1405
1663
|
const kitCommand = defineI18n({
|
|
@@ -1418,10 +1676,17 @@ const kitCommand = defineI18n({
|
|
|
1418
1676
|
ko: 'CLI, kit, template, target wrapper 상태를 출력합니다.',
|
|
1419
1677
|
}),
|
|
1420
1678
|
run: () => {
|
|
1679
|
+
console.info(`cli package: ${packageRoot}`);
|
|
1680
|
+
console.info(`mpgd-kit: ${detectedKitRoot ?? 'not detected'}`);
|
|
1681
|
+
console.info(`cli template: ${existsSync(gameTemplateDir) ? 'ok' : 'missing'}`);
|
|
1682
|
+
console.info(`mobile wrapper: ${kitFileStatus('apps/mobile-capacitor/package.json')}`);
|
|
1683
|
+
console.info(`ait wrapper: ${kitFileStatus('apps/target-ait/package.json')}`);
|
|
1684
|
+
console.info(`devvit wrapper: ${kitFileStatus('apps/target-devvit/package.json')}`);
|
|
1421
1685
|
},
|
|
1422
1686
|
}),
|
|
1423
1687
|
},
|
|
1424
1688
|
run: () => {
|
|
1689
|
+
console.info('Use "mpgd kit doctor".');
|
|
1425
1690
|
},
|
|
1426
1691
|
});
|
|
1427
1692
|
function commandResource(description, args = {}) {
|
|
@@ -1650,10 +1915,12 @@ function createGameApp(input) {
|
|
|
1650
1915
|
: [{ ...file, content: renderTemplate(prepared, context) }];
|
|
1651
1916
|
});
|
|
1652
1917
|
if (input.dryRun) {
|
|
1918
|
+
console.info(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
|
|
1653
1919
|
const plannedFiles = input.microsoftStore
|
|
1654
1920
|
? [...files.map((file) => file.relativePath), ...listMicrosoftStoreInitializerTemplateFiles()]
|
|
1655
1921
|
: files.map((file) => file.relativePath);
|
|
1656
1922
|
for (const relativePath of [...new Set(plannedFiles)].sort()) {
|
|
1923
|
+
console.info(`- ${path.join(path.relative(process.cwd(), appDir), relativePath)}`);
|
|
1657
1924
|
}
|
|
1658
1925
|
return;
|
|
1659
1926
|
}
|
|
@@ -1688,6 +1955,20 @@ function createGameApp(input) {
|
|
|
1688
1955
|
}
|
|
1689
1956
|
throw error;
|
|
1690
1957
|
}
|
|
1958
|
+
console.info(`Created ${appDir}`);
|
|
1959
|
+
console.info('Next steps:');
|
|
1960
|
+
console.info(` cd ${appDir}`);
|
|
1961
|
+
console.info(' pnpm install -w');
|
|
1962
|
+
console.info(' pnpm dev');
|
|
1963
|
+
console.info('Target builds require an mpgd-kit checkout:');
|
|
1964
|
+
console.info([
|
|
1965
|
+
' mpgd target build-all',
|
|
1966
|
+
`--targets-file ${path.join(appDir, 'mpgd.targets.json')}`,
|
|
1967
|
+
`--targets ${context.recommendedMatrixTargets}`,
|
|
1968
|
+
'--profile staging',
|
|
1969
|
+
'--ait-variant wrapper',
|
|
1970
|
+
'--kit-path <path-to-mpgd-kit>',
|
|
1971
|
+
].join(' '));
|
|
1691
1972
|
}
|
|
1692
1973
|
function assertValidGameName(name) {
|
|
1693
1974
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
@@ -2067,6 +2348,7 @@ function runTargetCommand(input) {
|
|
|
2067
2348
|
const args = input.action === 'build'
|
|
2068
2349
|
? ['build:target', target, profile]
|
|
2069
2350
|
: ['smoke:target', target];
|
|
2351
|
+
console.info(`[mpgd] ${input.action} ${target}`);
|
|
2070
2352
|
runPnpm(args, env);
|
|
2071
2353
|
}
|
|
2072
2354
|
function runTargetMatrix(input) {
|
|
@@ -2258,6 +2540,109 @@ function readLocalPositionals(positionals, commandPath) {
|
|
|
2258
2540
|
}
|
|
2259
2541
|
return positionals.slice(commandPath.length);
|
|
2260
2542
|
}
|
|
2543
|
+
/** Reject existing evidence destinations that are links into verified trees. */
|
|
2544
|
+
function assertReportFilesSafeToWrite(reportDir) {
|
|
2545
|
+
for (const name of ['hosted-pwa-verification.json', 'hosted-pwa-verification.md']) {
|
|
2546
|
+
const destination = path.join(reportDir, name);
|
|
2547
|
+
let stat;
|
|
2548
|
+
try {
|
|
2549
|
+
stat = lstatSync(destination);
|
|
2550
|
+
}
|
|
2551
|
+
catch {
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
if (stat.isSymbolicLink()) {
|
|
2555
|
+
throw new Error(`The report destination ${destination} is a symbolic link; refusing to follow `
|
|
2556
|
+
+ 'it because the report write must stay outside the verified trees.');
|
|
2557
|
+
}
|
|
2558
|
+
if (!stat.isFile()) {
|
|
2559
|
+
throw new Error(`The report destination ${destination} is not a regular file; refusing to `
|
|
2560
|
+
+ 'write evidence through sockets, FIFOs, devices, or directories.');
|
|
2561
|
+
}
|
|
2562
|
+
if (stat.nlink > 1) {
|
|
2563
|
+
throw new Error(`The report destination ${destination} has ${String(stat.nlink)} hard links; `
|
|
2564
|
+
+ 'refusing to write through a shared inode into the verified trees.');
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
function assertReportDirectoryOutsideVerifiedTrees(reportDir, sourceArtifactRoot, deploymentRoot) {
|
|
2569
|
+
const canonicalReportDir = realpathThroughExistingAncestor(reportDir);
|
|
2570
|
+
for (const [label, root] of [
|
|
2571
|
+
['source artifact', sourceArtifactRoot],
|
|
2572
|
+
['deployment', deploymentRoot],
|
|
2573
|
+
]) {
|
|
2574
|
+
const resolvedRoot = realpathThroughExistingAncestor(root);
|
|
2575
|
+
const distance = path.relative(resolvedRoot, canonicalReportDir);
|
|
2576
|
+
if (distance === '' || (!distance.startsWith('..') && !path.isAbsolute(distance))) {
|
|
2577
|
+
throw new Error(`The --report-dir must stay outside the verified ${label} directory; `
|
|
2578
|
+
+ `${reportDir} is inside ${resolvedRoot}, and writing evidence there would `
|
|
2579
|
+
+ 'mutate a read-only verification input.');
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
/** Canonicalize a possibly missing path through its nearest existing ancestor. */
|
|
2584
|
+
function realpathThroughExistingAncestor(candidate) {
|
|
2585
|
+
let absolute = path.resolve(candidate);
|
|
2586
|
+
while (true) {
|
|
2587
|
+
try {
|
|
2588
|
+
return realpathSync(absolute);
|
|
2589
|
+
}
|
|
2590
|
+
catch {
|
|
2591
|
+
const parent = path.dirname(absolute);
|
|
2592
|
+
if (parent === absolute) {
|
|
2593
|
+
return absolute;
|
|
2594
|
+
}
|
|
2595
|
+
absolute = parent;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
/** Markdown-escape a verification value, normalizing CR line endings first. */
|
|
2600
|
+
function escapeVerificationMarkdown(value) {
|
|
2601
|
+
return escapeEvidenceMarkdownInline(value.replaceAll('\r\n', ' ').replaceAll('\r', ' '));
|
|
2602
|
+
}
|
|
2603
|
+
function renderHostedPwaVerificationMarkdown(verification) {
|
|
2604
|
+
return [
|
|
2605
|
+
'# Hosted PWA Deployment Verification',
|
|
2606
|
+
'',
|
|
2607
|
+
`- Host: ${escapeVerificationMarkdown(verification.host)} (${escapeVerificationMarkdown(verification.profile)} profile)`,
|
|
2608
|
+
`- Source artifact: ${escapeVerificationMarkdown(verification.sourceArtifactRoot)}`,
|
|
2609
|
+
`- Deployment: ${escapeVerificationMarkdown(verification.deploymentRoot)}`,
|
|
2610
|
+
'- Release: '
|
|
2611
|
+
+ `${escapeVerificationMarkdown(verification.appVersion)} (build `
|
|
2612
|
+
+ `${escapeVerificationMarkdown(verification.buildId)}, revision `
|
|
2613
|
+
+ `${escapeVerificationMarkdown(verification.revision)})`,
|
|
2614
|
+
`- Verified game files: ${verification.verifiedGameFileCount}`,
|
|
2615
|
+
`- Recognized host files: ${verification.hostFileCount}`,
|
|
2616
|
+
`- Worker routes (include): ${escapeVerificationMarkdown(verification.workerRoutes.include.join(', '))}`,
|
|
2617
|
+
'',
|
|
2618
|
+
'## Verified',
|
|
2619
|
+
'',
|
|
2620
|
+
'- Every source artifact file is present in the deployment with an identical sha256 digest.',
|
|
2621
|
+
'- The source artifact release evidence revision recomputes from its own files.',
|
|
2622
|
+
'- All local index.html references resolve inside the deployment.',
|
|
2623
|
+
'- The `_routes.json` worker routing matches the reviewed profile exactly.',
|
|
2624
|
+
'- Cache-Control policies are explicit for every asset URL: immutable',
|
|
2625
|
+
' (content-hashed pipelines) or revalidation (stable names) both satisfy',
|
|
2626
|
+
' the contract; missing or conflicting policies fail. The service worker',
|
|
2627
|
+
' and stable-name icons use no-store; the index, manifest, release',
|
|
2628
|
+
' evidence, and legal pages use revalidation.',
|
|
2629
|
+
'- No redirect moves the PWA entry point or PWA-critical files.',
|
|
2630
|
+
'',
|
|
2631
|
+
'## Not verified',
|
|
2632
|
+
'',
|
|
2633
|
+
'- The Pages worker runtime behavior; exercise it with the host preview',
|
|
2634
|
+
' (for example `wrangler pages dev`) before deploying.',
|
|
2635
|
+
'- CDN or account-level cache settings outside the deployment directory.',
|
|
2636
|
+
'',
|
|
2637
|
+
].join('\n');
|
|
2638
|
+
}
|
|
2639
|
+
function readRequiredCliOption(value, label) {
|
|
2640
|
+
const read = readOptionalString(value);
|
|
2641
|
+
if (read === undefined) {
|
|
2642
|
+
throw new Error(`${label} is required.`);
|
|
2643
|
+
}
|
|
2644
|
+
return read;
|
|
2645
|
+
}
|
|
2261
2646
|
function readOptionalString(value) {
|
|
2262
2647
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
2263
2648
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { closeSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { closeSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
@@ -85,10 +85,10 @@ export function runMicrosoftStorePackageAcceptance(input, runtime = createMicros
|
|
|
85
85
|
...packageFiles.map((file) => ({ file, label: 'Microsoft Store package' })),
|
|
86
86
|
]);
|
|
87
87
|
for (const reportFile of reportFiles) {
|
|
88
|
-
|
|
88
|
+
removeEvidenceFile(reportFile);
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
removeEvidenceFile(jsonFile);
|
|
91
|
+
removeEvidenceFile(markdownFile);
|
|
92
92
|
const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-microsoft-store-package-'));
|
|
93
93
|
try {
|
|
94
94
|
const packages = packageFiles.map((packageFile, index) => {
|
|
@@ -136,10 +136,10 @@ export function runMicrosoftStorePackageAcceptance(input, runtime = createMicros
|
|
|
136
136
|
writeFileSync(markdownFile, renderMicrosoftStorePackageAcceptanceMarkdown(evidence));
|
|
137
137
|
}
|
|
138
138
|
catch (error) {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
removeEvidenceFile(jsonFile);
|
|
140
|
+
removeEvidenceFile(markdownFile);
|
|
141
141
|
for (const reportFile of reportFiles) {
|
|
142
|
-
|
|
142
|
+
removeEvidenceFile(reportFile);
|
|
143
143
|
}
|
|
144
144
|
throw error;
|
|
145
145
|
}
|
|
@@ -170,7 +170,7 @@ function runOptionalWindowsAppCertification(input) {
|
|
|
170
170
|
if (reportFile === undefined) {
|
|
171
171
|
throw new Error('Windows App Certification Kit report path was not prepared.');
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
removeEvidenceFile(reportFile);
|
|
174
174
|
input.runtime.runCommand(appCertExecutable, ['reset']);
|
|
175
175
|
input.runtime.runCommand(appCertExecutable, [
|
|
176
176
|
'test',
|
|
@@ -714,6 +714,17 @@ function sameFile(left, right) {
|
|
|
714
714
|
return false;
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
|
+
/** Remove only the file or link, never traverse a replacement directory link. */
|
|
718
|
+
function removeEvidenceFile(file) {
|
|
719
|
+
try {
|
|
720
|
+
unlinkSync(file);
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (!isMissingFileError(error)) {
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
717
728
|
function isMissingFileError(error) {
|
|
718
729
|
if (typeof error !== 'object' || error === null) {
|
|
719
730
|
return false;
|