@bitmagic/cli 0.1.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 (79) hide show
  1. package/dist/auth/device-flow.d.ts +18 -0
  2. package/dist/auth/device-flow.js +130 -0
  3. package/dist/auth/device-flow.js.map +1 -0
  4. package/dist/auth/session.d.ts +3 -0
  5. package/dist/auth/session.js +19 -0
  6. package/dist/auth/session.js.map +1 -0
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +18 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/cli.d.ts +70 -0
  11. package/dist/cli.js +96 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/check.d.ts +1 -0
  14. package/dist/commands/check.js +22 -0
  15. package/dist/commands/check.js.map +1 -0
  16. package/dist/commands/dev.d.ts +6 -0
  17. package/dist/commands/dev.js +89 -0
  18. package/dist/commands/dev.js.map +1 -0
  19. package/dist/commands/generate.d.ts +62 -0
  20. package/dist/commands/generate.js +348 -0
  21. package/dist/commands/generate.js.map +1 -0
  22. package/dist/commands/init.d.ts +23 -0
  23. package/dist/commands/init.js +123 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/login.d.ts +6 -0
  26. package/dist/commands/login.js +36 -0
  27. package/dist/commands/login.js.map +1 -0
  28. package/dist/commands/logout.d.ts +6 -0
  29. package/dist/commands/logout.js +18 -0
  30. package/dist/commands/logout.js.map +1 -0
  31. package/dist/commands/verify.d.ts +6 -0
  32. package/dist/commands/verify.js +124 -0
  33. package/dist/commands/verify.js.map +1 -0
  34. package/dist/commands/whoami.d.ts +11 -0
  35. package/dist/commands/whoami.js +29 -0
  36. package/dist/commands/whoami.js.map +1 -0
  37. package/dist/config/credentials.d.ts +13 -0
  38. package/dist/config/credentials.js +114 -0
  39. package/dist/config/credentials.js.map +1 -0
  40. package/dist/config/environments.d.ts +21 -0
  41. package/dist/config/environments.js +67 -0
  42. package/dist/config/environments.js.map +1 -0
  43. package/dist/errors.d.ts +8 -0
  44. package/dist/errors.js +13 -0
  45. package/dist/errors.js.map +1 -0
  46. package/dist/generate/apply-patches.d.ts +7 -0
  47. package/dist/generate/apply-patches.js +97 -0
  48. package/dist/generate/apply-patches.js.map +1 -0
  49. package/dist/generate/stream.d.ts +33 -0
  50. package/dist/generate/stream.js +185 -0
  51. package/dist/generate/stream.js.map +1 -0
  52. package/dist/http/client.d.ts +6 -0
  53. package/dist/http/client.js +49 -0
  54. package/dist/http/client.js.map +1 -0
  55. package/dist/project/context.d.ts +18 -0
  56. package/dist/project/context.js +67 -0
  57. package/dist/project/context.js.map +1 -0
  58. package/dist/scaffold/aliases.d.ts +27 -0
  59. package/dist/scaffold/aliases.js +51 -0
  60. package/dist/scaffold/aliases.js.map +1 -0
  61. package/dist/scaffold/engine-download.d.ts +27 -0
  62. package/dist/scaffold/engine-download.js +78 -0
  63. package/dist/scaffold/engine-download.js.map +1 -0
  64. package/dist/scaffold/project-files.d.ts +15 -0
  65. package/dist/scaffold/project-files.js +225 -0
  66. package/dist/scaffold/project-files.js.map +1 -0
  67. package/dist/scaffold/project.d.ts +28 -0
  68. package/dist/scaffold/project.js +118 -0
  69. package/dist/scaffold/project.js.map +1 -0
  70. package/dist/verify/browser.d.ts +13 -0
  71. package/dist/verify/browser.js +75 -0
  72. package/dist/verify/browser.js.map +1 -0
  73. package/dist/verify/classify.d.ts +29 -0
  74. package/dist/verify/classify.js +59 -0
  75. package/dist/verify/classify.js.map +1 -0
  76. package/dist/verify/wait-for-server.d.ts +7 -0
  77. package/dist/verify/wait-for-server.js +25 -0
  78. package/dist/verify/wait-for-server.js.map +1 -0
  79. package/package.json +70 -0
@@ -0,0 +1,225 @@
1
+ import { tsconfigPaths, importMapEntries, viteAliasEntries } from './aliases.js';
2
+ /** Mirrors game/package.json so the pro lane resolves exactly what CI builds against. */
3
+ const DEPENDENCIES = {
4
+ '@dimforge/rapier2d-compat': '^0.19.3',
5
+ '@dimforge/rapier3d-compat': '^0.19.3',
6
+ '@msgpack/msgpack': '^3.1.3',
7
+ fflate: '^0.8.2',
8
+ i18next: '^24.2.3',
9
+ 'i18next-browser-languagedetector': '^8.0.4',
10
+ jszip: '^3.10.1',
11
+ three: '^0.183.0',
12
+ };
13
+ const DEV_DEPENDENCIES = {
14
+ '@types/node': '^24.10.0',
15
+ '@types/three': '^0.183.0',
16
+ // engine/types/ammo.d.ts opens with a triple-slash reference to this package's ambient types.
17
+ 'ammojs-typed': '^1.0.6',
18
+ typescript: '^5.9.2',
19
+ vite: '^7.3.1',
20
+ };
21
+ /** Resolved in the browser from a CDN, not from node_modules — see the design's import-map note. */
22
+ const CDN_IMPORTS = {
23
+ three: 'https://esm.sh/three@0.182.0',
24
+ 'three/': 'https://esm.sh/three@0.182.0/',
25
+ '@dimforge/rapier3d-compat': 'https://esm.sh/@dimforge/rapier3d-compat@0.19.3',
26
+ '@dimforge/rapier2d-compat': 'https://esm.sh/@dimforge/rapier2d-compat@0.19.3',
27
+ i18next: 'https://esm.sh/i18next@24.2.3',
28
+ 'i18next-browser-languagedetector': 'https://esm.sh/i18next-browser-languagedetector@8.0.4',
29
+ '@msgpack/msgpack': 'https://esm.sh/@msgpack/msgpack@3.1.3',
30
+ fflate: 'https://esm.sh/fflate@0.8.2',
31
+ };
32
+ /** Every JSON file the scaffold writes is 2-space indented and newline-terminated. */
33
+ function renderJson(value) {
34
+ return `${JSON.stringify(value, null, 2)}\n`;
35
+ }
36
+ export function renderPackageJson(name) {
37
+ return renderJson({
38
+ name,
39
+ version: '0.1.0',
40
+ private: true,
41
+ type: 'module',
42
+ scripts: {
43
+ build: 'tsc',
44
+ check: 'tsc --noEmit',
45
+ dev: 'vite',
46
+ },
47
+ dependencies: DEPENDENCIES,
48
+ devDependencies: DEV_DEPENDENCIES,
49
+ });
50
+ }
51
+ export function renderTsconfig() {
52
+ return renderJson({
53
+ compilerOptions: {
54
+ target: 'es2020',
55
+ module: 'es2020',
56
+ lib: ['es2020', 'dom'],
57
+ rootDir: '.',
58
+ outDir: 'dist',
59
+ baseUrl: '.',
60
+ paths: tsconfigPaths(),
61
+ moduleResolution: 'bundler',
62
+ strict: true,
63
+ noUncheckedIndexedAccess: true,
64
+ exactOptionalPropertyTypes: false,
65
+ verbatimModuleSyntax: true,
66
+ isolatedModules: true,
67
+ moduleDetection: 'force',
68
+ skipLibCheck: true,
69
+ esModuleInterop: true,
70
+ allowSyntheticDefaultImports: true,
71
+ forceConsistentCasingInFileNames: true,
72
+ noImplicitAny: true,
73
+ resolveJsonModule: true,
74
+ sourceMap: true,
75
+ types: [],
76
+ typeRoots: ['engine/types', 'node_modules/@types', 'node_modules/ammojs-typed/ammo/ambient'],
77
+ },
78
+ include: ['src/**/*', 'engine/**/*'],
79
+ // The vendored engine ships its own __tests__/ directories; a scaffolded project never
80
+ // installs jest, so compiling them fails on missing test-runner globals (describe/it).
81
+ // engine/bundle/**/* mirrors game/tsconfig.json's exclusion of src/bundle/**/*: the
82
+ // monorepo's own CI does not typecheck that tree, so the scaffold must not hold it to a
83
+ // stricter standard than the repo it was vendored from.
84
+ exclude: [
85
+ 'node_modules',
86
+ 'dist',
87
+ 'engine/**/*.test.ts',
88
+ 'engine/**/__tests__/**/*',
89
+ 'src/**/*.test.ts',
90
+ 'src/**/__tests__/**/*',
91
+ 'engine/bundle/**/*',
92
+ ],
93
+ });
94
+ }
95
+ export function renderIndexHtml() {
96
+ const aliasImports = importMapEntries();
97
+ const imports = { ...CDN_IMPORTS, ...aliasImports };
98
+ // Derived from the same alias model as the import map above, not a hardcoded literal, so the
99
+ // two cannot drift: the entry point is the engine's import-map target plus its filename.
100
+ const engineEntryPoint = `${aliasImports['engine/']}GameTemplate.js`;
101
+ // The #game-container div below is required, not decorative: GameTemplate.ts (:196, :457)
102
+ // does document.getElementById('game-container') as HTMLElement, and GameEngine.ts (:557)
103
+ // appendChild()s into it. The "as HTMLElement" cast erases the null a missing div would
104
+ // produce, so its absence typechecks clean and only fails at runtime. The CSS rule mirrors
105
+ // game/index.html's #game-container rule verbatim.
106
+ return `<!DOCTYPE html>
107
+ <html lang="en">
108
+ <head>
109
+ <meta charset="UTF-8">
110
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
111
+ <title>Bitmagic Game</title>
112
+ <style>
113
+ * { margin: 0; padding: 0; box-sizing: border-box; }
114
+ html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
115
+ html, body { touch-action: none; }
116
+ canvas { display: block; }
117
+ #game-container {
118
+ position: fixed;
119
+ top: 0;
120
+ left: 0;
121
+ width: 100%;
122
+ height: 100%;
123
+ background: #000;
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div id="game-container"></div>
129
+
130
+ <!-- Static import map: must come before any module script. Third-party packages resolve
131
+ from a CDN at runtime; node_modules exists for tsc, not for the browser. -->
132
+ <script type="importmap">
133
+ ${JSON.stringify({ imports }, null, 8).replace(/^/gm, ' ')}
134
+ </script>
135
+
136
+ <script type="module" src="${engineEntryPoint}"></script>
137
+ </body>
138
+ </html>
139
+ `;
140
+ }
141
+ export function renderViteConfig() {
142
+ const entries = Object.entries(viteAliasEntries())
143
+ .map(([key, target]) => ` '${key}': '${target}'`)
144
+ .join(',\n');
145
+ return `import { defineConfig } from 'vite';
146
+
147
+ // Aliases mirror tsconfig paths but point at the COMPILED output: tsc emits to dist/ and the
148
+ // browser loads the emitted JavaScript. Keep these in step with tsconfig.json.
149
+ export default defineConfig({
150
+ root: '.',
151
+ base: '/',
152
+ cacheDir: '.vite-cache',
153
+ assetsInclude: ['**/*.wasm'],
154
+ resolve: {
155
+ alias: {
156
+ ${entries}
157
+ }
158
+ },
159
+ server: {
160
+ port: 3001
161
+ }
162
+ });
163
+ `;
164
+ }
165
+ export function renderGitignore() {
166
+ return ['dist/', 'node_modules/', '.bitmagic/', '.vite-cache/', ''].join('\n');
167
+ }
168
+ export function renderBitmagicJson(metadata) {
169
+ return renderJson(metadata);
170
+ }
171
+ export function renderAgentsMd() {
172
+ return `# Working on this Bitmagic game
173
+
174
+ ## Layout
175
+
176
+ - \`src/work/\` — **your game.** Everything you write lives here. \`Game.ts\` is the entry
177
+ point; \`game.json\` and \`world.json\` hold configuration and scene data. The \`work\`
178
+ directory name is load-bearing: the engine loads game data by fetching the literal path
179
+ \`src/work/world.json\`, not through an import or an alias, so renaming or moving this
180
+ directory breaks the game at runtime even though it still typechecks.
181
+ - \`engine/\` — **the Bitmagic engine, read-only.** It is vendored by the CLI and replaced
182
+ wholesale by \`bitmagic upgrade\`. Edits here are lost on upgrade and ignored on publish.
183
+ - \`engine/docs/\` — the engine's deep-dive documentation. Read it before guessing.
184
+
185
+ \`engine/types/work-game.d.ts\` declares \`module 'work/Game.js'\`, which is how the engine
186
+ imports your game. Renaming or removing the \`VoxelGame\` export from \`src/work/Game.ts\` still
187
+ typechecks against that declaration — it only fails at runtime, when the engine actually loads
188
+ the module and finds the export missing.
189
+
190
+ ## Imports
191
+
192
+ Engine code is reached through path aliases, not relative paths:
193
+
194
+ \`\`\`ts
195
+ import { PlayerController } from 'engine/PlayerController.js';
196
+ import type { GameData } from 'types/game.js';
197
+ \`\`\`
198
+
199
+ Always include the \`.js\` extension, even in TypeScript.
200
+
201
+ ## Before you change movement, cameras or rotation
202
+
203
+ Read \`engine/docs/coordinate-system.md\`. Gameplay faces **+Z** while cameras face **−Z**, and
204
+ the two yaw conventions differ. Guessing produces code that compiles and moves the wrong way.
205
+
206
+ ## Checking your work
207
+
208
+ \`\`\`
209
+ bitmagic check # typecheck against the vendored engine
210
+ \`\`\`
211
+
212
+ The engine is large; \`check\` is the fast way to find out whether an edit holds together.
213
+ `;
214
+ }
215
+ export function renderClaudeMd() {
216
+ return '@AGENTS.md\n';
217
+ }
218
+ export function renderSkillsReadme() {
219
+ return `# Claude Code skills
220
+
221
+ Claude Code skills placed in this directory apply to this project. See \`AGENTS.md\` at the
222
+ project root for the guidance an agent should read before editing this game.
223
+ `;
224
+ }
225
+ //# sourceMappingURL=project-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-files.js","sourceRoot":"","sources":["../../src/scaffold/project-files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { type ProjectMetadata } from './project-files.js';
2
+ export interface TemplateEntry {
3
+ id: string;
4
+ genre: string;
5
+ recommended?: boolean;
6
+ }
7
+ /**
8
+ * Directories moved out of the extracted tree into the project's vendored engine/.
9
+ *
10
+ * genres/ is vendored here for the engine's own debug tooling (see aliases.ts), which is a
11
+ * separate concern from src/ below, which is *derived* from genres/ for the creator's game.
12
+ * Both exist side by side, same as the real game project's src/genres/ and src/work/.
13
+ *
14
+ * Exported so tests can build their fixture from this list instead of hand-maintaining a
15
+ * parallel copy that silently goes stale when an entry is added here.
16
+ */
17
+ export declare const VENDORED_DIRS: string[];
18
+ export declare function resolveTemplate(templatesDir: string, requestedId?: string): TemplateEntry;
19
+ export interface ScaffoldOptions {
20
+ targetDir: string;
21
+ /** Where extractEngine unpacked game/src/* and game/docs. */
22
+ extractedEngineDir: string;
23
+ templatesDir: string;
24
+ template: TemplateEntry;
25
+ metadata: ProjectMetadata;
26
+ projectName: string;
27
+ }
28
+ export declare function scaffoldProject(options: ScaffoldOptions): void;
@@ -0,0 +1,118 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { CliError } from '../errors.js';
4
+ import { aliasSourceDir } from './aliases.js';
5
+ import { renderPackageJson, renderTsconfig, renderIndexHtml, renderViteConfig, renderGitignore, renderBitmagicJson, renderAgentsMd, renderClaudeMd, renderSkillsReadme, } from './project-files.js';
6
+ /**
7
+ * Directories moved out of the extracted tree into the project's vendored engine/.
8
+ *
9
+ * genres/ is vendored here for the engine's own debug tooling (see aliases.ts), which is a
10
+ * separate concern from src/ below, which is *derived* from genres/ for the creator's game.
11
+ * Both exist side by side, same as the real game project's src/genres/ and src/work/.
12
+ *
13
+ * Exported so tests can build their fixture from this list instead of hand-maintaining a
14
+ * parallel copy that silently goes stale when an entry is added here.
15
+ */
16
+ export const VENDORED_DIRS = [
17
+ 'engine',
18
+ 'types',
19
+ 'bundle',
20
+ 'debug',
21
+ 'editor',
22
+ 'genres',
23
+ 'docs',
24
+ 'utils',
25
+ 'core',
26
+ ];
27
+ export function resolveTemplate(templatesDir, requestedId) {
28
+ const indexPath = path.join(templatesDir, 'index.json');
29
+ let raw;
30
+ try {
31
+ raw = fs.readFileSync(indexPath, 'utf-8');
32
+ }
33
+ catch {
34
+ throw new CliError(`The engine bundle has no template index at ${indexPath}.`);
35
+ }
36
+ const parsed = JSON.parse(raw);
37
+ const templates = parsed?.templates ?? [];
38
+ // One guard covers both "the index is empty" and "no template is marked recommended": `first`
39
+ // doubles as the fallback for the no-id case below.
40
+ const [first] = templates;
41
+ if (!first) {
42
+ throw new CliError('The engine bundle lists no templates.');
43
+ }
44
+ if (requestedId === undefined) {
45
+ return templates.find((t) => t.recommended) ?? first;
46
+ }
47
+ const match = templates.find((t) => t.id === requestedId);
48
+ if (!match) {
49
+ throw new CliError(`Unknown template "${requestedId}". Available templates: ${templates.map((t) => t.id).join(', ')}.`);
50
+ }
51
+ return match;
52
+ }
53
+ /** Copies a directory's CONTENTS into dest, creating dest if needed. */
54
+ function copyContents(source, dest) {
55
+ fs.mkdirSync(dest, { recursive: true });
56
+ for (const entry of fs.readdirSync(source)) {
57
+ fs.cpSync(path.join(source, entry), path.join(dest, entry), { recursive: true });
58
+ }
59
+ }
60
+ export function scaffoldProject(options) {
61
+ const { targetDir, extractedEngineDir, templatesDir, template, metadata, projectName } = options;
62
+ const engineDir = path.join(targetDir, 'engine');
63
+ fs.mkdirSync(engineDir, { recursive: true });
64
+ for (const dir of VENDORED_DIRS) {
65
+ const source = path.join(extractedEngineDir, dir);
66
+ if (!fs.existsSync(source)) {
67
+ throw new CliError(`The engine bundle has no "${dir}" directory at ${source}. The tarball is incomplete or corrupt.`);
68
+ }
69
+ fs.cpSync(source, path.join(engineDir, dir), { recursive: true });
70
+ }
71
+ // The game: the genre baseline, then the template overlaid on top. Contents are copied, not
72
+ // the directory itself — a nested src/work/<genre>/ would break every relative import in
73
+ // Game.ts. This is a second copy of the same baseline vendored above as engine/genres/: that
74
+ // copy feeds the engine's debug tooling, this one seeds the creator's game.
75
+ //
76
+ // Derived from the 'work' alias rather than restated as a literal, so this can't drift from
77
+ // aliases.ts's sourceDir — which must stay 'src/work' because worldDataLoader.ts fetches that
78
+ // literal path (see the comment there). copyContents creates parent directories, so the
79
+ // intermediate src/ appears for free.
80
+ const srcDir = path.join(targetDir, aliasSourceDir('work'));
81
+ const genreDir = path.join(extractedEngineDir, 'genres', template.genre);
82
+ if (!fs.existsSync(genreDir)) {
83
+ throw new CliError(`The engine bundle has no baseline for genre "${template.genre}".`);
84
+ }
85
+ copyContents(genreDir, srcDir);
86
+ const overlayDir = path.join(templatesDir, template.id, 'source');
87
+ if (fs.existsSync(overlayDir)) {
88
+ copyContents(overlayDir, srcDir);
89
+ }
90
+ // AutoCacheBuster.ts registers this service worker at the absolute path
91
+ // /sw-cache-buster.js, so it must be served from the project root, not vendored under
92
+ // engine/ — a service worker cannot control scopes above its own path. Unlike VENDORED_DIRS
93
+ // above, a missing file here is not treated as a corrupt tarball: every tarball published
94
+ // before this feature shipped lacks it, and `init` must keep scaffolding against those.
95
+ const serviceWorker = path.join(extractedEngineDir, 'sw-cache-buster.js');
96
+ if (fs.existsSync(serviceWorker)) {
97
+ fs.cpSync(serviceWorker, path.join(targetDir, 'sw-cache-buster.js'));
98
+ }
99
+ const files = [
100
+ ['package.json', renderPackageJson(projectName)],
101
+ ['tsconfig.json', renderTsconfig()],
102
+ ['vite.config.js', renderViteConfig()],
103
+ ['index.html', renderIndexHtml()],
104
+ ['bitmagic.json', renderBitmagicJson(metadata)],
105
+ ['AGENTS.md', renderAgentsMd()],
106
+ ['CLAUDE.md', renderClaudeMd()],
107
+ ['.gitignore', renderGitignore()],
108
+ ['.claude/skills/README.md', renderSkillsReadme()],
109
+ ];
110
+ for (const [name, contents] of files) {
111
+ const filePath = path.join(targetDir, name);
112
+ // .claude/skills/README.md nests two directories deep; every other entry is a top-level
113
+ // file, for which mkdir on the already-existing targetDir is a harmless no-op.
114
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
115
+ fs.writeFileSync(filePath, contents);
116
+ }
117
+ }
118
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/scaffold/project.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { VerifyObservations } from './classify.js';
2
+ export interface HeadlessCheckOptions {
3
+ url: string;
4
+ screenshotPath: string;
5
+ consolePath: string;
6
+ /** How long to let the game settle after load before observing. */
7
+ settleMs: number;
8
+ }
9
+ /**
10
+ * Drives the creator's installed Chrome. `channel: 'chrome'` deliberately uses the system
11
+ * browser rather than a downloaded one, so installing the CLI stays a small, fast npm install.
12
+ */
13
+ export declare function runHeadlessCheck(options: HeadlessCheckOptions): Promise<VerifyObservations>;
@@ -0,0 +1,75 @@
1
+ import * as fs from 'fs';
2
+ import { chromium } from 'playwright-core';
3
+ import { CliError } from '../errors.js';
4
+ /**
5
+ * Drives the creator's installed Chrome. `channel: 'chrome'` deliberately uses the system
6
+ * browser rather than a downloaded one, so installing the CLI stays a small, fast npm install.
7
+ */
8
+ export async function runHeadlessCheck(options) {
9
+ let browser;
10
+ try {
11
+ browser = await chromium.launch({ headless: true, channel: 'chrome' });
12
+ }
13
+ catch {
14
+ // Not a game failure. Saying so plainly matters: an agent told "verification failed" would
15
+ // start editing game code to fix a missing browser.
16
+ throw new CliError('Could not launch Google Chrome. `bitmagic verify` drives the Chrome installed on this ' +
17
+ 'machine — install it from https://www.google.com/chrome/ and run verify again.');
18
+ }
19
+ // Declared outside the try so the `finally` below can write whatever was captured even if the
20
+ // browser phase throws (e.g. page.goto timing out) — artifacts are the point of a failing run.
21
+ const consoleLines = [];
22
+ const pageErrors = [];
23
+ try {
24
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
25
+ const page = await context.newPage();
26
+ page.on('console', (message) => consoleLines.push(`${message.type()}: ${message.text()}`));
27
+ page.on('pageerror', (error) => pageErrors.push(error.message));
28
+ page.on('requestfailed', (request) => consoleLines.push(`requestfailed: ${request.url()} ${request.failure()?.errorText ?? ''}`));
29
+ try {
30
+ await page.goto(options.url, { waitUntil: 'load', timeout: 45_000 });
31
+ }
32
+ catch (error) {
33
+ // Best-effort screenshot of whatever the page shows right now — often still blank, but
34
+ // worth having. The console log (written in `finally` below) is the useful artifact here.
35
+ try {
36
+ fs.writeFileSync(options.screenshotPath, await page.screenshot());
37
+ }
38
+ catch {
39
+ // Page may already be unusable (closed context, crashed renderer) — nothing to save.
40
+ }
41
+ const detail = error instanceof Error ? error.message : String(error);
42
+ throw new CliError(`The page never finished loading at ${options.url}: ${detail}`);
43
+ }
44
+ await page.waitForTimeout(options.settleMs);
45
+ const canvas = await page.evaluate(() => {
46
+ // This callback runs inside the browser Playwright drives, not in the CLI's Node process,
47
+ // so `document` is a real global there even though the CLI's eslint config declares no
48
+ // DOM globals for the process it actually lints.
49
+ // eslint-disable-next-line no-undef
50
+ const element = document.querySelector('#game-container canvas');
51
+ if (!(element instanceof HTMLCanvasElement))
52
+ return null;
53
+ const rect = element.getBoundingClientRect();
54
+ return {
55
+ width: Math.round(rect.width),
56
+ height: Math.round(rect.height),
57
+ // The drawing-buffer attributes, not CSS size — see CanvasObservation in classify.ts for
58
+ // why this replaced a getContext() probe.
59
+ bufferWidth: element.width,
60
+ bufferHeight: element.height,
61
+ };
62
+ });
63
+ const screenshotBuffer = await page.screenshot();
64
+ fs.writeFileSync(options.screenshotPath, screenshotBuffer);
65
+ return { consoleLines, pageErrors, canvas, screenshotBytes: screenshotBuffer.length };
66
+ }
67
+ finally {
68
+ // Written on success and failure alike, including when the try block above threw before
69
+ // ever reaching the screenshot: the console log is the most useful thing a creator or their
70
+ // agent can look at when the page never came up at all.
71
+ fs.writeFileSync(options.consolePath, [...consoleLines, ...pageErrors.map((e) => `pageerror: ${e}`)].join('\n'));
72
+ await browser.close();
73
+ }
74
+ }
75
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/verify/browser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ export interface CanvasObservation {
2
+ /** CSS pixel size from getBoundingClientRect() — whether the canvas is visible/laid out. */
3
+ width: number;
4
+ height: number;
5
+ /**
6
+ * canvas.width / canvas.height — the drawing-buffer *attributes*, not CSS size. A `<canvas>`
7
+ * nobody has touched keeps the HTML defaults (300x150); a renderer that actually claims the
8
+ * canvas resizes these to the real framebuffer dimensions. This is deliberately not a
9
+ * getContext() probe: getContext() itself creates a context — even 'webgpu' on a page with no
10
+ * navigator.gpu — whether or not anything ever renders into it, which is why that used to be
11
+ * checked and always passed.
12
+ */
13
+ bufferWidth: number;
14
+ bufferHeight: number;
15
+ }
16
+ export interface VerifyObservations {
17
+ /** Console lines, each prefixed with its type, e.g. `error: …`. */
18
+ consoleLines: string[];
19
+ pageErrors: string[];
20
+ canvas: CanvasObservation | null;
21
+ /** Byte length of the PNG screenshot taken after settling. */
22
+ screenshotBytes: number;
23
+ }
24
+ export interface VerifyResult {
25
+ ok: boolean;
26
+ failures: string[];
27
+ warnings: string[];
28
+ }
29
+ export declare function classifyVerifyRun(observations: VerifyObservations): VerifyResult;
@@ -0,0 +1,59 @@
1
+ /** Engine messages that mean the game did not come up. */
2
+ const FATAL_CONSOLE = [
3
+ 'world.json not found',
4
+ 'Invalid content type',
5
+ 'Failed to load game in standalone mode',
6
+ 'Failed to load game in template',
7
+ ];
8
+ /** Present in a healthy boot. Absence is suspicious but not proof of failure — these strings are
9
+ * engine-owned and may change between versions, so they warn rather than fail. */
10
+ const EXPECTED_MARKERS = ['Loaded world.json', 'Merged game data ready'];
11
+ /** An untouched <canvas> keeps these HTML defaults; a renderer that ever called setSize()
12
+ * resizes the drawing buffer away from them. */
13
+ const UNTOUCHED_CANVAS_WIDTH = 300;
14
+ const UNTOUCHED_CANVAS_HEIGHT = 150;
15
+ /**
16
+ * A uniformly black (or otherwise empty) 1280x720 PNG compresses to a few KB — measured at
17
+ * ~4.3KB for both an untouched 300x150 canvas and a canvas resized to 1280x720 but never drawn
18
+ * into. A real rendered frame from this engine is on the order of hundreds of KB (~245KB on a
19
+ * healthy `verify` run). 20KB sits with generous headroom above the blank case and well below a
20
+ * real frame — a smoke signal, not a pixel assertion.
21
+ */
22
+ const MIN_SCREENSHOT_BYTES = 20_000;
23
+ export function classifyVerifyRun(observations) {
24
+ const failures = [];
25
+ const warnings = [];
26
+ for (const error of observations.pageErrors) {
27
+ failures.push(`Uncaught error on the page: ${error}`);
28
+ }
29
+ const { canvas } = observations;
30
+ if (!canvas) {
31
+ failures.push('No <canvas> was rendered inside #game-container — the game did not start.');
32
+ }
33
+ else {
34
+ if (canvas.width === 0 || canvas.height === 0) {
35
+ failures.push(`The canvas has no size (${canvas.width}x${canvas.height}).`);
36
+ }
37
+ if (canvas.bufferWidth === UNTOUCHED_CANVAS_WIDTH && canvas.bufferHeight === UNTOUCHED_CANVAS_HEIGHT) {
38
+ failures.push(`The canvas was never resized by a renderer (still the ${UNTOUCHED_CANVAS_WIDTH}x` +
39
+ `${UNTOUCHED_CANVAS_HEIGHT} HTML default) — nothing claimed it.`);
40
+ }
41
+ }
42
+ if (observations.screenshotBytes < MIN_SCREENSHOT_BYTES) {
43
+ failures.push(`The screenshot is implausibly small (${observations.screenshotBytes} bytes) — the frame ` +
44
+ 'looks blank.');
45
+ }
46
+ for (const line of observations.consoleLines) {
47
+ const fatal = FATAL_CONSOLE.find((f) => line.includes(f));
48
+ if (fatal)
49
+ failures.push(`The engine reported: ${line}`);
50
+ }
51
+ const joined = observations.consoleLines.join('\n');
52
+ for (const marker of EXPECTED_MARKERS) {
53
+ if (!joined.includes(marker)) {
54
+ warnings.push(`Expected "${marker}" in the console and did not see it.`);
55
+ }
56
+ }
57
+ return { ok: failures.length === 0, failures, warnings };
58
+ }
59
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/verify/classify.ts"],"names":[],"mappings":"AA+BA,0DAA0D;AAC1D,MAAM,aAAa,GAAG;IACpB,sBAAsB;IACtB,sBAAsB;IACtB,wCAAwC;IACxC,iCAAiC;CAClC,CAAC;AAEF;mFACmF;AACnF,MAAM,gBAAgB,GAAG,CAAC,mBAAmB,EAAE,wBAAwB,CAAC,CAAC;AAEzE;iDACiD;AACjD,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAEpC;;;;;;GAMG;AACH,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC,MAAM,UAAU,iBAAiB,CAAC,YAAgC;IAChE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,UAAU,EAAE,CAAC;QAC5C,QAAQ,CAAC,IAAI,CAAC,+BAA+B,KAAK,EAAE,CAAC,CAAC;IACxD,CAAC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,YAAY,CAAC;IAChC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,QAAQ,CAAC,IAAI,CAAC,2EAA2E,CAAC,CAAC;IAC7F,CAAC;SAAM,CAAC;QACN,IAAI,MAAM,CAAC,KAAK,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9C,QAAQ,CAAC,IAAI,CAAC,2BAA2B,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;QAC9E,CAAC;QACD,IAAI,MAAM,CAAC,WAAW,KAAK,sBAAsB,IAAI,MAAM,CAAC,YAAY,KAAK,uBAAuB,EAAE,CAAC;YACrG,QAAQ,CAAC,IAAI,CACX,yDAAyD,sBAAsB,GAAG;gBAChF,GAAG,uBAAuB,sCAAsC,CACnE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,YAAY,CAAC,eAAe,GAAG,oBAAoB,EAAE,CAAC;QACxD,QAAQ,CAAC,IAAI,CACX,wCAAwC,YAAY,CAAC,eAAe,sBAAsB;YACxF,cAAc,CACjB,CAAC;IACJ,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,YAAY,CAAC,YAAY,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1D,IAAI,KAAK;YAAE,QAAQ,CAAC,IAAI,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,MAAM,GAAG,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpD,KAAK,MAAM,MAAM,IAAI,gBAAgB,EAAE,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,QAAQ,CAAC,IAAI,CAAC,aAAa,MAAM,sCAAsC,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Polls `http://localhost:<port>/` until it answers or `timeoutMs` elapses. Shared by `verify`
3
+ * (waiting on its throwaway vite instance) and `dev` (announcing its own once it's ready) — kept
4
+ * dependency-free so importing it doesn't pull playwright-core into `dev`, which never touches a
5
+ * browser.
6
+ */
7
+ export declare function waitForServer(port: number, timeoutMs: number): Promise<void>;
@@ -0,0 +1,25 @@
1
+ import { CliError } from '../errors.js';
2
+ /**
3
+ * Polls `http://localhost:<port>/` until it answers or `timeoutMs` elapses. Shared by `verify`
4
+ * (waiting on its throwaway vite instance) and `dev` (announcing its own once it's ready) — kept
5
+ * dependency-free so importing it doesn't pull playwright-core into `dev`, which never touches a
6
+ * browser.
7
+ */
8
+ export async function waitForServer(port, timeoutMs) {
9
+ const deadline = Date.now() + timeoutMs;
10
+ for (;;) {
11
+ try {
12
+ const response = await fetch(`http://localhost:${port}/`);
13
+ if (response.ok)
14
+ return;
15
+ }
16
+ catch {
17
+ // Not up yet; fall through to the retry delay.
18
+ }
19
+ if (Date.now() > deadline) {
20
+ throw new CliError(`The dev server did not start on port ${port} within ${timeoutMs}ms.`);
21
+ }
22
+ await new Promise((resolve) => setTimeout(resolve, 250));
23
+ }
24
+ }
25
+ //# sourceMappingURL=wait-for-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wait-for-server.js","sourceRoot":"","sources":["../../src/verify/wait-for-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAY,EAAE,SAAiB;IACjE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,oBAAoB,IAAI,GAAG,CAAC,CAAC;YAC1D,IAAI,QAAQ,CAAC,EAAE;gBAAE,OAAO;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,+CAA+C;QACjD,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,QAAQ,CAAC,wCAAwC,IAAI,WAAW,SAAS,KAAK,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC"}