@mpgd/cli 0.6.0 → 0.8.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/game-acceptance.d.ts +65 -0
- package/dist/game-acceptance.js +245 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +398 -32
- package/dist/production-target-readiness.d.ts +8 -0
- package/dist/production-target-readiness.js +207 -0
- package/package.json +15 -15
- package/templates/phaser-game/README.md +71 -1
- package/templates/phaser-game/agent/acceptance.md +12 -6
- package/templates/phaser-game/agent/brief.md +3 -1
- package/templates/phaser-game/agent/game-manifest.json +7 -0
- package/templates/phaser-game/apps/target-cloudflare-pages/package.json +3 -3
- package/templates/phaser-game/apps/target-devvit/README.md +70 -4
- package/templates/phaser-game/apps/target-devvit/devvit.json +8 -1
- package/templates/phaser-game/apps/target-devvit/package.json +15 -20
- package/templates/phaser-game/apps/target-devvit/src/client/game.html +13 -0
- package/templates/phaser-game/apps/target-devvit/src/client/game.ts +3 -0
- package/templates/phaser-game/apps/target-devvit/src/client/index.html +13 -0
- package/templates/phaser-game/apps/target-devvit/src/client/preview.ts +3 -0
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +228 -116
- package/templates/phaser-game/apps/target-devvit/src/server/postOperationStore.ts +9 -0
- package/templates/phaser-game/apps/target-devvit/tsconfig.json +7 -2
- package/templates/phaser-game/apps/target-devvit/vite.config.ts +41 -0
- package/templates/phaser-game/game.html +13 -0
- package/templates/phaser-game/gitignore +2 -0
- package/templates/phaser-game/index.html +2 -2
- package/templates/phaser-game/mpgd.game.json +8 -0
- package/templates/phaser-game/mpgd.targets.json +7 -0
- package/templates/phaser-game/package.json +10 -4
- package/templates/phaser-game/public/manifest.webmanifest +1 -0
- package/templates/phaser-game/src/entry.ts +7 -0
- package/templates/phaser-game/src/env.d.ts +3 -0
- package/templates/phaser-game/src/gameEntry.ts +3 -0
- package/templates/phaser-game/src/main.ts +11 -2
- package/templates/phaser-game/src/platform/buildGatewayModule.ts +5 -0
- package/templates/phaser-game/src/platform/buildGateways/ait.ts +11 -0
- package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/browser.ts +8 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +12 -0
- package/templates/phaser-game/src/platform/buildGateways/reddit.ts +11 -0
- package/templates/phaser-game/src/platform/buildGateways/redditSandbox.ts +15 -0
- package/templates/phaser-game/src/platform/devvitEntrypoint.ts +57 -0
- package/templates/phaser-game/src/platform/devvitInlinePreview.css +75 -0
- package/templates/phaser-game/src/platform/gameServices.ts +18 -61
- package/templates/phaser-game/src/platform/installPlatform.ts +6 -50
- package/templates/phaser-game/src/platform/microsoftStorePwa.ts +99 -0
- package/templates/phaser-game/tools/run-game-acceptance.mjs +22 -0
- package/templates/phaser-game/vite.config.ts +24 -73
- package/templates/phaser-game/vite.shared.ts +217 -0
- package/templates/phaser-game/apps/target-devvit/vite.server.config.ts +0 -37
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export declare const defaultGameAcceptanceCommandTimeoutMs: number;
|
|
2
|
+
export declare function resolveGameAcceptanceReleaseManifestFile(gameRoot: string, env?: NodeJS.ProcessEnv): string;
|
|
3
|
+
export type GameAcceptanceStatus = 'failed' | 'passed';
|
|
4
|
+
export type GameAcceptanceStepStatus = 'failed' | 'passed' | 'skipped';
|
|
5
|
+
export interface GameAcceptanceStep {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly command?: string;
|
|
9
|
+
readonly args?: readonly string[];
|
|
10
|
+
readonly cwd?: string;
|
|
11
|
+
readonly skipReason?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GameAcceptanceStepResult {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly label: string;
|
|
16
|
+
readonly command: string | null;
|
|
17
|
+
readonly args: readonly string[];
|
|
18
|
+
readonly cwd: string | null;
|
|
19
|
+
readonly status: GameAcceptanceStepStatus;
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly startedAt: string;
|
|
22
|
+
readonly durationMs: number;
|
|
23
|
+
readonly detail: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface GameAcceptanceReport {
|
|
26
|
+
readonly schemaVersion: 1;
|
|
27
|
+
readonly generatedAt: string;
|
|
28
|
+
readonly status: GameAcceptanceStatus;
|
|
29
|
+
readonly gameRoot: string;
|
|
30
|
+
readonly durationMs: number;
|
|
31
|
+
readonly options: Readonly<Record<string, string | boolean | null>>;
|
|
32
|
+
readonly steps: readonly GameAcceptanceStepResult[];
|
|
33
|
+
readonly evidence: {
|
|
34
|
+
readonly releaseManifest: {
|
|
35
|
+
readonly file: string;
|
|
36
|
+
readonly found: boolean;
|
|
37
|
+
readonly parseError: string | null;
|
|
38
|
+
readonly value: unknown;
|
|
39
|
+
} | null;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface GameAcceptanceCommandResult {
|
|
43
|
+
readonly exitCode: number;
|
|
44
|
+
readonly detail?: string;
|
|
45
|
+
}
|
|
46
|
+
export type GameAcceptanceCommandRunner = (step: Required<Pick<GameAcceptanceStep, 'command' | 'args' | 'cwd'>>) => GameAcceptanceCommandResult;
|
|
47
|
+
export interface RunGameAcceptanceInput {
|
|
48
|
+
readonly gameRoot: string;
|
|
49
|
+
readonly reportDir: string;
|
|
50
|
+
readonly releaseManifestFile?: string;
|
|
51
|
+
readonly options: Readonly<Record<string, string | boolean | null>>;
|
|
52
|
+
readonly steps: readonly GameAcceptanceStep[];
|
|
53
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
54
|
+
readonly commandTimeoutMs?: number;
|
|
55
|
+
readonly commandRunner?: GameAcceptanceCommandRunner;
|
|
56
|
+
readonly now?: () => number;
|
|
57
|
+
readonly log?: (message: string) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface RunGameAcceptanceResult {
|
|
60
|
+
readonly report: GameAcceptanceReport;
|
|
61
|
+
readonly jsonFile: string;
|
|
62
|
+
readonly markdownFile: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function runGameAcceptance(input: RunGameAcceptanceInput): RunGameAcceptanceResult;
|
|
65
|
+
export declare function renderGameAcceptanceMarkdown(report: GameAcceptanceReport): string;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const defaultGameAcceptanceCommandTimeoutMs = 30 * 60 * 1000;
|
|
5
|
+
const defaultGameAcceptanceReleaseManifestFile = 'artifacts/release-manifest.json';
|
|
6
|
+
export function resolveGameAcceptanceReleaseManifestFile(gameRoot, env = process.env) {
|
|
7
|
+
const configuredFile = env.MPGD_RELEASE_MANIFEST_FILE;
|
|
8
|
+
return path.resolve(gameRoot, configuredFile === undefined || configuredFile.length === 0
|
|
9
|
+
? defaultGameAcceptanceReleaseManifestFile
|
|
10
|
+
: configuredFile);
|
|
11
|
+
}
|
|
12
|
+
export function runGameAcceptance(input) {
|
|
13
|
+
const now = input.now ?? Date.now;
|
|
14
|
+
const log = input.log ?? console.log;
|
|
15
|
+
const commandTimeoutMs = input.commandTimeoutMs ?? defaultGameAcceptanceCommandTimeoutMs;
|
|
16
|
+
if (!Number.isInteger(commandTimeoutMs) || commandTimeoutMs <= 0) {
|
|
17
|
+
throw new Error(`Acceptance command timeout must be a positive integer: ${commandTimeoutMs}`);
|
|
18
|
+
}
|
|
19
|
+
const commandRunner = input.commandRunner
|
|
20
|
+
?? ((step) => runAcceptanceCommand(step, input.env ?? process.env, commandTimeoutMs));
|
|
21
|
+
const gameRoot = path.resolve(input.gameRoot);
|
|
22
|
+
const startedAtMs = now();
|
|
23
|
+
const results = [];
|
|
24
|
+
let failed = false;
|
|
25
|
+
for (const step of input.steps) {
|
|
26
|
+
if (failed) {
|
|
27
|
+
results.push(skippedStepResult(step, now(), 'A previous acceptance step failed.'));
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (step.skipReason !== undefined) {
|
|
31
|
+
log(`[mpgd:accept] skipped ${step.label}: ${step.skipReason}`);
|
|
32
|
+
results.push(skippedStepResult(step, now(), step.skipReason));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const runnableStep = resolveRunnableStep(step);
|
|
36
|
+
if (!runnableStep.ok) {
|
|
37
|
+
results.push(failedStepResult(step, now(), runnableStep.detail));
|
|
38
|
+
failed = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const command = runnableStep.command;
|
|
42
|
+
const args = step.args ?? [];
|
|
43
|
+
const cwd = path.resolve(runnableStep.cwd);
|
|
44
|
+
const stepStartedAtMs = now();
|
|
45
|
+
const startedAt = new Date(stepStartedAtMs).toISOString();
|
|
46
|
+
log(`[mpgd:accept] ${step.label}`);
|
|
47
|
+
let commandResult;
|
|
48
|
+
try {
|
|
49
|
+
commandResult = commandRunner({ command, args, cwd });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
commandResult = {
|
|
53
|
+
exitCode: 1,
|
|
54
|
+
detail: formatError(error),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const status = commandResult.exitCode === 0 ? 'passed' : 'failed';
|
|
58
|
+
results.push({
|
|
59
|
+
id: step.id,
|
|
60
|
+
label: step.label,
|
|
61
|
+
command,
|
|
62
|
+
args,
|
|
63
|
+
cwd,
|
|
64
|
+
status,
|
|
65
|
+
exitCode: commandResult.exitCode,
|
|
66
|
+
startedAt,
|
|
67
|
+
durationMs: Math.max(0, now() - stepStartedAtMs),
|
|
68
|
+
detail: commandResult.detail ?? null,
|
|
69
|
+
});
|
|
70
|
+
failed = status === 'failed';
|
|
71
|
+
}
|
|
72
|
+
const finishedAtMs = now();
|
|
73
|
+
const releaseManifest = readOptionalJsonEvidence(input.releaseManifestFile, gameRoot);
|
|
74
|
+
const report = {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
generatedAt: new Date(finishedAtMs).toISOString(),
|
|
77
|
+
status: failed ? 'failed' : 'passed',
|
|
78
|
+
gameRoot,
|
|
79
|
+
durationMs: Math.max(0, finishedAtMs - startedAtMs),
|
|
80
|
+
options: input.options,
|
|
81
|
+
steps: results,
|
|
82
|
+
evidence: {
|
|
83
|
+
releaseManifest,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const reportDir = path.resolve(input.reportDir);
|
|
87
|
+
const jsonFile = path.join(reportDir, 'acceptance-report.json');
|
|
88
|
+
const markdownFile = path.join(reportDir, 'acceptance-report.md');
|
|
89
|
+
mkdirSync(reportDir, { recursive: true });
|
|
90
|
+
writeFileSync(jsonFile, `${JSON.stringify(report, null, 2)}\n`);
|
|
91
|
+
writeFileSync(markdownFile, renderGameAcceptanceMarkdown(report));
|
|
92
|
+
return { report, jsonFile, markdownFile };
|
|
93
|
+
}
|
|
94
|
+
export function renderGameAcceptanceMarkdown(report) {
|
|
95
|
+
const lines = [
|
|
96
|
+
'# Game Acceptance Report',
|
|
97
|
+
'',
|
|
98
|
+
`Status: ${report.status}`,
|
|
99
|
+
`Generated: ${report.generatedAt}`,
|
|
100
|
+
`Game root: ${escapeMarkdownInline(report.gameRoot)}`,
|
|
101
|
+
`Duration: ${formatDuration(report.durationMs)}`,
|
|
102
|
+
'',
|
|
103
|
+
'## Steps',
|
|
104
|
+
'',
|
|
105
|
+
'| Step | Status | Duration | Detail |',
|
|
106
|
+
'| --- | --- | ---: | --- |',
|
|
107
|
+
...report.steps.map((step) => [
|
|
108
|
+
escapeMarkdownTable(step.label),
|
|
109
|
+
step.status,
|
|
110
|
+
formatDuration(step.durationMs),
|
|
111
|
+
escapeMarkdownTable(step.detail ?? ''),
|
|
112
|
+
].join(' | ')).map((row) => `| ${row} |`),
|
|
113
|
+
'',
|
|
114
|
+
'## Release Evidence',
|
|
115
|
+
'',
|
|
116
|
+
];
|
|
117
|
+
if (report.evidence.releaseManifest === null) {
|
|
118
|
+
lines.push('- Release manifest collection disabled.');
|
|
119
|
+
}
|
|
120
|
+
else if (!report.evidence.releaseManifest.found) {
|
|
121
|
+
lines.push(`- Release manifest not found: ${escapeMarkdownInline(report.evidence.releaseManifest.file)}`);
|
|
122
|
+
}
|
|
123
|
+
else if (report.evidence.releaseManifest.parseError !== null) {
|
|
124
|
+
lines.push(`- Release manifest is invalid: ${escapeMarkdownInline(report.evidence.releaseManifest.file)}`, ` - ${escapeMarkdownInline(report.evidence.releaseManifest.parseError)}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
lines.push(`- Release manifest: ${escapeMarkdownInline(report.evidence.releaseManifest.file)}`);
|
|
128
|
+
}
|
|
129
|
+
return `${lines.join('\n')}\n`;
|
|
130
|
+
}
|
|
131
|
+
function runAcceptanceCommand(step, env, timeoutMs) {
|
|
132
|
+
const result = spawnSync(step.command, [...step.args], {
|
|
133
|
+
cwd: step.cwd,
|
|
134
|
+
env,
|
|
135
|
+
stdio: 'inherit',
|
|
136
|
+
shell: process.platform === 'win32',
|
|
137
|
+
timeout: timeoutMs,
|
|
138
|
+
killSignal: 'SIGTERM',
|
|
139
|
+
});
|
|
140
|
+
if (result.error !== undefined) {
|
|
141
|
+
return {
|
|
142
|
+
exitCode: 1,
|
|
143
|
+
detail: isTimeoutError(result.error)
|
|
144
|
+
? `Command timed out after ${timeoutMs}ms.`
|
|
145
|
+
: result.error.message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
exitCode: result.status ?? 1,
|
|
150
|
+
...(result.signal === null ? {} : { detail: `Terminated by ${result.signal}.` }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function failedStepResult(step, nowMs, detail) {
|
|
154
|
+
return {
|
|
155
|
+
id: step.id,
|
|
156
|
+
label: step.label,
|
|
157
|
+
command: normalizeStepCommand(step.command),
|
|
158
|
+
args: step.args ?? [],
|
|
159
|
+
cwd: normalizeStepCwd(step.cwd),
|
|
160
|
+
status: 'failed',
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
startedAt: new Date(nowMs).toISOString(),
|
|
163
|
+
durationMs: 0,
|
|
164
|
+
detail,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function skippedStepResult(step, nowMs, detail) {
|
|
168
|
+
return {
|
|
169
|
+
id: step.id,
|
|
170
|
+
label: step.label,
|
|
171
|
+
command: normalizeStepCommand(step.command),
|
|
172
|
+
args: step.args ?? [],
|
|
173
|
+
cwd: normalizeStepCwd(step.cwd),
|
|
174
|
+
status: 'skipped',
|
|
175
|
+
exitCode: null,
|
|
176
|
+
startedAt: new Date(nowMs).toISOString(),
|
|
177
|
+
durationMs: 0,
|
|
178
|
+
detail,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function normalizeStepCommand(command) {
|
|
182
|
+
return command === undefined || command.length === 0 ? null : command;
|
|
183
|
+
}
|
|
184
|
+
function normalizeStepCwd(cwd) {
|
|
185
|
+
return cwd === undefined || cwd.length === 0 ? null : path.resolve(cwd);
|
|
186
|
+
}
|
|
187
|
+
function readOptionalJsonEvidence(file, gameRoot) {
|
|
188
|
+
if (file === undefined) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const resolved = path.resolve(gameRoot, file);
|
|
192
|
+
const displayFile = relativeOrAbsolute(gameRoot, resolved);
|
|
193
|
+
if (!existsSync(resolved)) {
|
|
194
|
+
return { file: displayFile, found: false, parseError: null, value: null };
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return {
|
|
198
|
+
file: displayFile,
|
|
199
|
+
found: true,
|
|
200
|
+
parseError: null,
|
|
201
|
+
value: JSON.parse(readFileSync(resolved, 'utf8')),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
file: displayFile,
|
|
207
|
+
found: true,
|
|
208
|
+
parseError: formatError(error),
|
|
209
|
+
value: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function relativeOrAbsolute(root, file) {
|
|
214
|
+
const relative = path.relative(root, file);
|
|
215
|
+
return relative.startsWith('..') || path.isAbsolute(relative) ? file : relative || '.';
|
|
216
|
+
}
|
|
217
|
+
function resolveRunnableStep(step) {
|
|
218
|
+
if (step.command === undefined || step.command.length === 0) {
|
|
219
|
+
return { ok: false, detail: `Acceptance step ${step.id} is missing command.` };
|
|
220
|
+
}
|
|
221
|
+
if (step.cwd === undefined || step.cwd.length === 0) {
|
|
222
|
+
return { ok: false, detail: `Acceptance step ${step.id} is missing cwd.` };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, command: step.command, cwd: step.cwd };
|
|
225
|
+
}
|
|
226
|
+
function escapeMarkdownTable(value) {
|
|
227
|
+
return escapeMarkdownInline(value).replaceAll('|', '\\|');
|
|
228
|
+
}
|
|
229
|
+
function escapeMarkdownInline(value) {
|
|
230
|
+
return value
|
|
231
|
+
.replaceAll('&', '&')
|
|
232
|
+
.replaceAll('<', '<')
|
|
233
|
+
.replaceAll('>', '>')
|
|
234
|
+
.replace(/([\\`*_[\]])/gu, '\\$1')
|
|
235
|
+
.replaceAll('\n', ' ');
|
|
236
|
+
}
|
|
237
|
+
function formatDuration(durationMs) {
|
|
238
|
+
return durationMs < 1000 ? `${durationMs}ms` : `${(durationMs / 1000).toFixed(1)}s`;
|
|
239
|
+
}
|
|
240
|
+
function formatError(error) {
|
|
241
|
+
return error instanceof Error ? error.message : String(error);
|
|
242
|
+
}
|
|
243
|
+
function isTimeoutError(error) {
|
|
244
|
+
return 'code' in error && error.code === 'ETIMEDOUT';
|
|
245
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, type GameAcceptanceCommandResult, type GameAcceptanceCommandRunner, type GameAcceptanceReport, type GameAcceptanceStatus, type GameAcceptanceStep, type GameAcceptanceStepResult, type GameAcceptanceStepStatus, type RunGameAcceptanceInput, type RunGameAcceptanceResult, } from './game-acceptance.js';
|
|
2
|
+
export { assertProductionTargetReadiness, type ProductionTargetReadinessInput, } from './production-target-readiness.js';
|
|
1
3
|
export declare function runMpgdCli(args: readonly string[]): Promise<void>;
|
|
2
4
|
export declare function runCreateGameCli(args: readonly string[]): Promise<void>;
|
|
3
5
|
export declare function readCliArgs(): readonly string[];
|