@yali-engine/cli 0.0.0-stage → 0.4.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 (75) hide show
  1. package/bin/yali.mjs +12 -0
  2. package/dist/budgets.d.ts +25 -0
  3. package/dist/budgets.js +27 -0
  4. package/dist/budgets.js.map +1 -0
  5. package/dist/cdp.d.ts +25 -0
  6. package/dist/cdp.js +106 -0
  7. package/dist/cdp.js.map +1 -0
  8. package/dist/cli.d.ts +7 -0
  9. package/dist/cli.js +83 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/cmd-bench.d.ts +17 -0
  12. package/dist/cmd-bench.js +74 -0
  13. package/dist/cmd-bench.js.map +1 -0
  14. package/dist/cmd-build.d.ts +13 -0
  15. package/dist/cmd-build.js +40 -0
  16. package/dist/cmd-build.js.map +1 -0
  17. package/dist/cmd-convert.d.ts +27 -0
  18. package/dist/cmd-convert.js +456 -0
  19. package/dist/cmd-convert.js.map +1 -0
  20. package/dist/cmd-create.d.ts +8 -0
  21. package/dist/cmd-create.js +248 -0
  22. package/dist/cmd-create.js.map +1 -0
  23. package/dist/cmd-dev.d.ts +9 -0
  24. package/dist/cmd-dev.js +24 -0
  25. package/dist/cmd-dev.js.map +1 -0
  26. package/dist/cmd-game.d.ts +24 -0
  27. package/dist/cmd-game.js +127 -0
  28. package/dist/cmd-game.js.map +1 -0
  29. package/dist/cmd-headless.d.ts +15 -0
  30. package/dist/cmd-headless.js +59 -0
  31. package/dist/cmd-headless.js.map +1 -0
  32. package/dist/cmd-inspect.d.ts +13 -0
  33. package/dist/cmd-inspect.js +64 -0
  34. package/dist/cmd-inspect.js.map +1 -0
  35. package/dist/cmd-test.d.ts +9 -0
  36. package/dist/cmd-test.js +68 -0
  37. package/dist/cmd-test.js.map +1 -0
  38. package/dist/collision.d.ts +31 -0
  39. package/dist/collision.js +471 -0
  40. package/dist/collision.js.map +1 -0
  41. package/dist/commands.d.ts +18 -0
  42. package/dist/commands.js +105 -0
  43. package/dist/commands.js.map +1 -0
  44. package/dist/encoder.d.ts +63 -0
  45. package/dist/encoder.js +119 -0
  46. package/dist/encoder.js.map +1 -0
  47. package/dist/index.d.ts +18 -0
  48. package/dist/index.js +19 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/main.d.ts +1 -0
  51. package/dist/main.js +9 -0
  52. package/dist/main.js.map +1 -0
  53. package/dist/native-encoder.d.ts +39 -0
  54. package/dist/native-encoder.js +142 -0
  55. package/dist/native-encoder.js.map +1 -0
  56. package/dist/output.d.ts +28 -0
  57. package/dist/output.js +27 -0
  58. package/dist/output.js.map +1 -0
  59. package/dist/png.d.ts +26 -0
  60. package/dist/png.js +134 -0
  61. package/dist/png.js.map +1 -0
  62. package/dist/project.d.ts +15 -0
  63. package/dist/project.js +35 -0
  64. package/dist/project.js.map +1 -0
  65. package/dist/size.d.ts +47 -0
  66. package/dist/size.js +100 -0
  67. package/dist/size.js.map +1 -0
  68. package/package.json +43 -3
  69. package/vendor/ENCODER-BUILD.json +33 -0
  70. package/vendor/LICENSE-basis_universal +201 -0
  71. package/vendor/LICENSE-zstd +30 -0
  72. package/vendor/NOTICE +33 -0
  73. package/vendor/basis_encoder.mjs +2 -0
  74. package/vendor/basis_encoder.wasm +0 -0
  75. package/README.md +0 -3
@@ -0,0 +1,248 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join, relative, resolve, sep } from 'node:path';
3
+ import { YaliError } from '@yali-engine/core';
4
+ import { ok } from "./output.js";
5
+ /** This CLI's version, from its package.json (one directory up from src/ and from dist/). */
6
+ const CLI_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
7
+ /** The Yali workspace root above `dir` (it has packages/core named @yali-engine/core), or null. */
8
+ function yaliWorkspace(dir) {
9
+ for (let d = dir;; d = dirname(d)) {
10
+ const core = join(d, 'packages/core/package.json');
11
+ if (existsSync(join(d, 'pnpm-workspace.yaml')) && existsSync(core) && JSON.parse(readFileSync(core, 'utf8')).name === '@yali-engine/core')
12
+ return d;
13
+ if (dirname(d) === d)
14
+ return null;
15
+ }
16
+ }
17
+ /** The template, file by file. `{{name}}`, `{{port}}` and `{{v}}` (dependency range) are filled in. */
18
+ export const TEMPLATE = {
19
+ 'package.json': `{
20
+ "name": "{{name}}",
21
+ "private": true,
22
+ "version": "0.0.0",
23
+ "type": "module",
24
+ "scripts": {
25
+ "dev": "yali dev",
26
+ "build": "yali build",
27
+ "test": "yali test",
28
+ "bench": "yali bench"
29
+ },
30
+ "dependencies": {
31
+ "@yali-engine/core": "{{v}}",
32
+ "@yali-engine/render": "{{v}}"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.14.0",
36
+ "@webgpu/types": "^0.1.60",
37
+ "@yali-engine/cli": "{{v}}",
38
+ "@yali-engine/vite": "{{v}}",
39
+ "typescript": "^5.9.0",
40
+ "vite": "^7.0.0"
41
+ }
42
+ }
43
+ `,
44
+ 'tsconfig.json': `{
45
+ "compilerOptions": {
46
+ "target": "ES2023",
47
+ "module": "ESNext",
48
+ "moduleResolution": "Bundler",
49
+ "lib": ["ES2023", "DOM"],
50
+ "types": ["node", "@webgpu/types", "vite/client", "@yali-engine/vite/client"],
51
+ "strict": true,
52
+ "noEmit": true,
53
+ "allowImportingTsExtensions": true,
54
+ "verbatimModuleSyntax": true,
55
+ "erasableSyntaxOnly": true,
56
+ "skipLibCheck": true
57
+ },
58
+ "include": ["src", "test", "vite.config.ts", "yali.config.ts"]
59
+ }
60
+ `,
61
+ 'yali.config.ts': `import { defineConfig } from '@yali-engine/vite';
62
+
63
+ // port: the dev server refuses any other port rather than drifting.
64
+ // threads: COOP/COEP headers, so SharedArrayBuffer (the threaded core, script workers) works.
65
+ export default defineConfig({
66
+ {{portLine}} threads: true,
67
+ });
68
+ `,
69
+ 'vite.config.ts': `import { yali } from '@yali-engine/vite';
70
+ import { defineConfig } from 'vite';
71
+
72
+ export default defineConfig({ plugins: [yali()] });
73
+ `,
74
+ 'index.html': `<!doctype html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="utf-8" />
78
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
79
+ <title>{{name}}</title>
80
+ <style>
81
+ html, body { margin: 0; height: 100%; background: #09090c; overflow: hidden; }
82
+ canvas { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <canvas id="view"></canvas>
87
+ <script type="module" src="/src/main.ts"></script>
88
+ </body>
89
+ </html>
90
+ `,
91
+ 'public/scene.json': `{
92
+ "lightDir": [0.45, 0.8, 0.4]
93
+ }
94
+ `,
95
+ 'src/game.ts': `import { type Transforms, worldTools, type YaliPlugin } from '@yali-engine/core';
96
+ import { type RenderContext, rgba8 } from '@yali-engine/render';
97
+
98
+ /** Grid side: SIDE × SIDE cubes. */
99
+ export const SIDE = 64;
100
+
101
+ /**
102
+ * The game as a plugin: a grid of cubes in the ECS and a fixed-step system that bobs them.
103
+ * Every entity with \`render.Mesh\` is drawn by the render plugin. Gameplay writes
104
+ * Transform at 60 Hz; rendering reads GlobalTransform, interpolated between fixed steps,
105
+ * so motion is smooth at any display rate.
106
+ */
107
+ export function gamePlugin(): YaliPlugin {
108
+ return {
109
+ name: 'game',
110
+ version: '0.1.0',
111
+ dependencies: [
112
+ { name: '@yali-engine/render', range: '^0.4.0' },
113
+ { name: '@yali-engine/transforms', range: '^0.4.0' },
114
+ ],
115
+ build(b) {
116
+ // The App owns the world; these systems run in its Rust schedule.
117
+ const world = b.app.world;
118
+ const { Transform, bundle } = b.app.resource<Transforms>('transforms');
119
+ const render = b.app.resource<RenderContext>('render');
120
+ const Mesh = render.Mesh!;
121
+ // Identity rotation, scale 1; \`render.Mesh\` zeroed = mesh 0 (the cube), material 0 (white).
122
+ world.spawnBatch([...bundle, Mesh], SIDE * SIDE);
123
+ // Agents query and edit the scene through these (\`yali-mcp\`: world_query, world_set).
124
+ for (const t of worldTools(world)) b.tool(t);
125
+ const all = world.query({ with: [Transform, Mesh], columns: [[Transform, 't', 'write'], [Transform, 's', 'write'], [Mesh, 'color', 'write']] });
126
+ let k = 0;
127
+ all.forEachChunk((c) => {
128
+ const p = c.f64(0), s = c.f32(1), color = c.u32(2);
129
+ for (let i = 0; i < c.length; i++, k++) {
130
+ p[i * 3] = (k % SIDE) * 2 - SIDE;
131
+ p[i * 3 + 2] = Math.floor(k / SIDE) * 2 - SIDE;
132
+ s[i] = 0.8;
133
+ color[i] = rgba8(0.3 + (0.6 * (k % SIDE)) / SIDE, 0.5, 0.3 + (0.6 * Math.floor(k / SIDE)) / SIDE);
134
+ }
135
+ });
136
+
137
+ // FixedUpdate runs at 60 Hz whatever the display rate.
138
+ let t = 0;
139
+ const heights = world.query({ with: [Transform], columns: [[Transform, 't', 'write']] });
140
+ const bob = (c: { length: number; f64(i: number): Float64Array }) => {
141
+ const p = c.f64(0);
142
+ for (let i = 0; i < c.length; i++) p[i * 3 + 1] = Math.sin(t + p[i * 3] * 0.15 + p[i * 3 + 2] * 0.1);
143
+ };
144
+ b.system({ name: 'game.bob', stage: 'FixedUpdate', run: (f) => { t += f.time.fixedDelta; heights.forEachChunk(bob); } });
145
+
146
+ render.camera.distance = SIDE * 2.2;
147
+ },
148
+ };
149
+ }
150
+ `,
151
+ 'src/main.ts': `import { App, type AssetServer, assetsPlugin, jsonLoader, transformsPlugin } from '@yali-engine/core';
152
+ import { type RenderContext, renderPlugin } from '@yali-engine/render';
153
+ import { assetWatch } from 'virtual:yali/assets';
154
+ import { loadCore, threads } from 'virtual:yali/threads';
155
+
156
+ import { gamePlugin } from './game.ts';
157
+
158
+ const canvas = document.getElementById('view') as HTMLCanvasElement;
159
+ // Threaded where the page can run threads (yali dev serves COOP/COEP), single-threaded elsewhere.
160
+ const loaded = await loadCore();
161
+ const app = App.create({
162
+ core: loaded.core,
163
+ threads: threads(loaded),
164
+ plugins: [
165
+ renderPlugin({ canvas }),
166
+ // In dev, editing public/scene.json hot-reloads it (no page reload).
167
+ assetsPlugin({ loaders: [jsonLoader], baseUrl: location.href, watch: assetWatch }),
168
+ transformsPlugin(),
169
+ gamePlugin(),
170
+ ],
171
+ });
172
+ await app.init();
173
+
174
+ const render = app.resource<RenderContext>('render');
175
+ const scene = app.resource<AssetServer>('assets').load<{ lightDir: [number, number, number] } | null>('scene.json');
176
+ scene.onChange(() => {
177
+ const s = scene.get();
178
+ if (s) render.camera.lightDir.set(s.lightDir);
179
+ });
180
+
181
+ // The tools probe: \`yali bench\` and \`yali inspect --live\` read it.
182
+ (globalThis as { __yali?: unknown }).__yali = {
183
+ app,
184
+ profile: (window?: number) => app.profiler.snapshot(window),
185
+ trace: () => app.profiler.trace(),
186
+ quality: () => app.quality.knobs(),
187
+ graph: () => render.graph?.describe() ?? null,
188
+ };
189
+ app.start();
190
+ `,
191
+ 'test/game.test.ts': `import assert from 'node:assert/strict';
192
+ import { test } from 'node:test';
193
+
194
+ import { App, type YaliPlugin } from '@yali-engine/core';
195
+
196
+ // Headless: no GPU, no browser. The App is stepped by hand.
197
+ test('a FixedUpdate system runs once per fixed step', async () => {
198
+ let steps = 0;
199
+ const counter: YaliPlugin = {
200
+ name: 'counter',
201
+ version: '0.1.0',
202
+ build: (b) => b.system({ name: 'counter.tick', stage: 'FixedUpdate', run: () => void steps++ }),
203
+ };
204
+ const app = App.create({ plugins: [counter], frames: { request: () => 0, cancel() {} }, pauseWhenHidden: false });
205
+ await app.init();
206
+ app.step(3 / 60);
207
+ assert.equal(steps, 3);
208
+ const leaks = await app.dispose();
209
+ assert.deepEqual(leaks, { gpuResources: 0, listeners: 0, workers: 0, handles: 0 });
210
+ });
211
+ `,
212
+ };
213
+ /** `yali create <dir>`: writes the template. Refuses a non-empty directory. */
214
+ export function createCommand(opts) {
215
+ if (!opts.dir)
216
+ throw new YaliError('MISSING_ARGUMENT', 'yali create needs a directory.', 'Run `yali create my-game`.');
217
+ const dir = resolve(opts.dir);
218
+ if (existsSync(dir) && readdirSync(dir).length > 0) {
219
+ throw new YaliError('TARGET_NOT_EMPTY', `${dir} is not empty.`, 'Pick a new directory; create never overwrites files.');
220
+ }
221
+ let portLine = '';
222
+ if (opts.port !== undefined) {
223
+ const p = Number(opts.port);
224
+ if (!Number.isInteger(p) || p < 1024 || p > 65535)
225
+ throw new YaliError('BAD_OPTION', `--port must be 1024–65535, got ${opts.port}.`, 'Pick a free port for this project.');
226
+ portLine = ` port: ${p},\n`;
227
+ }
228
+ const workspace = yaliWorkspace(dir);
229
+ const name = basename(dir).toLowerCase().replace(/[^a-z0-9-]+/g, '-');
230
+ // Outside the workspace, the release this CLI belongs to: every @yali-engine package is published at
231
+ // the same version (`^0.0.0` matched no 0.1.0 package, so a fresh app could not install).
232
+ const v = workspace ? 'workspace:*' : `^${CLI_VERSION}`;
233
+ const files = [];
234
+ for (const [rel, body] of Object.entries(TEMPLATE)) {
235
+ const path = join(dir, rel);
236
+ mkdirSync(dirname(path), { recursive: true });
237
+ writeFileSync(path, body.replaceAll('{{name}}', name).replaceAll('{{v}}', v).replaceAll('{{portLine}}', portLine));
238
+ files.push(rel);
239
+ }
240
+ const warnings = workspace
241
+ ? workspace && !relative(workspace, dir).split(sep)[0]?.match(/^(examples|bench|packages)$/)
242
+ ? [`${relative(workspace, dir)} is outside the workspace globs (packages/*, examples/*, bench/*): pnpm will not link it.`]
243
+ : []
244
+ : ['The @yali-engine packages are not published yet: outside the Yali workspace, `pnpm install` cannot resolve them.'];
245
+ const next = [`cd ${relative(process.cwd(), dir) || '.'}`, workspace ? 'pnpm install (from the workspace root)' : 'pnpm install', 'pnpm dev'];
246
+ return ok('create', { dir, name, files, workspace, dependencyRange: v, warnings, next }, [`created ${dir}`, ...files.map((f) => ` ${f}`), ...warnings.map((w) => `warning: ${w}`), 'next:', ...next.map((n) => ` ${n}`)]);
247
+ }
248
+ //# sourceMappingURL=cmd-create.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmd-create.js","sourceRoot":"","sources":["../src/cmd-create.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type Outcome } from './output.ts';
2
+ /**
3
+ * `yali dev`: starts the Vite dev server with `@yali-engine/vite` and keeps running. The outcome
4
+ * (printed once the server listens) carries the URL; Ctrl+C stops it.
5
+ */
6
+ export declare function devCommand(opts: {
7
+ root?: string;
8
+ json: boolean;
9
+ }): Promise<Outcome>;
@@ -0,0 +1,24 @@
1
+ import { yali } from '@yali-engine/vite';
2
+ import { createServer } from 'vite';
3
+ import { ok } from "./output.js";
4
+ import { loadProject } from "./project.js";
5
+ /**
6
+ * `yali dev`: starts the Vite dev server with `@yali-engine/vite` and keeps running. The outcome
7
+ * (printed once the server listens) carries the URL; Ctrl+C stops it.
8
+ */
9
+ export async function devCommand(opts) {
10
+ const project = await loadProject(opts.root);
11
+ const server = await createServer({
12
+ root: project.root,
13
+ configFile: project.viteConfig ?? false,
14
+ plugins: project.viteConfig ? [] : [yali()],
15
+ logLevel: opts.json ? 'silent' : 'info',
16
+ });
17
+ await server.listen();
18
+ const url = server.resolvedUrls?.local[0] ?? `http://localhost:${server.config.server.port}/`;
19
+ const stop = () => void server.close().then(() => process.exit(0));
20
+ process.once('SIGINT', stop);
21
+ process.once('SIGTERM', stop);
22
+ return ok('dev', { url, port: server.config.server.port, root: project.root, threads: project.config.threads }, [`dev server: ${url}`, 'Ctrl+C to stop']);
23
+ }
24
+ //# sourceMappingURL=cmd-dev.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmd-dev.js","sourceRoot":"","sources":["../src/cmd-dev.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,EAAE,EAAgB,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAsC;IACrE,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC;QAChC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,KAAK;QACvC,OAAO,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3C,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;KACxC,CAAC,CAAC;IACH,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,oBAAoB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC;IAC9F,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7B,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,eAAe,GAAG,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAC5J,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type Outcome } from './output.ts';
2
+ /** One compiler message from cargo, as the result and the stderr stream carry it. */
3
+ export interface GameDiagnostic {
4
+ readonly level: string;
5
+ readonly message: string;
6
+ readonly file?: string;
7
+ readonly line?: number;
8
+ readonly column?: number;
9
+ /** rustc's own rendering, for display. */
10
+ readonly rendered?: string;
11
+ }
12
+ export interface GameBuildOptions {
13
+ readonly crate?: string;
14
+ readonly out?: string;
15
+ readonly threads: boolean;
16
+ readonly dev: boolean;
17
+ /** Runtime plugins to compile in (ADR-0082): `anim`, `physics`, comma-separated. They are
18
+ * `yali-core-wasm` features, enabled through the game crate's dependency on it. */
19
+ readonly features?: string;
20
+ readonly json: boolean;
21
+ /** Where --json's event lines go (default stderr). */
22
+ readonly emit?: (line: string) => void;
23
+ }
24
+ export declare function gameCommand(opts: GameBuildOptions): Promise<Outcome>;
@@ -0,0 +1,127 @@
1
+ // `yali game`: builds a game crate (ADR-0077) — a cdylib that depends on yali-core-wasm — into
2
+ // the one module the page loads, the core plus the game's Rust systems: `game.wasm`, or
3
+ // `game.threads.wasm` with --threads. Release builds run binaryen's `wasm-opt -O3` (ADR-0074);
4
+ // --dev skips it (the dev server's rebuild on a Rust edit).
5
+ //
6
+ // Scriptable for a CLI, CI or an editor: no prompts, and with --json the result is the one
7
+ // envelope on stdout while progress and cargo's diagnostics stream to stderr as NDJSON
8
+ // (`{ "event": "progress" | "diagnostic", … }`), so a caller can show them as they happen.
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import { join, resolve } from 'node:path';
13
+ import { createInterface } from 'node:readline';
14
+ import { brotliCompressSync, constants } from 'node:zlib';
15
+ import { YaliError } from '@yali-engine/core';
16
+ import { ok } from "./output.js";
17
+ /** Runtime plugins a core variant can carry (ADR-0082; `yali-core-wasm`'s Cargo features). */
18
+ const CORE_FEATURES = ['anim', 'physics'];
19
+ const TARGET = { single: 'wasm32-unknown-unknown', threads: 'wasm32-wasip1-threads' };
20
+ // The threaded build exports the per-thread setup symbols the host needs to give each worker
21
+ // instance its own stack and TLS (it spawns workers itself, not via std::thread).
22
+ const THREAD_EXPORTS = ['__stack_pointer', '__wasm_init_tls', '__tls_size', '__tls_align'];
23
+ // The features rustc enables for these targets, listed: a game crate's module may carry no
24
+ // target_features section for wasm-opt to detect them from.
25
+ const WASM_FEATURES = ['simd', 'bulk-memory', 'bulk-memory-opt', 'sign-ext', 'nontrapping-float-to-int', 'mutable-globals', 'multivalue', 'reference-types'];
26
+ export async function gameCommand(opts) {
27
+ const emit = (event) => {
28
+ if (opts.json)
29
+ (opts.emit ?? ((l) => process.stderr.write(l)))(`${JSON.stringify(event)}\n`);
30
+ };
31
+ if (!opts.crate)
32
+ throw new YaliError('BAD_OPTION', 'yali game needs --crate <dir>: the game crate.', 'Usage: yali game --crate rust --out wasm [--threads] [--dev].');
33
+ if (!opts.out)
34
+ throw new YaliError('BAD_OPTION', 'yali game needs --out <dir>: where game.wasm goes.', 'Usage: yali game --crate rust --out wasm [--threads] [--dev].');
35
+ const manifest = resolve(opts.crate, 'Cargo.toml');
36
+ if (!existsSync(manifest))
37
+ throw new YaliError('NO_CRATE', `No Cargo.toml in ${resolve(opts.crate)}.`, 'Point --crate at the game crate, the directory with its Cargo.toml.');
38
+ if (spawnSync('cargo', ['--version'], { encoding: 'utf8' }).status !== 0) {
39
+ throw new YaliError('NO_CARGO', 'cargo is not on PATH.', 'Install Rust with rustup (https://rustup.rs); the toolchain in rust-toolchain.toml is then fetched by itself.');
40
+ }
41
+ const target = opts.threads ? TARGET.threads : TARGET.single;
42
+ const plugins = (opts.features ?? '').split(',').map((f) => f.trim()).filter(Boolean);
43
+ const unknown = plugins.filter((f) => !CORE_FEATURES.includes(f));
44
+ if (unknown.length > 0) {
45
+ throw new YaliError('BAD_OPTION', `Unknown --features ${unknown.join(', ')}.`, `Core features are ${CORE_FEATURES.join(', ')} (ADR-0082), comma-separated.`);
46
+ }
47
+ const meta = spawnSync('cargo', ['metadata', '--format-version', '1', '--no-deps', '--manifest-path', manifest], { encoding: 'utf8', maxBuffer: 64 << 20 });
48
+ if (meta.status !== 0)
49
+ throw new YaliError('CARGO_METADATA', `cargo metadata failed: ${meta.stderr.trim().split('\n').slice(-3).join(' ')}`, 'Fix the crate manifest; the message is cargo\'s.');
50
+ const m = JSON.parse(meta.stdout);
51
+ const name = m.packages.find((p) => resolve(p.manifest_path) === manifest)?.name;
52
+ if (!name)
53
+ throw new YaliError('CARGO_METADATA', `cargo metadata did not list ${manifest}.`, 'Make the crate a package (a [package] table), not only a workspace.');
54
+ emit({ event: 'progress', step: 'cargo', target });
55
+ const t0 = performance.now();
56
+ const rustflags = ['-C target-feature=+simd128', ...(opts.threads ? THREAD_EXPORTS.map((s) => `-C link-arg=--export=${s}`) : [])].join(' ');
57
+ const diagnostics = [];
58
+ const stderrTail = [];
59
+ // A dependency's features, named from the selected package: the game depends on yali-core-wasm.
60
+ const cargoFeatures = plugins.length > 0 ? ['--features', plugins.map((f) => `yali-core-wasm/${f}`).join(',')] : [];
61
+ const cargo = spawn('cargo', ['build', '--manifest-path', manifest, '-p', name, '--target', target, '--release', '--message-format=json', ...cargoFeatures], {
62
+ env: { ...process.env, RUSTFLAGS: rustflags },
63
+ stdio: ['ignore', 'pipe', 'pipe'],
64
+ });
65
+ const lines = createInterface({ input: cargo.stdout });
66
+ lines.on('line', (line) => {
67
+ let msg;
68
+ try {
69
+ msg = JSON.parse(line);
70
+ }
71
+ catch {
72
+ return;
73
+ }
74
+ if (msg.reason === 'compiler-message' && msg.message) {
75
+ const span = msg.message.spans?.find((s) => s.is_primary);
76
+ const d = { level: msg.message.level, message: msg.message.message, rendered: msg.message.rendered, ...(span ? { file: span.file_name, line: span.line_start, column: span.column_start } : {}) };
77
+ diagnostics.push(d);
78
+ emit({ event: 'diagnostic', ...d });
79
+ // Plain json leaves the rendering to us: shown as cargo would without --json.
80
+ if (!opts.json && d.rendered)
81
+ process.stderr.write(d.rendered);
82
+ }
83
+ else if (msg.reason === 'compiler-artifact' && msg.target) {
84
+ emit({ event: 'progress', step: 'compiled', crate: msg.target.name, fresh: msg.fresh === true });
85
+ }
86
+ });
87
+ // cargo's own status lines (Compiling …) and its errors: shown as they come without --json.
88
+ createInterface({ input: cargo.stderr }).on('line', (line) => {
89
+ if (line.trim())
90
+ stderrTail.push(line);
91
+ if (stderrTail.length > 20)
92
+ stderrTail.shift();
93
+ if (!opts.json)
94
+ process.stderr.write(`${line}\n`);
95
+ });
96
+ const code = await new Promise((done) => cargo.on('close', (c) => done(c ?? 1)));
97
+ if (code !== 0) {
98
+ const errors = diagnostics.filter((d) => d.level === 'error');
99
+ const first = errors[0];
100
+ const where = first?.file ? ` (${first.file}:${first.line})` : '';
101
+ throw Object.assign(new YaliError('CARGO_BUILD', first ? `${errors.length} error(s); first: ${first.message}${where}` : `cargo build failed: ${stderrTail.slice(-3).join(' ')}`, 'Fix the Rust errors; with --json each one is a diagnostic event on stderr.'), first?.file ? { path: first.file } : {});
102
+ }
103
+ const cargoMs = Math.round(performance.now() - t0);
104
+ const built = join(m.target_directory, target, 'release', `${name.replaceAll('-', '_')}.wasm`);
105
+ // Named like the core variants (ADR-0084): game[.anim][.physics][.threads].wasm.
106
+ const outName = `game${CORE_FEATURES.filter((f) => plugins.includes(f)).map((f) => `.${f}`).join('')}${opts.threads ? '.threads' : ''}.wasm`;
107
+ const outDir = resolve(opts.out);
108
+ const out = join(outDir, outName);
109
+ mkdirSync(outDir, { recursive: true });
110
+ if (opts.dev) {
111
+ copyFileSync(built, out);
112
+ }
113
+ else {
114
+ emit({ event: 'progress', step: 'wasm-opt' });
115
+ const wasmOpt = createRequire(import.meta.url).resolve('binaryen/bin/wasm-opt');
116
+ const features = [...WASM_FEATURES, ...(opts.threads ? ['threads'] : [])];
117
+ const r = spawnSync(process.execPath, [wasmOpt, '-O3', ...features.map((f) => `--enable-${f}`), built, '-o', out], { encoding: 'utf8' });
118
+ if (r.status !== 0)
119
+ throw new YaliError('WASM_OPT', `wasm-opt failed: ${r.stderr.trim().split('\n').slice(-2).join(' ')}`, 'Report it with the message; `--dev` builds without wasm-opt meanwhile.');
120
+ }
121
+ const bytes = readFileSync(out);
122
+ const brotliBytes = brotliCompressSync(bytes, { params: { [constants.BROTLI_PARAM_QUALITY]: 11 } }).length;
123
+ const warnings = diagnostics.filter((d) => d.level === 'warning').length;
124
+ const result = { wasm: out, crate: name, target, threads: opts.threads, features: plugins, optimized: !opts.dev, rawBytes: bytes.length, brotliBytes, cargoMs, warnings, diagnostics };
125
+ return ok('game', result, [`built ${out} (${name}, ${target}) in ${cargoMs} ms: ${(brotliBytes / 1024).toFixed(1)} KB br${opts.dev ? ', not optimized' : ''}${warnings ? `, ${warnings} warning(s)` : ''}`]);
126
+ }
127
+ //# sourceMappingURL=cmd-game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmd-game.js","sourceRoot":"","sources":["../src/cmd-game.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type Outcome } from './output.ts';
2
+ /**
3
+ * `yali headless`: runs the project's headless entry (`yali.config.ts` → `headless`) for
4
+ * `--frames` fixed frames with `--seed`, optionally replaying an input recording, and
5
+ * reports the world hash, the render stub's checksum and the costliest systems
6
+ * (`runHeadless`, ADR-0041). The same inputs give the same hashes, so a run is a test.
7
+ */
8
+ export declare function headlessCommand(opts: {
9
+ root?: string;
10
+ frames?: string;
11
+ seed?: string;
12
+ query?: string;
13
+ recording?: string;
14
+ expect?: string;
15
+ }): Promise<Outcome>;
@@ -0,0 +1,59 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { existsSync, readFileSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+ import { runHeadless, YaliError } from '@yali-engine/core';
13
+ import { ok } from "./output.js";
14
+ import { loadProject } from "./project.js";
15
+ /**
16
+ * `yali headless`: runs the project's headless entry (`yali.config.ts` → `headless`) for
17
+ * `--frames` fixed frames with `--seed`, optionally replaying an input recording, and
18
+ * reports the world hash, the render stub's checksum and the costliest systems
19
+ * (`runHeadless`, ADR-0041). The same inputs give the same hashes, so a run is a test.
20
+ */
21
+ export async function headlessCommand(opts) {
22
+ const project = await loadProject(opts.root);
23
+ const rel = project.config.headless;
24
+ if (!rel) {
25
+ throw new YaliError('NO_HEADLESS_ENTRY', 'yali.config has no `headless` entry.', "Add headless: 'src/headless.ts' to yali.config.ts, exporting (ctx: HeadlessContext) => App (see @yali-engine/core).");
26
+ }
27
+ const file = resolve(project.root, rel);
28
+ if (!existsSync(file))
29
+ throw new YaliError('NO_HEADLESS_ENTRY', `The headless entry ${rel} does not exist.`, 'Fix the `headless` path in yali.config.ts.');
30
+ const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
31
+ if (typeof mod.default !== 'function') {
32
+ throw new YaliError('BAD_HEADLESS_ENTRY', `${rel} has no default export function.`, 'Export default (ctx: HeadlessContext) => App | Promise<App>.');
33
+ }
34
+ const frames = Number(opts.frames ?? 240);
35
+ const seed = Number(opts.seed ?? 1);
36
+ if (!Number.isInteger(seed))
37
+ throw new YaliError('BAD_OPTION', `--seed must be an integer, got ${opts.seed}.`, 'Pass e.g. --seed 1.');
38
+ const options = Object.fromEntries(new URLSearchParams(opts.query ?? ''));
39
+ let recording;
40
+ if (opts.recording) {
41
+ try {
42
+ recording = JSON.parse(readFileSync(resolve(opts.recording), 'utf8'));
43
+ }
44
+ catch (err) {
45
+ throw new YaliError('BAD_RECORDING', `Could not read the recording ${opts.recording}: ${err instanceof Error ? err.message : String(err)}`, 'Pass a JSON file written from input.stop_recording.');
46
+ }
47
+ }
48
+ const r = await runHeadless(mod.default, { frames, seed, options, root: project.root, recording });
49
+ if (opts.expect && r.worldHash !== opts.expect) {
50
+ throw new YaliError('HASH_MISMATCH', `The world hash is ${r.worldHash}, expected ${opts.expect}.`, 'A change altered the simulation. If it was meant to, update the expected hash.');
51
+ }
52
+ const leaked = Object.values(r.leaks).some((n) => n > 0);
53
+ return ok('headless', r, [
54
+ `${rel}: ${r.frames} frames, seed ${r.seed}${r.replayed !== null ? `, replayed ${r.replayed} recorded frames` : ''} in ${(r.ms / 1000).toFixed(2)} s`,
55
+ `world hash ${r.worldHash ?? '(no world)'} entities ${r.entities ?? '-'}${r.render ? ` render checksum ${r.render.checksum.toString(16)} (${r.render.instances} instances)` : ''}`,
56
+ `cpu p50 ${r.cpu.p50.toFixed(2)} ms ${Object.keys(r.systemErrors).length ? `systems that threw: ${JSON.stringify(r.systemErrors)}` : 'no system errors'}${leaked ? ` LEAKS ${JSON.stringify(r.leaks)}` : ''}`,
57
+ ]);
58
+ }
59
+ //# sourceMappingURL=cmd-headless.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cmd-headless.js","sourceRoot":"","sources":["../src/cmd-headless.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { type Outcome } from './output.ts';
2
+ /**
3
+ * `yali inspect`: the project as the tools see it — config, build steps and whether they
4
+ * are stale, budgets, Yali dependencies. With `--live`, also the running app over CDP:
5
+ * lifecycle state and recent events, profiler snapshot, governor knobs, frame graph.
6
+ */
7
+ export declare function inspectCommand(opts: {
8
+ root?: string;
9
+ live?: boolean;
10
+ url?: string;
11
+ cdpPort?: string;
12
+ probe?: string;
13
+ }): Promise<Outcome>;
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { isStale } from '@yali-engine/vite';
4
+ import { loadBudgets } from "./budgets.js";
5
+ import { openPage } from "./cdp.js";
6
+ import { ok } from "./output.js";
7
+ import { cdpPort, loadProject, pageUrl } from "./project.js";
8
+ /**
9
+ * `yali inspect`: the project as the tools see it — config, build steps and whether they
10
+ * are stale, budgets, Yali dependencies. With `--live`, also the running app over CDP:
11
+ * lifecycle state and recent events, profiler snapshot, governor knobs, frame graph.
12
+ */
13
+ export async function inspectCommand(opts) {
14
+ const project = await loadProject(opts.root);
15
+ const rel = (p) => (p ? relative(project.root, p).split(sep).join('/') : null);
16
+ const pkg = JSON.parse(readFileSync(join(project.root, 'package.json'), 'utf8'));
17
+ const yaliDeps = Object.fromEntries(Object.entries({ ...pkg.dependencies, ...pkg.devDependencies }).filter(([k]) => k.startsWith('@yali-engine/')));
18
+ const builds = project.config.builds.map((s) => ({ name: s.name, stale: isStale(project.root, s), outputs: s.outputs }));
19
+ const { budgets, file: budgetsFile } = loadBudgets(project.root);
20
+ const result = {
21
+ root: project.root,
22
+ name: pkg.name ?? null,
23
+ configFile: rel(project.configFile),
24
+ viteConfig: rel(project.viteConfig),
25
+ config: { port: project.config.port ?? null, threads: project.config.threads, assetsDir: project.config.assetsDir ?? null },
26
+ builds,
27
+ budgets: { file: budgetsFile, ...budgets },
28
+ dependencies: yaliDeps,
29
+ };
30
+ const text = [
31
+ `${pkg.name ?? '(unnamed)'} at ${project.root}`,
32
+ `config: ${rel(project.configFile) ?? 'none'} port ${project.config.port ?? 'auto'} threads ${project.config.threads}`,
33
+ ...builds.map((b) => `build step ${b.name}: ${b.stale ? 'STALE' : 'fresh'}`),
34
+ `budgets: ${budgetsFile ?? 'engine defaults'} (size ${budgets.size_brotli_kb.core_runtime_total} KB, threaded ${budgets.size_brotli_kb.core_runtime_threaded ?? budgets.size_brotli_kb.core_runtime_total} KB, frame ${budgets.frame_ms.desktop} ms)`,
35
+ `@yali-engine deps: ${Object.entries(yaliDeps).map(([k, v]) => `${k}@${v}`).join(', ') || 'none'}`,
36
+ ];
37
+ if (opts.live) {
38
+ const url = pageUrl(project, opts.url, undefined);
39
+ const probe = JSON.stringify(opts.probe ?? '__yali');
40
+ const page = await openPage(url, cdpPort(opts.cdpPort));
41
+ try {
42
+ await new Promise((r) => setTimeout(r, 2500));
43
+ const live = await page.evaluate(`(() => {
44
+ const p = globalThis[${probe}];
45
+ if (!p) return null;
46
+ const app = p.app;
47
+ return {
48
+ state: app?.lifecycle?.state ?? null,
49
+ events: app?.lifecycle?.history?.slice(-10).map((e) => e.type) ?? [],
50
+ profile: p.profile ? p.profile(60) : null,
51
+ quality: p.quality ? p.quality() : null,
52
+ graph: p.graph ? p.graph() : null,
53
+ };
54
+ })()`);
55
+ result.live = live ? { url, ...live, consoleErrors: [...page.errors] } : { url, error: 'no tools probe in the page' };
56
+ text.push(live ? `live ${url}: ${String(live.state)}, ${live.events.join(' → ')}` : `live ${url}: no tools probe in the page`);
57
+ }
58
+ finally {
59
+ await page.close();
60
+ }
61
+ }
62
+ return ok('inspect', result, text);
63
+ }
64
+ //# sourceMappingURL=cmd-inspect.js.map