@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.
- package/dist/auth/device-flow.d.ts +18 -0
- package/dist/auth/device-flow.js +130 -0
- package/dist/auth/device-flow.js.map +1 -0
- package/dist/auth/session.d.ts +3 -0
- package/dist/auth/session.js +19 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +18 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +70 -0
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +1 -0
- package/dist/commands/check.js +22 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/dev.d.ts +6 -0
- package/dist/commands/dev.js +89 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/generate.d.ts +62 -0
- package/dist/commands/generate.js +348 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +23 -0
- package/dist/commands/init.js +123 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.js +36 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +6 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.js +124 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/whoami.d.ts +11 -0
- package/dist/commands/whoami.js +29 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config/credentials.d.ts +13 -0
- package/dist/config/credentials.js +114 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/environments.d.ts +21 -0
- package/dist/config/environments.js +67 -0
- package/dist/config/environments.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/generate/apply-patches.d.ts +7 -0
- package/dist/generate/apply-patches.js +97 -0
- package/dist/generate/apply-patches.js.map +1 -0
- package/dist/generate/stream.d.ts +33 -0
- package/dist/generate/stream.js +185 -0
- package/dist/generate/stream.js.map +1 -0
- package/dist/http/client.d.ts +6 -0
- package/dist/http/client.js +49 -0
- package/dist/http/client.js.map +1 -0
- package/dist/project/context.d.ts +18 -0
- package/dist/project/context.js +67 -0
- package/dist/project/context.js.map +1 -0
- package/dist/scaffold/aliases.d.ts +27 -0
- package/dist/scaffold/aliases.js +51 -0
- package/dist/scaffold/aliases.js.map +1 -0
- package/dist/scaffold/engine-download.d.ts +27 -0
- package/dist/scaffold/engine-download.js +78 -0
- package/dist/scaffold/engine-download.js.map +1 -0
- package/dist/scaffold/project-files.d.ts +15 -0
- package/dist/scaffold/project-files.js +225 -0
- package/dist/scaffold/project-files.js.map +1 -0
- package/dist/scaffold/project.d.ts +28 -0
- package/dist/scaffold/project.js +118 -0
- package/dist/scaffold/project.js.map +1 -0
- package/dist/verify/browser.d.ts +13 -0
- package/dist/verify/browser.js +75 -0
- package/dist/verify/browser.js.map +1 -0
- package/dist/verify/classify.d.ts +29 -0
- package/dist/verify/classify.js +59 -0
- package/dist/verify/classify.js.map +1 -0
- package/dist/verify/wait-for-server.d.ts +7 -0
- package/dist/verify/wait-for-server.js +25 -0
- package/dist/verify/wait-for-server.js.map +1 -0
- 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"}
|