@donut-games/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/donut.js ADDED
@@ -0,0 +1,1724 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../cli/dist/index.js
4
+ import path9 from "path";
5
+ import { Command } from "commander";
6
+ import chalk6 from "chalk";
7
+
8
+ // ../cli/dist/commands/init.js
9
+ import path2 from "path";
10
+ import { spawn } from "child_process";
11
+ import { createRequire } from "module";
12
+ import { fileURLToPath } from "url";
13
+ import fileSystemExtra2 from "fs-extra";
14
+ import chalk from "chalk";
15
+
16
+ // ../cli/dist/commands/template-copier.js
17
+ import path from "path";
18
+ import fileSystemExtra from "fs-extra";
19
+ var TEMPLATE_FILE_SUFFIX = ".tpl";
20
+ async function copyTemplate(options) {
21
+ const { sourceDirectory, destinationDirectory, variant, tokens, logProgress } = options;
22
+ await fileSystemExtra.ensureDir(destinationDirectory);
23
+ const writtenFilesInOrder = [];
24
+ const seenRelativePaths = /* @__PURE__ */ new Set();
25
+ const recordWrite = (relativePath) => {
26
+ if (seenRelativePaths.has(relativePath)) {
27
+ return;
28
+ }
29
+ seenRelativePaths.add(relativePath);
30
+ writtenFilesInOrder.push(relativePath);
31
+ };
32
+ const copyLayer = async (layerDirectory) => {
33
+ if (await fileSystemExtra.pathExists(layerDirectory) === false) {
34
+ return;
35
+ }
36
+ const layerEntries = await collectFilesRecursively(layerDirectory);
37
+ for (const sourceRelativePath of layerEntries) {
38
+ const destinationRelativePath = applyRenameRulesToPath(sourceRelativePath);
39
+ const absoluteSourcePath = path.join(layerDirectory, sourceRelativePath);
40
+ const absoluteDestinationPath = path.join(destinationDirectory, destinationRelativePath);
41
+ await fileSystemExtra.ensureDir(path.dirname(absoluteDestinationPath));
42
+ if (destinationRelativePath.endsWith(TEMPLATE_FILE_SUFFIX)) {
43
+ throw new Error(`Template suffix was not stripped for ${destinationRelativePath}`);
44
+ }
45
+ if (sourceRelativePath.endsWith(TEMPLATE_FILE_SUFFIX)) {
46
+ const rawContents = await fileSystemExtra.readFile(absoluteSourcePath, "utf8");
47
+ const renderedContents = renderTemplate(rawContents, tokens);
48
+ await fileSystemExtra.writeFile(absoluteDestinationPath, renderedContents, "utf8");
49
+ } else {
50
+ await fileSystemExtra.copyFile(absoluteSourcePath, absoluteDestinationPath);
51
+ }
52
+ recordWrite(destinationRelativePath);
53
+ if (logProgress !== void 0) {
54
+ logProgress(destinationRelativePath);
55
+ }
56
+ }
57
+ };
58
+ await copyLayer(path.join(sourceDirectory, "base"));
59
+ await copyLayer(path.join(sourceDirectory, variant));
60
+ return { createdFiles: writtenFilesInOrder };
61
+ }
62
+ function applyRenameRules(fileName) {
63
+ if (fileName.length > 0 && fileName.startsWith("_")) {
64
+ return "." + fileName.slice(1);
65
+ }
66
+ return fileName;
67
+ }
68
+ function renderTemplate(contents, tokens) {
69
+ let rendered = contents;
70
+ const orderedTokenNames = [
71
+ "projectName",
72
+ "rendererTarget",
73
+ "donutEngineVersion",
74
+ "pixiVersion",
75
+ "threeVersion"
76
+ ];
77
+ for (const tokenName of orderedTokenNames) {
78
+ const placeholder = "{{" + tokenName + "}}";
79
+ const tokenValue = tokens[tokenName];
80
+ rendered = rendered.split(placeholder).join(tokenValue);
81
+ }
82
+ return rendered;
83
+ }
84
+ function applyRenameRulesToPath(relativePath) {
85
+ const segments = relativePath.split(path.sep);
86
+ const renamedSegments = segments.map((segment) => applyRenameRules(segment));
87
+ const lastSegmentIndex = renamedSegments.length - 1;
88
+ const lastSegment = renamedSegments[lastSegmentIndex];
89
+ if (lastSegment.endsWith(TEMPLATE_FILE_SUFFIX)) {
90
+ renamedSegments[lastSegmentIndex] = lastSegment.slice(0, lastSegment.length - TEMPLATE_FILE_SUFFIX.length);
91
+ }
92
+ return renamedSegments.join(path.sep);
93
+ }
94
+ async function collectFilesRecursively(rootDirectory) {
95
+ const collectedPaths = [];
96
+ const walkDirectory = async (absoluteDirectory) => {
97
+ const directoryEntries = await fileSystemExtra.readdir(absoluteDirectory, {
98
+ withFileTypes: true
99
+ });
100
+ for (const directoryEntry of directoryEntries) {
101
+ const absoluteEntryPath = path.join(absoluteDirectory, directoryEntry.name);
102
+ if (directoryEntry.isDirectory()) {
103
+ await walkDirectory(absoluteEntryPath);
104
+ continue;
105
+ }
106
+ if (directoryEntry.isFile()) {
107
+ const relativePath = path.relative(rootDirectory, absoluteEntryPath);
108
+ collectedPaths.push(relativePath);
109
+ }
110
+ }
111
+ };
112
+ await walkDirectory(rootDirectory);
113
+ collectedPaths.sort();
114
+ return collectedPaths;
115
+ }
116
+
117
+ // ../cli/dist/commands/init.js
118
+ var DEFAULT_DONUT_ENGINE_VERSION = "0.1.0";
119
+ var DEFAULT_PIXI_VERSION = "0.1.0";
120
+ var DEFAULT_THREE_VERSION = "0.1.0";
121
+ async function initializeProject(options) {
122
+ const { projectName, rendererTarget } = options;
123
+ const log = options.log ?? ((message) => console.log(message));
124
+ const absoluteDirectory = path2.resolve(options.targetDirectory);
125
+ if (await fileSystemExtra2.pathExists(absoluteDirectory)) {
126
+ const existingEntries = await fileSystemExtra2.readdir(absoluteDirectory);
127
+ if (existingEntries.length > 0) {
128
+ throw new Error(`Target directory "${absoluteDirectory}" already exists and is not empty.`);
129
+ }
130
+ }
131
+ await fileSystemExtra2.ensureDir(absoluteDirectory);
132
+ log(chalk.cyan(`Scaffolding "${projectName}" (${rendererTarget}) at ${absoluteDirectory}`));
133
+ const templateSourceDirectory = await resolveTemplateDirectory();
134
+ const templateTokens = {
135
+ projectName,
136
+ rendererTarget,
137
+ donutEngineVersion: DEFAULT_DONUT_ENGINE_VERSION,
138
+ pixiVersion: DEFAULT_PIXI_VERSION,
139
+ threeVersion: DEFAULT_THREE_VERSION
140
+ };
141
+ const copyResult = await copyTemplate({
142
+ sourceDirectory: templateSourceDirectory,
143
+ destinationDirectory: absoluteDirectory,
144
+ variant: rendererTarget,
145
+ tokens: templateTokens,
146
+ logProgress: (relativePath) => {
147
+ log(chalk.green(` created ${relativePath}`));
148
+ }
149
+ });
150
+ if (options.skipInstall !== true) {
151
+ await runDependencyInstall(absoluteDirectory, log);
152
+ }
153
+ log(chalk.cyan(`
154
+ Done. Next steps:`));
155
+ log(chalk.white(` cd ${projectName}`));
156
+ if (options.skipInstall === true) {
157
+ log(chalk.white(` npm install`));
158
+ }
159
+ log(chalk.white(` npm run dev`));
160
+ return { absoluteDirectory, createdFiles: copyResult.createdFiles };
161
+ }
162
+ async function resolveTemplateDirectory() {
163
+ const checkedPaths = [];
164
+ const shippedCandidate = fileURLToPath(new URL("../template", import.meta.url));
165
+ checkedPaths.push(shippedCandidate);
166
+ if (await fileSystemExtra2.pathExists(shippedCandidate)) {
167
+ return shippedCandidate;
168
+ }
169
+ try {
170
+ const nodeRequire = createRequire(import.meta.url);
171
+ const createGameManifestPath = nodeRequire.resolve("@donut-games/create-game/package.json");
172
+ const monorepoCandidate = path2.join(path2.dirname(createGameManifestPath), "template");
173
+ checkedPaths.push(monorepoCandidate);
174
+ if (await fileSystemExtra2.pathExists(monorepoCandidate)) {
175
+ return monorepoCandidate;
176
+ }
177
+ } catch {
178
+ }
179
+ const thisModuleDirectory = path2.dirname(fileURLToPath(import.meta.url));
180
+ let walkingDirectory = thisModuleDirectory;
181
+ for (let walkDepth = 0; walkDepth < 8; walkDepth += 1) {
182
+ const candidate = path2.join(walkingDirectory, "packages", "create-donut-game", "template");
183
+ checkedPaths.push(candidate);
184
+ if (await fileSystemExtra2.pathExists(candidate)) {
185
+ return candidate;
186
+ }
187
+ const parentDirectory = path2.dirname(walkingDirectory);
188
+ if (parentDirectory === walkingDirectory) {
189
+ break;
190
+ }
191
+ walkingDirectory = parentDirectory;
192
+ }
193
+ throw new Error(`Unable to locate the @donut-games/create-game template directory. Checked:
194
+ ${checkedPaths.join("\n ")}`);
195
+ }
196
+ async function runDependencyInstall(workingDirectory, log) {
197
+ const candidatePackageManagers = ["pnpm", "npm"];
198
+ for (const packageManager of candidatePackageManagers) {
199
+ const exitCode = await spawnInstall(packageManager, workingDirectory);
200
+ if (exitCode === "binary-missing") {
201
+ continue;
202
+ }
203
+ if (exitCode === 0) {
204
+ return;
205
+ }
206
+ break;
207
+ }
208
+ log(chalk.yellow("\u26A0 dependency install failed, run `npm install` manually"));
209
+ }
210
+ function spawnInstall(packageManager, workingDirectory) {
211
+ return new Promise((resolve) => {
212
+ const child = spawn(packageManager, ["install"], {
213
+ cwd: workingDirectory,
214
+ stdio: "inherit",
215
+ shell: false
216
+ });
217
+ child.on("error", (error) => {
218
+ if (error.code === "ENOENT") {
219
+ resolve("binary-missing");
220
+ return;
221
+ }
222
+ resolve(1);
223
+ });
224
+ child.on("exit", (code) => {
225
+ resolve(code ?? 1);
226
+ });
227
+ });
228
+ }
229
+
230
+ // ../cli/dist/commands/dev.js
231
+ import path4 from "path";
232
+ import fileSystemExtra4 from "fs-extra";
233
+ import chalk2 from "chalk";
234
+
235
+ // ../cli/dist/shared/monorepo-aliases.js
236
+ import path3 from "path";
237
+ import fileSystemExtra3 from "fs-extra";
238
+ var INTERNAL_DONUT_PACKAGE_NAMES = [
239
+ "core",
240
+ "math",
241
+ "pixi",
242
+ "three",
243
+ "ctrllr",
244
+ "player"
245
+ ];
246
+ var PUBLISHED_ENGINE_SUBPATH_NAMES = [
247
+ "math",
248
+ "core",
249
+ "ctrllr",
250
+ "pixi",
251
+ "three",
252
+ "player"
253
+ ];
254
+ async function resolveMonorepoDevAliases(monorepoRoot) {
255
+ const packagesDirectory = path3.join(monorepoRoot, "packages");
256
+ const resolvedAliasEntries = [];
257
+ const umbrellaSourceDirectory = path3.join(packagesDirectory, "donut-engine", "src");
258
+ for (const subpathName of PUBLISHED_ENGINE_SUBPATH_NAMES) {
259
+ const subpathBarrelPath = path3.join(umbrellaSourceDirectory, subpathName, "index.ts");
260
+ if (await fileSystemExtra3.pathExists(subpathBarrelPath)) {
261
+ resolvedAliasEntries.push({
262
+ find: `@donut-games/engine/${subpathName}`,
263
+ replacement: subpathBarrelPath
264
+ });
265
+ }
266
+ }
267
+ const umbrellaBarrelPath = path3.join(umbrellaSourceDirectory, "index.ts");
268
+ if (await fileSystemExtra3.pathExists(umbrellaBarrelPath)) {
269
+ resolvedAliasEntries.push({
270
+ find: /^@donut-games\/engine$/,
271
+ replacement: umbrellaBarrelPath
272
+ });
273
+ }
274
+ for (const internalPackageName of INTERNAL_DONUT_PACKAGE_NAMES) {
275
+ const internalSourceDirectory = path3.join(packagesDirectory, internalPackageName, "src");
276
+ if (await fileSystemExtra3.pathExists(internalSourceDirectory)) {
277
+ resolvedAliasEntries.push({
278
+ find: `@donut/${internalPackageName}`,
279
+ replacement: internalSourceDirectory
280
+ });
281
+ }
282
+ }
283
+ await assertEveryPublishedSubpathHasAnAlias(monorepoRoot, resolvedAliasEntries);
284
+ return resolvedAliasEntries;
285
+ }
286
+ async function assertEveryPublishedSubpathHasAnAlias(monorepoRoot, resolvedAliasEntries) {
287
+ const umbrellaPackageJsonPath = path3.join(monorepoRoot, "packages", "donut-engine", "package.json");
288
+ if (!await fileSystemExtra3.pathExists(umbrellaPackageJsonPath)) {
289
+ return;
290
+ }
291
+ const parsedManifest = await fileSystemExtra3.readJson(umbrellaPackageJsonPath);
292
+ const exportsField = parsedManifest.exports;
293
+ if (exportsField === void 0 || exportsField === null) {
294
+ return;
295
+ }
296
+ const aliasedStringFinds = new Set(resolvedAliasEntries.map((entry) => entry.find).filter((find) => typeof find === "string"));
297
+ const missingAliasSubpaths = [];
298
+ for (const exportKey of Object.keys(exportsField)) {
299
+ if (exportKey === "." || exportKey === "./package.json") {
300
+ continue;
301
+ }
302
+ if (!exportKey.startsWith("./")) {
303
+ continue;
304
+ }
305
+ const subpathName = exportKey.slice(2);
306
+ const expectedAliasKey = `@donut-games/engine/${subpathName}`;
307
+ if (!aliasedStringFinds.has(expectedAliasKey)) {
308
+ missingAliasSubpaths.push(expectedAliasKey);
309
+ }
310
+ }
311
+ if (missingAliasSubpaths.length > 0) {
312
+ throw new Error(`resolveMonorepoDevAliases: missing Vite aliases for published subpaths \u2014 ${missingAliasSubpaths.join(", ")}. Every subpath in packages/donut-engine/package.json "exports" must have a matching alias.`);
313
+ }
314
+ }
315
+
316
+ // ../cli/dist/commands/dev.js
317
+ async function readProjectManifest(projectDirectory) {
318
+ const manifestPath = path4.join(projectDirectory, "donut.json");
319
+ if (!await fileSystemExtra4.pathExists(manifestPath)) {
320
+ throw new Error(`No donut.json found in ${projectDirectory}. Run 'donut init' first.`);
321
+ }
322
+ const parsed = await fileSystemExtra4.readJson(manifestPath);
323
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
324
+ throw new Error(`donut.json is missing required field 'name'.`);
325
+ }
326
+ if (parsed.rendererTarget !== "pixi-2d" && parsed.rendererTarget !== "three-3d") {
327
+ throw new Error(`donut.json has invalid 'rendererTarget' \u2014 expected 'pixi-2d' or 'three-3d'.`);
328
+ }
329
+ return {
330
+ name: parsed.name,
331
+ rendererTarget: parsed.rendererTarget,
332
+ donutEngineVersion: parsed.donutEngineVersion
333
+ };
334
+ }
335
+ async function detectMonorepoRoot(projectDirectory) {
336
+ let currentDirectory = path4.resolve(projectDirectory);
337
+ for (let depth = 0; depth < 8; depth++) {
338
+ const workspaceFile = path4.join(currentDirectory, "pnpm-workspace.yaml");
339
+ if (await fileSystemExtra4.pathExists(workspaceFile)) {
340
+ return currentDirectory;
341
+ }
342
+ const parentDirectory = path4.dirname(currentDirectory);
343
+ if (parentDirectory === currentDirectory) {
344
+ return void 0;
345
+ }
346
+ currentDirectory = parentDirectory;
347
+ }
348
+ return void 0;
349
+ }
350
+ async function writeDevEntryFiles(projectDirectory, manifest) {
351
+ const donutDirectory = path4.join(projectDirectory, ".donut");
352
+ await fileSystemExtra4.ensureDir(donutDirectory);
353
+ const gitIgnorePath = path4.join(donutDirectory, ".gitignore");
354
+ await fileSystemExtra4.writeFile(gitIgnorePath, "*\n", "utf8");
355
+ const indexHtmlPath = path4.join(donutDirectory, "index.html");
356
+ await fileSystemExtra4.writeFile(indexHtmlPath, buildIndexHtml(manifest), "utf8");
357
+ const devEntryPath = path4.join(donutDirectory, "dev-entry.ts");
358
+ await fileSystemExtra4.writeFile(devEntryPath, buildDevEntrySource(manifest), "utf8");
359
+ return donutDirectory;
360
+ }
361
+ function buildIndexHtml(manifest) {
362
+ return `<!doctype html>
363
+ <html lang="en">
364
+ <head>
365
+ <meta charset="utf-8" />
366
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
367
+ <title>${manifest.name} \u2014 Donut dev</title>
368
+ <style>
369
+ html, body { margin: 0; padding: 0; height: 100%; background: #0a0a12; overflow: hidden; }
370
+ canvas#game-canvas { display: block; width: 100vw; height: 100vh; }
371
+ </style>
372
+ </head>
373
+ <body>
374
+ <canvas id="game-canvas"></canvas>
375
+ <script type="module" src="./dev-entry.ts"></script>
376
+ </body>
377
+ </html>
378
+ `;
379
+ }
380
+ function buildDevEntrySource(manifest) {
381
+ return `// AUTO-GENERATED by \`donut dev\`. Do not edit \u2014 regenerated every launch.
382
+ import { startGame } from '@donut/player';
383
+ import { createDefaultScene } from '../src/scenes/default-scene';
384
+
385
+ const discoveredComponentModules = import.meta.glob('../src/components/**/*.ts', { eager: true });
386
+ const discoveredSystemModules = import.meta.glob('../src/systems/**/*.ts', { eager: true });
387
+
388
+ for (const modulePath of Object.keys(discoveredComponentModules)) {
389
+ console.log('[Donut dev] discovered component module:', modulePath);
390
+ }
391
+ for (const modulePath of Object.keys(discoveredSystemModules)) {
392
+ console.log('[Donut dev] discovered system module:', modulePath);
393
+ }
394
+
395
+ async function bootstrapDevelopmentRuntime(): Promise<void> {
396
+ const canvasElement = document.getElementById('game-canvas');
397
+ if (!(canvasElement instanceof HTMLCanvasElement)) {
398
+ throw new Error('[Donut dev] Missing <canvas id="game-canvas">');
399
+ }
400
+
401
+ await startGame({
402
+ canvas: canvasElement,
403
+ rendererTarget: '${manifest.rendererTarget}',
404
+ sceneFactory: createDefaultScene,
405
+ enableDevelopmentBridge: true,
406
+ enableDebugOverlay: true,
407
+ });
408
+
409
+ console.log('[Donut dev] booted renderer=${manifest.rendererTarget}');
410
+ }
411
+
412
+ bootstrapDevelopmentRuntime().catch((error) => {
413
+ console.error('[Donut dev] bootstrap failed', error);
414
+ });
415
+ `;
416
+ }
417
+ async function buildViteConfig(parameters) {
418
+ const { projectDirectory, donutRoot, port, open } = parameters;
419
+ const monorepoRoot = await detectMonorepoRoot(projectDirectory);
420
+ const resolveAlias = monorepoRoot !== void 0 ? await resolveMonorepoDevAliases(monorepoRoot) : [];
421
+ return {
422
+ root: donutRoot,
423
+ configFile: false,
424
+ server: {
425
+ port,
426
+ open,
427
+ strictPort: false,
428
+ fs: {
429
+ allow: [
430
+ projectDirectory,
431
+ ...monorepoRoot !== void 0 ? [monorepoRoot] : []
432
+ ]
433
+ }
434
+ },
435
+ resolve: {
436
+ alias: resolveAlias
437
+ },
438
+ optimizeDeps: {
439
+ // Let Vite figure out deps from the generated entry.
440
+ }
441
+ };
442
+ }
443
+ async function runDevServer(options) {
444
+ const { projectDirectory, port = 5173, open = true, log = (message) => console.log(message), startViteServer } = options;
445
+ const manifest = await readProjectManifest(projectDirectory);
446
+ log(chalk2.cyan(`Donut dev \u2014 project '${manifest.name}' (${manifest.rendererTarget})`));
447
+ const donutRoot = await writeDevEntryFiles(projectDirectory, manifest);
448
+ const viteConfig = await buildViteConfig({ projectDirectory, donutRoot, port, open });
449
+ const startServer = startViteServer ?? (async (configuration) => {
450
+ const viteModule = await import("vite");
451
+ return viteModule.createServer(configuration);
452
+ });
453
+ const server = await startServer(viteConfig);
454
+ await server.listen();
455
+ const resolvedLocalUrl = server.resolvedUrls?.local?.[0];
456
+ if (resolvedLocalUrl !== void 0) {
457
+ log(chalk2.green(`Dev server ready at ${resolvedLocalUrl}`));
458
+ } else {
459
+ log(chalk2.green(`Dev server listening on port ${port}`));
460
+ }
461
+ }
462
+
463
+ // ../cli/dist/commands/validate.js
464
+ import path5 from "path";
465
+ import { spawn as spawn2 } from "child_process";
466
+ import { createRequire as createRequire2 } from "module";
467
+ import { fileURLToPath as fileURLToPath2 } from "url";
468
+ import fileSystemExtra5 from "fs-extra";
469
+ var REQUIRED_MANIFEST_FIELDS = ["name", "version", "rendererTarget", "donutEngineVersion"];
470
+ var ALLOWED_RENDERER_TARGETS = /* @__PURE__ */ new Set(["pixi-2d", "three-3d"]);
471
+ var GAME_LOGIC_FORBIDDEN_RENDERER_IMPORT_PREFIXES = [
472
+ "@donut-games/engine/pixi",
473
+ "@donut-games/engine/three",
474
+ "@donut/pixi",
475
+ "@donut/three"
476
+ ];
477
+ var WRONG_RENDERER_FORBIDDEN_IMPORT_BY_TARGET = {
478
+ "pixi-2d": "@donut-games/engine/three",
479
+ "three-3d": "@donut-games/engine/pixi"
480
+ };
481
+ var WRONG_RENDERER_ALLOWED_IMPORT_BY_TARGET = {
482
+ "pixi-2d": "@donut-games/engine/pixi",
483
+ "three-3d": "@donut-games/engine/three"
484
+ };
485
+ async function runValidation(options) {
486
+ const { projectDirectory } = options;
487
+ const errors = [];
488
+ const warnings = [];
489
+ const parsedManifest = await validateManifest(projectDirectory, errors);
490
+ if (options.skipTypeCheck !== true) {
491
+ await validateTypes(projectDirectory, errors);
492
+ }
493
+ const rendererTarget = parsedManifest?.rendererTarget;
494
+ await validateImportGraph(projectDirectory, rendererTarget, errors);
495
+ await validateAssetReferences(projectDirectory, errors);
496
+ return { errors, warnings };
497
+ }
498
+ async function validateManifest(projectDirectory, errors) {
499
+ const manifestPath = path5.join(projectDirectory, "donut.json");
500
+ if (!await fileSystemExtra5.pathExists(manifestPath)) {
501
+ errors.push({
502
+ filePath: manifestPath,
503
+ message: "donut.json is missing at the project root",
504
+ code: "MANIFEST_INVALID"
505
+ });
506
+ return null;
507
+ }
508
+ let raw;
509
+ try {
510
+ raw = await fileSystemExtra5.readFile(manifestPath, "utf8");
511
+ } catch (readError) {
512
+ errors.push({
513
+ filePath: manifestPath,
514
+ message: `Unable to read donut.json: ${readError.message}`,
515
+ code: "MANIFEST_INVALID"
516
+ });
517
+ return null;
518
+ }
519
+ let parsed;
520
+ try {
521
+ parsed = JSON.parse(raw);
522
+ } catch (parseError) {
523
+ errors.push({
524
+ filePath: manifestPath,
525
+ message: `donut.json is not valid JSON: ${parseError.message}`,
526
+ code: "MANIFEST_INVALID"
527
+ });
528
+ return null;
529
+ }
530
+ let hasRequiredFieldError = false;
531
+ for (const fieldName of REQUIRED_MANIFEST_FIELDS) {
532
+ const value = parsed[fieldName];
533
+ if (typeof value !== "string" || value.trim().length === 0) {
534
+ errors.push({
535
+ filePath: manifestPath,
536
+ message: `donut.json missing required string field "${fieldName}"`,
537
+ code: "MANIFEST_INVALID"
538
+ });
539
+ hasRequiredFieldError = true;
540
+ }
541
+ }
542
+ const rendererTargetValue = parsed.rendererTarget;
543
+ if (typeof rendererTargetValue === "string" && !ALLOWED_RENDERER_TARGETS.has(rendererTargetValue)) {
544
+ errors.push({
545
+ filePath: manifestPath,
546
+ message: `donut.json "rendererTarget" must be one of pixi-2d, three-3d (got "${rendererTargetValue}")`,
547
+ code: "MANIFEST_INVALID"
548
+ });
549
+ }
550
+ if (hasRequiredFieldError) {
551
+ return null;
552
+ }
553
+ return {
554
+ name: parsed.name,
555
+ version: parsed.version,
556
+ rendererTarget: parsed.rendererTarget,
557
+ donutEngineVersion: parsed.donutEngineVersion
558
+ };
559
+ }
560
+ async function validateTypes(projectDirectory, errors) {
561
+ const tsconfigPath = path5.join(projectDirectory, "tsconfig.json");
562
+ if (!await fileSystemExtra5.pathExists(tsconfigPath)) {
563
+ return;
564
+ }
565
+ const typeScriptCompilerPath = resolveTypeScriptCompiler(projectDirectory);
566
+ if (typeScriptCompilerPath === void 0) {
567
+ errors.push({
568
+ filePath: tsconfigPath,
569
+ message: "Unable to locate a TypeScript compiler (tsc) to run the type-check",
570
+ code: "TYPE_ERROR"
571
+ });
572
+ return;
573
+ }
574
+ const { stdout, stderr } = await runProcess(process.execPath, [typeScriptCompilerPath, "--noEmit", "-p", tsconfigPath], projectDirectory);
575
+ const combined = `${stdout}
576
+ ${stderr}`;
577
+ const diagnosticLineRegex = /^([^\s(][^(]*?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm;
578
+ let match;
579
+ while ((match = diagnosticLineRegex.exec(combined)) !== null) {
580
+ const [, rawFilePath, lineText, columnText, typeScriptCode, messageText] = match;
581
+ const absoluteFilePath = path5.isAbsolute(rawFilePath) ? rawFilePath : path5.join(projectDirectory, rawFilePath);
582
+ errors.push({
583
+ filePath: absoluteFilePath,
584
+ line: Number.parseInt(lineText, 10),
585
+ column: Number.parseInt(columnText, 10),
586
+ message: `${typeScriptCode}: ${messageText.trim()}`,
587
+ code: "TYPE_ERROR"
588
+ });
589
+ }
590
+ }
591
+ function resolveTypeScriptCompiler(projectDirectory) {
592
+ const projectLocal = path5.join(projectDirectory, "node_modules", "typescript", "bin", "tsc");
593
+ if (fileSystemExtra5.existsSync(projectLocal)) {
594
+ return projectLocal;
595
+ }
596
+ try {
597
+ const requireFromHere = createRequire2(import.meta.url);
598
+ const typeScriptPackageJsonPath = requireFromHere.resolve("typescript/package.json");
599
+ const typeScriptPackageDirectory = path5.dirname(typeScriptPackageJsonPath);
600
+ const fallback = path5.join(typeScriptPackageDirectory, "bin", "tsc");
601
+ if (fileSystemExtra5.existsSync(fallback)) {
602
+ return fallback;
603
+ }
604
+ } catch {
605
+ }
606
+ return void 0;
607
+ }
608
+ function runProcess(command, argumentList, workingDirectory) {
609
+ return new Promise((resolve) => {
610
+ const child = spawn2(command, argumentList, {
611
+ cwd: workingDirectory,
612
+ env: process.env
613
+ });
614
+ let stdout = "";
615
+ let stderr = "";
616
+ child.stdout.on("data", (chunk) => {
617
+ stdout += chunk.toString("utf8");
618
+ });
619
+ child.stderr.on("data", (chunk) => {
620
+ stderr += chunk.toString("utf8");
621
+ });
622
+ child.on("close", (exitCode) => {
623
+ resolve({ stdout, stderr, exitCode });
624
+ });
625
+ child.on("error", () => {
626
+ resolve({ stdout, stderr, exitCode: null });
627
+ });
628
+ });
629
+ }
630
+ async function validateImportGraph(projectDirectory, rendererTarget, errors) {
631
+ await validateGameLogicImports(projectDirectory, errors);
632
+ await validateWrongRendererImports(projectDirectory, rendererTarget, errors);
633
+ }
634
+ async function validateGameLogicImports(projectDirectory, errors) {
635
+ const gameLogicDirectories = [
636
+ path5.join(projectDirectory, "src", "components"),
637
+ path5.join(projectDirectory, "src", "systems")
638
+ ];
639
+ const importLineRegex = /^\s*import\s+[^;]*?from\s+['"]([^'"]+)['"]/;
640
+ for (const directoryPath of gameLogicDirectories) {
641
+ if (!await fileSystemExtra5.pathExists(directoryPath)) {
642
+ continue;
643
+ }
644
+ const typeScriptFiles = await collectTypeScriptFiles(directoryPath);
645
+ for (const filePath of typeScriptFiles) {
646
+ const contents = await fileSystemExtra5.readFile(filePath, "utf8");
647
+ const lines = contents.split(/\r?\n/);
648
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
649
+ const lineText = lines[lineIndex];
650
+ const matched = importLineRegex.exec(lineText);
651
+ if (matched === null) {
652
+ continue;
653
+ }
654
+ const importedSpecifier = matched[1];
655
+ if (GAME_LOGIC_FORBIDDEN_RENDERER_IMPORT_PREFIXES.some((disallowedPrefix) => importedSpecifier === disallowedPrefix || importedSpecifier.startsWith(`${disallowedPrefix}/`))) {
656
+ errors.push({
657
+ filePath,
658
+ line: lineIndex + 1,
659
+ message: `Renderer import "${importedSpecifier}" is not allowed in game logic (components/systems must only import @donut-games/engine/core, @donut-games/engine/math, or relative paths)`,
660
+ code: "RENDERER_IMPORT_IN_GAME_LOGIC"
661
+ });
662
+ }
663
+ }
664
+ }
665
+ }
666
+ }
667
+ async function validateWrongRendererImports(projectDirectory, rendererTarget, errors) {
668
+ if (rendererTarget === void 0) {
669
+ return;
670
+ }
671
+ const wrongRendererImportSpecifierPrefix = WRONG_RENDERER_FORBIDDEN_IMPORT_BY_TARGET[rendererTarget];
672
+ const allowedRendererImportSpecifierPrefix = WRONG_RENDERER_ALLOWED_IMPORT_BY_TARGET[rendererTarget];
673
+ if (wrongRendererImportSpecifierPrefix === void 0 || allowedRendererImportSpecifierPrefix === void 0) {
674
+ return;
675
+ }
676
+ const sourceRoot = path5.join(projectDirectory, "src");
677
+ if (!await fileSystemExtra5.pathExists(sourceRoot)) {
678
+ return;
679
+ }
680
+ const importLineRegex = /^\s*import\s+[^;]*?from\s+['"]([^'"]+)['"]/;
681
+ const typeScriptFiles = await collectTypeScriptFiles(sourceRoot);
682
+ for (const filePath of typeScriptFiles) {
683
+ const contents = await fileSystemExtra5.readFile(filePath, "utf8");
684
+ const lines = contents.split(/\r?\n/);
685
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
686
+ const lineText = lines[lineIndex];
687
+ const matched = importLineRegex.exec(lineText);
688
+ if (matched === null) {
689
+ continue;
690
+ }
691
+ const importedSpecifier = matched[1];
692
+ if (importedSpecifier === wrongRendererImportSpecifierPrefix || importedSpecifier.startsWith(`${wrongRendererImportSpecifierPrefix}/`)) {
693
+ errors.push({
694
+ filePath,
695
+ line: lineIndex + 1,
696
+ message: `Renderer import "${importedSpecifier}" is not allowed in a "${rendererTarget}" project. This project's donut.json specifies rendererTarget: "${rendererTarget}", which only permits ${allowedRendererImportSpecifierPrefix} for renderer-specific code.`,
697
+ code: "WRONG_RENDERER_IMPORT"
698
+ });
699
+ }
700
+ }
701
+ }
702
+ }
703
+ async function collectTypeScriptFiles(directoryPath) {
704
+ const collected = [];
705
+ const entries = await fileSystemExtra5.readdir(directoryPath, { withFileTypes: true });
706
+ for (const entry of entries) {
707
+ const entryPath = path5.join(directoryPath, entry.name);
708
+ if (entry.isDirectory()) {
709
+ collected.push(...await collectTypeScriptFiles(entryPath));
710
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
711
+ collected.push(entryPath);
712
+ }
713
+ }
714
+ return collected;
715
+ }
716
+ async function validateAssetReferences(projectDirectory, errors) {
717
+ const sourceRoot = path5.join(projectDirectory, "src");
718
+ const assetsRoot = path5.join(projectDirectory, "assets");
719
+ if (!await fileSystemExtra5.pathExists(sourceRoot)) {
720
+ return;
721
+ }
722
+ const typeScriptFiles = await collectTypeScriptFiles(sourceRoot);
723
+ const texturePathRegex = /texturePath\s*(?::[^='"]*)?=?\s*['"]([^'"]*)['"]/g;
724
+ const assetsLiteralRegex = /['"]assets\/([^'"]+)['"]/g;
725
+ for (const filePath of typeScriptFiles) {
726
+ const contents = await fileSystemExtra5.readFile(filePath, "utf8");
727
+ const lines = contents.split(/\r?\n/);
728
+ const referencedAssets = [];
729
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
730
+ const lineText = lines[lineIndex];
731
+ texturePathRegex.lastIndex = 0;
732
+ let textureMatch;
733
+ while ((textureMatch = texturePathRegex.exec(lineText)) !== null) {
734
+ const referenced = textureMatch[1];
735
+ if (referenced.length === 0) {
736
+ continue;
737
+ }
738
+ referencedAssets.push({ relativePath: referenced, line: lineIndex + 1 });
739
+ }
740
+ assetsLiteralRegex.lastIndex = 0;
741
+ let assetsMatch;
742
+ while ((assetsMatch = assetsLiteralRegex.exec(lineText)) !== null) {
743
+ referencedAssets.push({
744
+ relativePath: assetsMatch[1],
745
+ line: lineIndex + 1
746
+ });
747
+ }
748
+ }
749
+ for (const { relativePath, line } of referencedAssets) {
750
+ const normalized = relativePath.startsWith("assets/") ? relativePath.slice("assets/".length) : relativePath;
751
+ const absoluteAssetPath = path5.join(assetsRoot, normalized);
752
+ if (!await fileSystemExtra5.pathExists(absoluteAssetPath)) {
753
+ errors.push({
754
+ filePath,
755
+ line,
756
+ message: `Referenced asset "${relativePath}" not found under assets/`,
757
+ code: "MISSING_ASSET"
758
+ });
759
+ }
760
+ }
761
+ }
762
+ }
763
+
764
+ // ../cli/dist/commands/build.js
765
+ import path6 from "path";
766
+ import fileSystemExtra6 from "fs-extra";
767
+ import chalk3 from "chalk";
768
+ import JSZip from "jszip";
769
+ var BuildViolationError = class extends Error {
770
+ violations;
771
+ constructor(message, violations) {
772
+ super(message);
773
+ this.name = "BuildViolationError";
774
+ this.violations = violations;
775
+ }
776
+ };
777
+ var FORBIDDEN_PATTERNS = [
778
+ { pattern: /\bfetch\s*\(/g, severity: "error" },
779
+ { pattern: /\blocalStorage\b/g, severity: "error" },
780
+ { pattern: /\bsessionStorage\b/g, severity: "error" },
781
+ { pattern: /\bXMLHttpRequest\b/g, severity: "error" },
782
+ { pattern: /\bWebSocket\b/g, severity: "error" },
783
+ { pattern: /\beval\s*\(/g, severity: "error" },
784
+ { pattern: /new\s+Function\s*\(/g, severity: "error" },
785
+ { pattern: /\bimport\s*\(/g, severity: "error" },
786
+ { pattern: /\.innerHTML\s*=/g, severity: "error" },
787
+ { pattern: /\bwindow\b/g, severity: "warning" },
788
+ { pattern: /\bdocument\b/g, severity: "warning" }
789
+ ];
790
+ async function collectTypeScriptFilesRecursively(directoryPath) {
791
+ if (!await fileSystemExtra6.pathExists(directoryPath)) {
792
+ return [];
793
+ }
794
+ const collected = [];
795
+ const entries = await fileSystemExtra6.readdir(directoryPath, { withFileTypes: true });
796
+ for (const entry of entries) {
797
+ const entryPath = path6.join(directoryPath, entry.name);
798
+ if (entry.isDirectory()) {
799
+ collected.push(...await collectTypeScriptFilesRecursively(entryPath));
800
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
801
+ collected.push(entryPath);
802
+ }
803
+ }
804
+ return collected;
805
+ }
806
+ async function scanUserSourcesForForbiddenPatterns(projectDirectory) {
807
+ const directoriesToScan = [
808
+ path6.join(projectDirectory, "src", "components"),
809
+ path6.join(projectDirectory, "src", "systems")
810
+ ];
811
+ const violations = [];
812
+ for (const directoryPath of directoriesToScan) {
813
+ const files = await collectTypeScriptFilesRecursively(directoryPath);
814
+ for (const filePath of files) {
815
+ const contents = await fileSystemExtra6.readFile(filePath, "utf8");
816
+ const lines = contents.split(/\r?\n/);
817
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
818
+ const lineText = lines[lineIndex];
819
+ for (const { pattern, severity } of FORBIDDEN_PATTERNS) {
820
+ pattern.lastIndex = 0;
821
+ let match;
822
+ while ((match = pattern.exec(lineText)) !== null) {
823
+ violations.push({
824
+ filePath,
825
+ line: lineIndex + 1,
826
+ pattern: pattern.source,
827
+ match: match[0],
828
+ severity
829
+ });
830
+ }
831
+ }
832
+ }
833
+ }
834
+ }
835
+ return violations;
836
+ }
837
+ async function detectProjectSourceKind(projectDirectory, log) {
838
+ const projectPackageJsonPath = path6.join(projectDirectory, "package.json");
839
+ if (!await fileSystemExtra6.pathExists(projectPackageJsonPath)) {
840
+ throw new Error(`Cannot determine project source kind \u2014 no package.json found at ${projectPackageJsonPath}.`);
841
+ }
842
+ const parsedManifest = await fileSystemExtra6.readJson(projectPackageJsonPath);
843
+ const mergedDependencies = {
844
+ ...parsedManifest.dependencies ?? {},
845
+ ...parsedManifest.devDependencies ?? {}
846
+ };
847
+ const hasPublishedEngineDependency = "@donut-games/engine" in mergedDependencies;
848
+ const hasAnyInternalWorkspaceDependency = Object.keys(mergedDependencies).some((dependencyName) => dependencyName.startsWith("@donut/"));
849
+ if (hasPublishedEngineDependency && hasAnyInternalWorkspaceDependency) {
850
+ log(chalk3.yellow("\u26A0 Both @donut-games/engine and internal @donut/* workspace dependencies are declared. Preferring published imports for the build entry."));
851
+ return "published";
852
+ }
853
+ if (hasPublishedEngineDependency) {
854
+ return "published";
855
+ }
856
+ if (hasAnyInternalWorkspaceDependency) {
857
+ return "workspace";
858
+ }
859
+ throw new Error(`Cannot determine project source kind \u2014 package.json at ${projectPackageJsonPath} declares neither "@donut-games/engine" nor any internal "@donut/*" workspace dependency. Add "@donut-games/engine" to dependencies to build against the published engine surface.`);
860
+ }
861
+ function buildBuildEntrySource(manifest, projectSourceKind) {
862
+ const usesPixi = manifest.rendererTarget.startsWith("pixi");
863
+ const coreSpecifier = projectSourceKind === "published" ? "@donut-games/engine/core" : "@donut/core";
864
+ const ctrllrSpecifier = projectSourceKind === "published" ? "@donut-games/engine/ctrllr" : "@donut/ctrllr";
865
+ const pixiSpecifier = projectSourceKind === "published" ? "@donut-games/engine/pixi" : "@donut/pixi";
866
+ return `// AUTO-GENERATED by \`donut build\`. Do not edit.
867
+ import { World, SystemType, MotionSystem } from '${coreSpecifier}';
868
+ import { MockCtrllrManager, CtrllrInputSystem } from '${ctrllrSpecifier}';
869
+ ${usesPixi ? `import { PixiRendererAdapter, PixiRenderSystem } from '${pixiSpecifier}';
870
+ ` : ""}import { createDefaultScene } from '../src/scenes/default-scene';
871
+
872
+ const discoveredComponentModules = import.meta.glob('../src/components/**/*.ts', { eager: true });
873
+ const discoveredSystemModules = import.meta.glob('../src/systems/**/*.ts', { eager: true });
874
+ const discoveredSceneModules = import.meta.glob('../src/scenes/**/*.ts', { eager: true });
875
+ void discoveredComponentModules;
876
+ void discoveredSystemModules;
877
+ void discoveredSceneModules;
878
+
879
+ export interface StartGameOptions {
880
+ readonly canvas: HTMLCanvasElement;
881
+ }
882
+
883
+ export async function startGame(options: StartGameOptions): Promise<void> {
884
+ const world = new World();
885
+ world.addSystem(new CtrllrInputSystem(world));
886
+ world.addSystem(new MotionSystem(world));
887
+
888
+ ${usesPixi ? `const rendererAdapter = new PixiRendererAdapter({
889
+ canvas: options.canvas,
890
+ width: options.canvas.width || 800,
891
+ height: options.canvas.height || 600,
892
+ backgroundColor: 0x0a0a12,
893
+ });
894
+ await rendererAdapter.initialize();
895
+ world.addSystem(new PixiRenderSystem(world, rendererAdapter));` : `console.warn('[Donut] three-3d renderer target is not yet implemented');`}
896
+
897
+ const ctrllrManager = new MockCtrllrManager();
898
+
899
+ createDefaultScene(world, ctrllrManager);
900
+
901
+ let previousTimestamp: number | undefined;
902
+ function gameLoop(currentTimestamp: number): void {
903
+ if (previousTimestamp === undefined) previousTimestamp = currentTimestamp;
904
+ const elapsedMilliseconds = currentTimestamp - previousTimestamp;
905
+ previousTimestamp = currentTimestamp;
906
+ world.update(SystemType.Update, elapsedMilliseconds);
907
+ world.update(SystemType.Draw, elapsedMilliseconds);
908
+ requestAnimationFrame(gameLoop);
909
+ }
910
+ requestAnimationFrame(gameLoop);
911
+ }
912
+
913
+ // Auto-start when a canvas with id="game-canvas" is present.
914
+ if (typeof document !== 'undefined') {
915
+ const canvasElement = document.getElementById('game-canvas');
916
+ if (canvasElement instanceof HTMLCanvasElement) {
917
+ startGame({ canvas: canvasElement }).catch((error) => {
918
+ console.error('[Donut] startGame failed', error);
919
+ });
920
+ }
921
+ }
922
+ `;
923
+ }
924
+ async function writeBuildEntryFile(projectDirectory, manifest, projectSourceKind) {
925
+ const donutDirectory = path6.join(projectDirectory, ".donut");
926
+ await fileSystemExtra6.ensureDir(donutDirectory);
927
+ const gitIgnorePath = path6.join(donutDirectory, ".gitignore");
928
+ if (!await fileSystemExtra6.pathExists(gitIgnorePath)) {
929
+ await fileSystemExtra6.writeFile(gitIgnorePath, "*\n", "utf8");
930
+ }
931
+ const entryPath = path6.join(donutDirectory, "build-entry.ts");
932
+ await fileSystemExtra6.writeFile(entryPath, buildBuildEntrySource(manifest, projectSourceKind), "utf8");
933
+ return entryPath;
934
+ }
935
+ async function buildViteBuildConfig(parameters) {
936
+ const { projectDirectory, donutRoot, buildEntryPath, stagingDirectory } = parameters;
937
+ const monorepoRoot = await detectMonorepoRoot(projectDirectory);
938
+ const resolveAlias = monorepoRoot !== void 0 ? await resolveMonorepoDevAliases(monorepoRoot) : [];
939
+ return {
940
+ root: donutRoot,
941
+ configFile: false,
942
+ logLevel: "warn",
943
+ resolve: { alias: resolveAlias },
944
+ build: {
945
+ outDir: stagingDirectory,
946
+ emptyOutDir: true,
947
+ minify: "esbuild",
948
+ lib: {
949
+ entry: buildEntryPath,
950
+ formats: ["es"],
951
+ fileName: () => "game.js"
952
+ },
953
+ rollupOptions: {
954
+ output: {
955
+ inlineDynamicImports: true,
956
+ entryFileNames: "game.js"
957
+ }
958
+ }
959
+ }
960
+ };
961
+ }
962
+ async function copyAssetsAndCollectEntries(projectAssetsDirectory, stagingAssetsDirectory, imageAssetOptimizer, log) {
963
+ let sharpUnavailableWarningEmitted = false;
964
+ if (!await fileSystemExtra6.pathExists(projectAssetsDirectory)) {
965
+ return [];
966
+ }
967
+ await fileSystemExtra6.ensureDir(stagingAssetsDirectory);
968
+ const entries = [];
969
+ async function walk(currentSource, currentDestination, relativePrefix) {
970
+ const directoryEntries = await fileSystemExtra6.readdir(currentSource, { withFileTypes: true });
971
+ for (const directoryEntry of directoryEntries) {
972
+ if (directoryEntry.name === ".gitkeep") {
973
+ continue;
974
+ }
975
+ const sourcePath = path6.join(currentSource, directoryEntry.name);
976
+ const destinationPath = path6.join(currentDestination, directoryEntry.name);
977
+ const relativePath = relativePrefix === "" ? directoryEntry.name : `${relativePrefix}/${directoryEntry.name}`;
978
+ if (directoryEntry.isDirectory()) {
979
+ await fileSystemExtra6.ensureDir(destinationPath);
980
+ await walk(sourcePath, destinationPath, relativePath);
981
+ } else if (directoryEntry.isFile()) {
982
+ const extensionWithDot = path6.extname(directoryEntry.name).toLowerCase();
983
+ const extension = extensionWithDot.startsWith(".") ? extensionWithDot.slice(1) : extensionWithDot;
984
+ const isOptimizableImage = ["png", "jpg", "jpeg", "webp"].includes(extension);
985
+ let wroteOptimizedOutput = false;
986
+ if (isOptimizableImage) {
987
+ const optimizedBuffer = await imageAssetOptimizer(sourcePath, extension);
988
+ if (optimizedBuffer !== null) {
989
+ await fileSystemExtra6.writeFile(destinationPath, optimizedBuffer);
990
+ wroteOptimizedOutput = true;
991
+ } else if (!sharpUnavailableWarningEmitted) {
992
+ log(chalk3.yellow("\u2139 sharp not installed \u2014 assets copied uncompressed"));
993
+ sharpUnavailableWarningEmitted = true;
994
+ }
995
+ }
996
+ if (!wroteOptimizedOutput) {
997
+ await fileSystemExtra6.copy(sourcePath, destinationPath);
998
+ }
999
+ const stat = await fileSystemExtra6.stat(destinationPath);
1000
+ entries.push({
1001
+ name: directoryEntry.name,
1002
+ path: `assets/${relativePath}`,
1003
+ type: extension,
1004
+ sizeBytes: stat.size
1005
+ });
1006
+ }
1007
+ }
1008
+ }
1009
+ await walk(projectAssetsDirectory, stagingAssetsDirectory, "");
1010
+ return entries;
1011
+ }
1012
+ async function collectRuntimeVersions(projectDirectory) {
1013
+ const runtimeVersions = {};
1014
+ const monorepoRoot = await detectMonorepoRoot(projectDirectory);
1015
+ const donutPackageNames = ["core", "math", "pixi", "three", "ctrllr", "player"];
1016
+ if (monorepoRoot !== void 0) {
1017
+ const packagesDirectory = path6.join(monorepoRoot, "packages");
1018
+ for (const packageName of donutPackageNames) {
1019
+ const packageJsonPath = path6.join(packagesDirectory, packageName, "package.json");
1020
+ if (await fileSystemExtra6.pathExists(packageJsonPath)) {
1021
+ const parsed = await fileSystemExtra6.readJson(packageJsonPath);
1022
+ if (typeof parsed.name === "string" && typeof parsed.version === "string") {
1023
+ runtimeVersions[parsed.name] = parsed.version;
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ return runtimeVersions;
1029
+ }
1030
+ async function runViteBuildProgrammatically(viteConfig) {
1031
+ const viteModule = await import("vite");
1032
+ await viteModule.build(viteConfig);
1033
+ }
1034
+ async function runBuild(options) {
1035
+ const log = options.log ?? ((message) => console.log(message));
1036
+ const startTimeMilliseconds = Date.now();
1037
+ const projectDirectory = path6.resolve(options.projectDirectory);
1038
+ const outputDirectory = path6.resolve(options.outputDirectory ?? path6.join(projectDirectory, "dist"));
1039
+ const validationReport = await runValidation({
1040
+ projectDirectory,
1041
+ skipTypeCheck: true
1042
+ });
1043
+ if (validationReport.errors.length > 0) {
1044
+ const formattedErrors = validationReport.errors.map((validationError) => ` ${validationError.filePath}: ${validationError.message}`).join("\n");
1045
+ throw new BuildViolationError(`Validation failed with ${validationReport.errors.length} error(s):
1046
+ ${formattedErrors}`, []);
1047
+ }
1048
+ const scanFindings = await scanUserSourcesForForbiddenPatterns(projectDirectory);
1049
+ const fatalViolations = scanFindings.filter((finding) => finding.severity === "error");
1050
+ const warnings = scanFindings.filter((finding) => finding.severity === "warning");
1051
+ if (fatalViolations.length > 0) {
1052
+ throw new BuildViolationError(`Security scan failed with ${fatalViolations.length} violation(s)`, fatalViolations);
1053
+ }
1054
+ const donutManifest = await readProjectManifest(projectDirectory);
1055
+ log(`Donut build \u2014 project '${donutManifest.name}' (${donutManifest.rendererTarget})`);
1056
+ const projectSourceKind = await detectProjectSourceKind(projectDirectory, log);
1057
+ const buildEntryPath = await writeBuildEntryFile(projectDirectory, donutManifest, projectSourceKind);
1058
+ const stagingDirectory = path6.join(outputDirectory, "bundle-staging");
1059
+ await fileSystemExtra6.ensureDir(outputDirectory);
1060
+ await fileSystemExtra6.remove(stagingDirectory);
1061
+ await fileSystemExtra6.ensureDir(stagingDirectory);
1062
+ const donutRoot = path6.join(projectDirectory, ".donut");
1063
+ const viteConfig = await buildViteBuildConfig({
1064
+ projectDirectory,
1065
+ donutRoot,
1066
+ buildEntryPath,
1067
+ stagingDirectory
1068
+ });
1069
+ const runVite = options.runViteBuild ?? runViteBuildProgrammatically;
1070
+ await runVite(viteConfig);
1071
+ const builtGameJsPath = path6.join(stagingDirectory, "game.js");
1072
+ if (!await fileSystemExtra6.pathExists(builtGameJsPath)) {
1073
+ throw new Error(`Expected Vite build to emit game.js at ${builtGameJsPath} \u2014 file not found.`);
1074
+ }
1075
+ const projectAssetsDirectory = path6.join(projectDirectory, "assets");
1076
+ const stagingAssetsDirectory = path6.join(stagingDirectory, "assets");
1077
+ const imageAssetOptimizer = options.optimizeImageAsset ?? optimizeImageAssetIfPossible;
1078
+ const assetEntries = await copyAssetsAndCollectEntries(projectAssetsDirectory, stagingAssetsDirectory, imageAssetOptimizer, log);
1079
+ const runtimeVersions = await collectRuntimeVersions(projectDirectory);
1080
+ const gameJsContents = await fileSystemExtra6.readFile(builtGameJsPath);
1081
+ const bundleFileName = `${donutManifest.name}-${donutManifest.version ?? "0.0.0"}.donut`;
1082
+ const bundlePath = path6.join(outputDirectory, bundleFileName);
1083
+ const projectManifestVersion = await readProjectManifestVersion(projectDirectory);
1084
+ const partialManifest = {
1085
+ name: donutManifest.name,
1086
+ version: projectManifestVersion,
1087
+ rendererTarget: donutManifest.rendererTarget,
1088
+ entryPoint: "game.js",
1089
+ donutEngineVersion: donutManifest.donutEngineVersion ?? "0.0.0",
1090
+ runtimeVersions,
1091
+ assets: assetEntries,
1092
+ buildTimestamp: (/* @__PURE__ */ new Date()).toISOString()
1093
+ };
1094
+ const zipArchive = new JSZip();
1095
+ zipArchive.file("game.js", gameJsContents);
1096
+ for (const assetEntry of assetEntries) {
1097
+ const absoluteAssetPath = path6.join(stagingDirectory, assetEntry.path);
1098
+ const assetBuffer = await fileSystemExtra6.readFile(absoluteAssetPath);
1099
+ zipArchive.file(assetEntry.path, assetBuffer);
1100
+ }
1101
+ let candidateBundleSize = 0;
1102
+ let paddingLength = 8;
1103
+ let finalBuffer = Buffer.alloc(0);
1104
+ let finalManifest = { ...partialManifest, bundleSizeBytes: 0 };
1105
+ for (let iteration = 0; iteration < 16; iteration += 1) {
1106
+ finalManifest = { ...partialManifest, bundleSizeBytes: candidateBundleSize };
1107
+ const renderedManifest = renderManifestJsonWithPadding(finalManifest, paddingLength);
1108
+ zipArchive.file("manifest.json", renderedManifest, { compression: "STORE" });
1109
+ finalBuffer = await zipArchive.generateAsync({
1110
+ type: "nodebuffer",
1111
+ compression: "DEFLATE"
1112
+ });
1113
+ if (finalBuffer.byteLength === candidateBundleSize) {
1114
+ break;
1115
+ }
1116
+ const previousDigits = String(candidateBundleSize).length;
1117
+ const newDigits = String(finalBuffer.byteLength).length;
1118
+ if (newDigits !== previousDigits) {
1119
+ paddingLength += Math.abs(newDigits - previousDigits) + 2;
1120
+ }
1121
+ candidateBundleSize = finalBuffer.byteLength;
1122
+ }
1123
+ await fileSystemExtra6.writeFile(bundlePath, finalBuffer);
1124
+ const bundleStat = await fileSystemExtra6.stat(bundlePath);
1125
+ if (bundleStat.size !== finalManifest.bundleSizeBytes) {
1126
+ throw new Error(`Internal build error: bundleSizeBytes (${finalManifest.bundleSizeBytes}) does not match on-disk archive size (${bundleStat.size}).`);
1127
+ }
1128
+ const buildTimeMilliseconds = Date.now() - startTimeMilliseconds;
1129
+ return {
1130
+ bundlePath,
1131
+ bundleSizeBytes: bundleStat.size,
1132
+ buildTimeMilliseconds,
1133
+ warnings,
1134
+ violations: [],
1135
+ manifest: finalManifest
1136
+ };
1137
+ }
1138
+ function renderManifestJsonWithPadding(manifest, paddingLength) {
1139
+ const manifestWithPadding = {
1140
+ ...manifest,
1141
+ _sizePadding: " ".repeat(Math.max(0, paddingLength))
1142
+ };
1143
+ return JSON.stringify(manifestWithPadding, null, 2);
1144
+ }
1145
+ async function optimizeImageAssetIfPossible(sourcePath, extension) {
1146
+ let sharpModule;
1147
+ try {
1148
+ sharpModule = await import(
1149
+ /* @vite-ignore */
1150
+ "sharp"
1151
+ );
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ try {
1156
+ const pipeline = sharpModule.default(sourcePath).rotate();
1157
+ let configured;
1158
+ if (extension === "png") {
1159
+ configured = pipeline.png({ compressionLevel: 9 });
1160
+ } else if (extension === "jpg" || extension === "jpeg") {
1161
+ configured = pipeline.jpeg({ quality: 82, mozjpeg: true });
1162
+ } else if (extension === "webp") {
1163
+ configured = pipeline.webp({ quality: 82 });
1164
+ } else {
1165
+ return null;
1166
+ }
1167
+ const output = await configured.toBuffer();
1168
+ return output;
1169
+ } catch {
1170
+ return null;
1171
+ }
1172
+ }
1173
+ async function readProjectManifestVersion(projectDirectory) {
1174
+ const manifestPath = path6.join(projectDirectory, "donut.json");
1175
+ if (!await fileSystemExtra6.pathExists(manifestPath)) {
1176
+ return "0.0.0";
1177
+ }
1178
+ const raw = await fileSystemExtra6.readJson(manifestPath);
1179
+ return typeof raw.version === "string" ? raw.version : "0.0.0";
1180
+ }
1181
+
1182
+ // ../cli/dist/commands/publish.js
1183
+ import path7 from "path";
1184
+ import fileSystemExtra7 from "fs-extra";
1185
+ import chalk4 from "chalk";
1186
+ import JsZip from "jszip";
1187
+ import * as acorn from "acorn";
1188
+ import * as acornWalk from "acorn-walk";
1189
+ var StubBundleUploader = class {
1190
+ logger;
1191
+ constructor(logger = (message) => console.log(message)) {
1192
+ this.logger = logger;
1193
+ }
1194
+ async upload(parameters) {
1195
+ const { manifest, bundleBuffer } = parameters;
1196
+ this.logger(chalk4.yellow(`[StubBundleUploader] Would upload ${manifest.name}@${manifest.version} (${bundleBuffer.byteLength} bytes)`));
1197
+ const gameUrl = `https://donut.games/${encodeURIComponent(manifest.name)}/${encodeURIComponent(manifest.version)}`;
1198
+ return { gameUrl };
1199
+ }
1200
+ };
1201
+ var FORBIDDEN_CALL_IDENTIFIERS = /* @__PURE__ */ new Set([
1202
+ "fetch",
1203
+ "eval",
1204
+ "Function",
1205
+ "XMLHttpRequest",
1206
+ "WebSocket"
1207
+ ]);
1208
+ async function runPublish(options) {
1209
+ const log = options.log ?? ((message) => console.log(message));
1210
+ const uploader = options.uploader ?? new StubBundleUploader(log);
1211
+ const authToken = options.authToken ?? "stub-auth-token";
1212
+ const bundlePath = await resolveBundlePath(options.projectDirectory, options.bundlePath);
1213
+ log(chalk4.cyan(`donut publish: using bundle ${bundlePath}`));
1214
+ const bundleBuffer = await fileSystemExtra7.readFile(bundlePath);
1215
+ const zipArchive = await JsZip.loadAsync(bundleBuffer);
1216
+ const manifest = await readAndValidateManifest(zipArchive);
1217
+ await validateGameEntryExists(zipArchive);
1218
+ await validateAssetsExist(zipArchive, manifest);
1219
+ const gameSource = await readZipTextFile(zipArchive, "game.js");
1220
+ const violations = scanBundleSourceForViolations(gameSource);
1221
+ if (violations.length > 0) {
1222
+ const formatted = violations.map((violation) => ` - [${violation.pattern}] ${violation.description}` + (violation.line !== void 0 ? ` (line ${violation.line})` : "")).join("\n");
1223
+ throw new Error(`Publish aborted: bundled game.js contains forbidden APIs:
1224
+ ${formatted}`);
1225
+ }
1226
+ const warnings = [];
1227
+ const assetCount = manifest.assets.length;
1228
+ const bundleSizeBytes = bundleBuffer.byteLength;
1229
+ log(chalk4.green("\nBundle validated:"));
1230
+ log(` name: ${manifest.name}`);
1231
+ log(` version: ${manifest.version}`);
1232
+ log(` renderer: ${manifest.rendererTarget}`);
1233
+ log(` assets: ${assetCount}`);
1234
+ log(` bundle size: ${bundleSizeBytes} bytes`);
1235
+ log(chalk4.yellow("\nDRY RUN \u2014 no network request made"));
1236
+ const { gameUrl } = await uploader.upload({
1237
+ bundleBuffer,
1238
+ manifest,
1239
+ authToken
1240
+ });
1241
+ log(chalk4.cyan(`
1242
+ Placeholder game URL: ${gameUrl}`));
1243
+ return {
1244
+ success: true,
1245
+ gameUrl,
1246
+ bundleSizeBytes,
1247
+ warnings,
1248
+ violations
1249
+ };
1250
+ }
1251
+ async function resolveBundlePath(projectDirectory, explicitBundlePath) {
1252
+ if (explicitBundlePath !== void 0) {
1253
+ const absoluteBundlePath = path7.isAbsolute(explicitBundlePath) ? explicitBundlePath : path7.join(projectDirectory, explicitBundlePath);
1254
+ if (!await fileSystemExtra7.pathExists(absoluteBundlePath)) {
1255
+ throw new Error(`Bundle not found at ${absoluteBundlePath}`);
1256
+ }
1257
+ return absoluteBundlePath;
1258
+ }
1259
+ const distDirectory = path7.join(projectDirectory, "dist");
1260
+ if (!await fileSystemExtra7.pathExists(distDirectory)) {
1261
+ throw new Error(`No bundle specified and no dist/ directory found at ${distDirectory}. Run "donut build" first.`);
1262
+ }
1263
+ const entries = await fileSystemExtra7.readdir(distDirectory);
1264
+ const donutBundles = entries.filter((entry) => entry.endsWith(".donut"));
1265
+ if (donutBundles.length === 0) {
1266
+ throw new Error(`No .donut bundles found in ${distDirectory}. Run "donut build" first.`);
1267
+ }
1268
+ const statsByPath = await Promise.all(donutBundles.map(async (entry) => {
1269
+ const fullPath = path7.join(distDirectory, entry);
1270
+ const stats = await fileSystemExtra7.stat(fullPath);
1271
+ return { fullPath, modifiedTimeMilliseconds: stats.mtimeMs };
1272
+ }));
1273
+ statsByPath.sort((left, right) => right.modifiedTimeMilliseconds - left.modifiedTimeMilliseconds);
1274
+ return statsByPath[0].fullPath;
1275
+ }
1276
+ async function readAndValidateManifest(zipArchive) {
1277
+ const manifestFile = zipArchive.file("manifest.json");
1278
+ if (manifestFile === null) {
1279
+ throw new Error("Bundle is missing required /manifest.json");
1280
+ }
1281
+ const manifestText = await manifestFile.async("string");
1282
+ let parsed;
1283
+ try {
1284
+ parsed = JSON.parse(manifestText);
1285
+ } catch (parseError) {
1286
+ throw new Error(`Bundle manifest.json is not valid JSON: ${parseError.message}`);
1287
+ }
1288
+ if (typeof parsed !== "object" || parsed === null) {
1289
+ throw new Error("Bundle manifest.json is not an object");
1290
+ }
1291
+ const candidate = parsed;
1292
+ const requiredStringFields = [
1293
+ "name",
1294
+ "version",
1295
+ "rendererTarget",
1296
+ "entryPoint",
1297
+ "donutEngineVersion",
1298
+ "buildTimestamp"
1299
+ ];
1300
+ for (const fieldName of requiredStringFields) {
1301
+ if (typeof candidate[fieldName] !== "string" || candidate[fieldName].length === 0) {
1302
+ throw new Error(`Bundle manifest.json is missing required string field "${fieldName}"`);
1303
+ }
1304
+ }
1305
+ const rendererTarget = candidate.rendererTarget;
1306
+ if (rendererTarget !== "pixi-2d" && rendererTarget !== "three-3d") {
1307
+ throw new Error(`Bundle manifest.json has invalid rendererTarget "${String(rendererTarget)}"`);
1308
+ }
1309
+ if (candidate.entryPoint !== "game.js") {
1310
+ throw new Error(`Bundle manifest.json entryPoint must be "game.js" (got "${String(candidate.entryPoint)}")`);
1311
+ }
1312
+ if (typeof candidate.bundleSizeBytes !== "number") {
1313
+ throw new Error('Bundle manifest.json is missing required number field "bundleSizeBytes"');
1314
+ }
1315
+ if (typeof candidate.runtimeVersions !== "object" || candidate.runtimeVersions === null) {
1316
+ throw new Error('Bundle manifest.json is missing required object field "runtimeVersions"');
1317
+ }
1318
+ if (!Array.isArray(candidate.assets)) {
1319
+ throw new Error('Bundle manifest.json is missing required array field "assets"');
1320
+ }
1321
+ return parsed;
1322
+ }
1323
+ async function validateGameEntryExists(zipArchive) {
1324
+ const entry = zipArchive.file("game.js");
1325
+ if (entry === null) {
1326
+ throw new Error("Bundle is missing required /game.js");
1327
+ }
1328
+ }
1329
+ async function validateAssetsExist(zipArchive, manifest) {
1330
+ for (const asset of manifest.assets) {
1331
+ const entry = zipArchive.file(asset.path);
1332
+ if (entry === null) {
1333
+ throw new Error(`Bundle manifest declares asset "${asset.path}" but it is missing from the archive`);
1334
+ }
1335
+ }
1336
+ }
1337
+ async function readZipTextFile(zipArchive, pathInZip) {
1338
+ const entry = zipArchive.file(pathInZip);
1339
+ if (entry === null) {
1340
+ throw new Error(`Bundle is missing required file ${pathInZip}`);
1341
+ }
1342
+ return entry.async("string");
1343
+ }
1344
+ function scanBundleSourceForViolations(sourceText) {
1345
+ const violations = [];
1346
+ let ast;
1347
+ try {
1348
+ ast = acorn.parse(sourceText, {
1349
+ ecmaVersion: "latest",
1350
+ sourceType: "module",
1351
+ locations: true,
1352
+ allowHashBang: true
1353
+ });
1354
+ } catch (parseError) {
1355
+ violations.push({
1356
+ pattern: "parse-error",
1357
+ description: `Bundle game.js failed to parse: ${parseError.message}`
1358
+ });
1359
+ return violations;
1360
+ }
1361
+ acornWalk.simple(ast, {
1362
+ CallExpression(rawNode) {
1363
+ const node = rawNode;
1364
+ const callee = node.callee;
1365
+ if (callee.type === "Identifier") {
1366
+ const identifierNode = callee;
1367
+ if (FORBIDDEN_CALL_IDENTIFIERS.has(identifierNode.name)) {
1368
+ violations.push({
1369
+ pattern: identifierNode.name,
1370
+ description: `Forbidden call to ${identifierNode.name}(...)`,
1371
+ line: node.loc?.start.line,
1372
+ column: node.loc?.start.column
1373
+ });
1374
+ }
1375
+ }
1376
+ },
1377
+ NewExpression(rawNode) {
1378
+ const node = rawNode;
1379
+ const callee = node.callee;
1380
+ if (callee.type === "Identifier") {
1381
+ const identifierNode = callee;
1382
+ if (FORBIDDEN_CALL_IDENTIFIERS.has(identifierNode.name)) {
1383
+ violations.push({
1384
+ pattern: identifierNode.name,
1385
+ description: `Forbidden construction of ${identifierNode.name}`,
1386
+ line: node.loc?.start.line,
1387
+ column: node.loc?.start.column
1388
+ });
1389
+ }
1390
+ }
1391
+ },
1392
+ AssignmentExpression(rawNode) {
1393
+ const node = rawNode;
1394
+ if (node.left.type === "MemberExpression") {
1395
+ const memberNode = node.left;
1396
+ if (memberNode.property.type === "Identifier" && memberNode.property.name === "innerHTML") {
1397
+ violations.push({
1398
+ pattern: "innerHTML",
1399
+ description: "Forbidden assignment to .innerHTML",
1400
+ line: node.loc?.start.line,
1401
+ column: node.loc?.start.column
1402
+ });
1403
+ }
1404
+ }
1405
+ },
1406
+ ImportExpression(rawNode) {
1407
+ const node = rawNode;
1408
+ violations.push({
1409
+ pattern: "dynamic-import",
1410
+ description: "Forbidden dynamic import() expression",
1411
+ line: node.loc?.start.line,
1412
+ column: node.loc?.start.column
1413
+ });
1414
+ }
1415
+ });
1416
+ return violations;
1417
+ }
1418
+
1419
+ // ../cli/dist/commands/review.js
1420
+ import path8 from "path";
1421
+ import fileSystemExtra8 from "fs-extra";
1422
+ import chalk5 from "chalk";
1423
+ import JsZip2 from "jszip";
1424
+ var REVIEW_PROMPT_TEXT = 'You are reviewing code for the Donut game engine. Games must only use the Donut framework API. Check each file for: direct DOM access (document, window, innerHTML), network requests (fetch, XMLHttpRequest, WebSocket), code execution (eval, new Function, dynamic import()), storage (localStorage, sessionStorage, indexedDB), or any attempt to escape the game runtime. Respond with a JSON array of findings: [{ "severity": "violation"|"warning"|"pass", "file": "path", "description": "what and why" }]. Return [{"severity":"pass","description":"clean"}] if nothing is wrong.';
1425
+ var StubClaudeReviewClient = class {
1426
+ logger;
1427
+ constructor(logger = (message) => console.log(message)) {
1428
+ this.logger = logger;
1429
+ }
1430
+ async review(_request) {
1431
+ this.logger(chalk5.yellow("AI review skipped (no ANTHROPIC_API_KEY set)"));
1432
+ const findings = [
1433
+ { severity: "pass", description: "clean" }
1434
+ ];
1435
+ return { findings, rawResponse: JSON.stringify(findings) };
1436
+ }
1437
+ };
1438
+ function createDefaultClaudeReviewClient(logger = (message) => console.log(message)) {
1439
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1440
+ if (apiKey === void 0 || apiKey.length === 0) {
1441
+ return new StubClaudeReviewClient(logger);
1442
+ }
1443
+ return new AnthropicClaudeReviewClient(apiKey);
1444
+ }
1445
+ var AnthropicClaudeReviewClient = class {
1446
+ apiKey;
1447
+ constructor(apiKey) {
1448
+ this.apiKey = apiKey;
1449
+ }
1450
+ async review(request) {
1451
+ const { default: AnthropicClient } = await import("@anthropic-ai/sdk");
1452
+ const client = new AnthropicClient({ apiKey: this.apiKey });
1453
+ const message = await client.messages.create({
1454
+ model: "claude-opus-4-6",
1455
+ max_tokens: 4096,
1456
+ system: request.prompt,
1457
+ messages: [
1458
+ {
1459
+ role: "user",
1460
+ content: request.sourceText
1461
+ }
1462
+ ]
1463
+ });
1464
+ const firstTextBlock = message.content.find((block) => block.type === "text");
1465
+ const rawResponse = firstTextBlock?.text ?? "";
1466
+ const findings = parseReviewFindingsFromText(rawResponse);
1467
+ return { findings, rawResponse };
1468
+ }
1469
+ };
1470
+ function parseReviewFindingsFromText(text) {
1471
+ const firstArrayStart = text.indexOf("[");
1472
+ const lastArrayEnd = text.lastIndexOf("]");
1473
+ if (firstArrayStart === -1 || lastArrayEnd === -1 || lastArrayEnd <= firstArrayStart) {
1474
+ return [
1475
+ {
1476
+ severity: "warning",
1477
+ description: `Could not parse review findings as JSON array: ${text.slice(0, 200)}`
1478
+ }
1479
+ ];
1480
+ }
1481
+ const candidate = text.slice(firstArrayStart, lastArrayEnd + 1);
1482
+ try {
1483
+ const parsed = JSON.parse(candidate);
1484
+ if (!Array.isArray(parsed)) {
1485
+ throw new Error("Expected JSON array");
1486
+ }
1487
+ return parsed.map((entry) => {
1488
+ const record = entry;
1489
+ const severity = record.severity;
1490
+ const normalizedSeverity = severity === "violation" || severity === "warning" || severity === "pass" ? severity : "warning";
1491
+ return {
1492
+ severity: normalizedSeverity,
1493
+ file: typeof record.file === "string" ? record.file : void 0,
1494
+ description: typeof record.description === "string" ? record.description : "no description"
1495
+ };
1496
+ });
1497
+ } catch (parseError) {
1498
+ return [
1499
+ {
1500
+ severity: "warning",
1501
+ description: `Could not parse review findings: ${parseError.message}`
1502
+ }
1503
+ ];
1504
+ }
1505
+ }
1506
+ async function runReview(options) {
1507
+ const log = options.log ?? ((message) => console.log(message));
1508
+ const claudeClient = options.claudeClient ?? createDefaultClaudeReviewClient(log);
1509
+ const sources = await collectReviewSources(options.projectDirectory, options.bundlePath);
1510
+ if (sources.length === 0) {
1511
+ log(chalk5.yellow("donut review: no source files found to review"));
1512
+ return {
1513
+ overallPass: true,
1514
+ findings: [
1515
+ { severity: "pass", description: "No reviewable sources found" }
1516
+ ]
1517
+ };
1518
+ }
1519
+ const sourceText = sources.map((file) => `// === FILE: ${file.displayPath} ===
1520
+ ${file.contents}
1521
+ // === END FILE: ${file.displayPath} ===`).join("\n\n");
1522
+ log(chalk5.cyan(`donut review: sending ${sources.length} file(s) for AI review`));
1523
+ const response = await claudeClient.review({
1524
+ sourceText,
1525
+ prompt: REVIEW_PROMPT_TEXT
1526
+ });
1527
+ const hasViolation = response.findings.some((finding) => finding.severity === "violation");
1528
+ for (const finding of response.findings) {
1529
+ const prefix = `[${finding.severity.toUpperCase()}]${finding.file !== void 0 ? ` ${finding.file}` : ""}`;
1530
+ const line = `${prefix}: ${finding.description}`;
1531
+ if (finding.severity === "violation") {
1532
+ log(chalk5.red(line));
1533
+ } else if (finding.severity === "warning") {
1534
+ log(chalk5.yellow(line));
1535
+ } else {
1536
+ log(chalk5.green(line));
1537
+ }
1538
+ }
1539
+ return {
1540
+ overallPass: hasViolation === false,
1541
+ findings: response.findings
1542
+ };
1543
+ }
1544
+ async function collectReviewSources(projectDirectory, bundlePath) {
1545
+ if (bundlePath !== void 0) {
1546
+ const collected = await collectReviewSourcesFromBundle(projectDirectory, bundlePath);
1547
+ if (collected.length > 0) {
1548
+ return collected;
1549
+ }
1550
+ }
1551
+ return collectReviewSourcesFromDisk(projectDirectory);
1552
+ }
1553
+ async function collectReviewSourcesFromBundle(projectDirectory, bundlePath) {
1554
+ const absoluteBundlePath = path8.isAbsolute(bundlePath) ? bundlePath : path8.join(projectDirectory, bundlePath);
1555
+ if (!await fileSystemExtra8.pathExists(absoluteBundlePath)) {
1556
+ throw new Error(`Bundle not found at ${absoluteBundlePath}`);
1557
+ }
1558
+ const buffer = await fileSystemExtra8.readFile(absoluteBundlePath);
1559
+ const zipArchive = await JsZip2.loadAsync(buffer);
1560
+ const sources = [];
1561
+ const entries = [];
1562
+ zipArchive.forEach((relativePath, file) => {
1563
+ if (relativePath.startsWith("src/") && (relativePath.endsWith(".ts") || relativePath.endsWith(".js")) && file.dir === false) {
1564
+ entries.push({ path: relativePath, file });
1565
+ }
1566
+ });
1567
+ for (const entry of entries) {
1568
+ const contents = await entry.file.async("string");
1569
+ sources.push({ displayPath: entry.path, contents });
1570
+ }
1571
+ return sources;
1572
+ }
1573
+ async function collectReviewSourcesFromDisk(projectDirectory) {
1574
+ const componentsDirectory = path8.join(projectDirectory, "src", "components");
1575
+ const systemsDirectory = path8.join(projectDirectory, "src", "systems");
1576
+ const sources = [];
1577
+ for (const directoryPath of [componentsDirectory, systemsDirectory]) {
1578
+ if (!await fileSystemExtra8.pathExists(directoryPath)) {
1579
+ continue;
1580
+ }
1581
+ const typeScriptFiles = await collectTypeScriptFilesRecursively2(directoryPath);
1582
+ for (const filePath of typeScriptFiles) {
1583
+ const contents = await fileSystemExtra8.readFile(filePath, "utf8");
1584
+ sources.push({
1585
+ displayPath: path8.relative(projectDirectory, filePath),
1586
+ contents
1587
+ });
1588
+ }
1589
+ }
1590
+ return sources;
1591
+ }
1592
+ async function collectTypeScriptFilesRecursively2(directoryPath) {
1593
+ const collected = [];
1594
+ const entries = await fileSystemExtra8.readdir(directoryPath, {
1595
+ withFileTypes: true
1596
+ });
1597
+ for (const entry of entries) {
1598
+ const entryPath = path8.join(directoryPath, entry.name);
1599
+ if (entry.isDirectory()) {
1600
+ collected.push(...await collectTypeScriptFilesRecursively2(entryPath));
1601
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
1602
+ collected.push(entryPath);
1603
+ }
1604
+ }
1605
+ return collected;
1606
+ }
1607
+
1608
+ // ../cli/dist/index.js
1609
+ var program = new Command();
1610
+ program.name("donut").description("Donut game engine command-line tooling").version("0.1.0");
1611
+ program.command("init").description("Scaffold a new Donut game project").argument("<project-name>", "name of the new project directory").option("--2d", "use the 2D (Pixi) renderer target", false).option("--3d", "use the 3D (Three) renderer target", false).option("--skip-install", "skip running pnpm/npm install after scaffolding", false).action(async (projectName, options) => {
1612
+ const useThreeDimensional = options["3d"] === true;
1613
+ const rendererTarget = useThreeDimensional ? "three-3d" : "pixi-2d";
1614
+ try {
1615
+ await initializeProject({
1616
+ projectName,
1617
+ rendererTarget,
1618
+ targetDirectory: projectName,
1619
+ skipInstall: options.skipInstall === true
1620
+ });
1621
+ } catch (error) {
1622
+ console.error(chalk6.red(error.message));
1623
+ process.exit(1);
1624
+ }
1625
+ });
1626
+ program.command("dev").description("Run the project in a local development server").option("--port <port>", "port for the dev server", "5173").option("--no-open", "do not automatically open a browser window").action(async (options) => {
1627
+ const parsedPort = Number.parseInt(options.port, 10);
1628
+ try {
1629
+ await runDevServer({
1630
+ projectDirectory: process.cwd(),
1631
+ port: Number.isFinite(parsedPort) ? parsedPort : 5173,
1632
+ open: options.open
1633
+ });
1634
+ } catch (error) {
1635
+ console.error(chalk6.red(error.message));
1636
+ process.exit(1);
1637
+ }
1638
+ });
1639
+ program.command("validate").description("Validate project structure, types, and asset references").action(async () => {
1640
+ try {
1641
+ const report = await runValidation({ projectDirectory: process.cwd() });
1642
+ const formatFinding = (finding) => {
1643
+ const locationParts = [finding.filePath];
1644
+ if (finding.line !== void 0) {
1645
+ locationParts.push(String(finding.line));
1646
+ if (finding.column !== void 0) {
1647
+ locationParts.push(String(finding.column));
1648
+ }
1649
+ }
1650
+ return `${locationParts.join(":")}: ${finding.code}: ${finding.message}`;
1651
+ };
1652
+ for (const warning of report.warnings) {
1653
+ console.warn(chalk6.yellow(formatFinding(warning)));
1654
+ }
1655
+ for (const error of report.errors) {
1656
+ console.error(chalk6.red(formatFinding(error)));
1657
+ }
1658
+ if (report.errors.length === 0 && report.warnings.length === 0) {
1659
+ console.log(chalk6.green("donut validate: no problems found"));
1660
+ }
1661
+ process.exit(report.errors.length === 0 ? 0 : 1);
1662
+ } catch (error) {
1663
+ console.error(chalk6.red(error.message));
1664
+ process.exit(1);
1665
+ }
1666
+ });
1667
+ program.command("build").description("Build a distributable .donut bundle for the project").option("--output <dir>", "output directory for the bundle", "dist").action(async (options) => {
1668
+ const projectDirectory = process.cwd();
1669
+ const outputDirectory = path9.isAbsolute(options.output) ? options.output : path9.join(projectDirectory, options.output);
1670
+ try {
1671
+ const report = await runBuild({ projectDirectory, outputDirectory });
1672
+ for (const warning of report.warnings) {
1673
+ console.warn(chalk6.yellow(`${warning.filePath}:${warning.line}: warning: matched /${warning.pattern}/ ("${warning.match}")`));
1674
+ }
1675
+ console.log(chalk6.green(`donut build: wrote ${report.bundlePath} (${report.bundleSizeBytes} bytes) in ${report.buildTimeMilliseconds}ms`));
1676
+ } catch (error) {
1677
+ if (error instanceof BuildViolationError) {
1678
+ console.error(chalk6.red(error.message));
1679
+ for (const violation of error.violations) {
1680
+ console.error(chalk6.red(` ${violation.filePath}:${violation.line}: matched /${violation.pattern}/ ("${violation.match}")`));
1681
+ }
1682
+ } else {
1683
+ console.error(chalk6.red(error.message));
1684
+ }
1685
+ process.exit(1);
1686
+ }
1687
+ });
1688
+ program.command("publish").description("Publish a built .donut bundle (dry-run by default)").option("--bundle <path>", "explicit path to a .donut bundle to publish").option("--auth-token <token>", "bearer token for the upload endpoint").action(async (options) => {
1689
+ try {
1690
+ const report = await runPublish({
1691
+ projectDirectory: process.cwd(),
1692
+ bundlePath: options.bundle,
1693
+ authToken: options.authToken
1694
+ });
1695
+ if (report.success) {
1696
+ console.log(chalk6.green(`donut publish: ${report.gameUrl}`));
1697
+ }
1698
+ } catch (error) {
1699
+ console.error(chalk6.red(error.message));
1700
+ process.exit(1);
1701
+ }
1702
+ });
1703
+ program.command("review").description("Run AI-assisted security/style review on the project sources").option("--bundle <path>", "review the sources packaged into a built bundle").action(async (options) => {
1704
+ try {
1705
+ const report = await runReview({
1706
+ projectDirectory: process.cwd(),
1707
+ bundlePath: options.bundle
1708
+ });
1709
+ if (report.overallPass === false) {
1710
+ process.exit(1);
1711
+ }
1712
+ } catch (error) {
1713
+ console.error(chalk6.red(error.message));
1714
+ process.exit(1);
1715
+ }
1716
+ });
1717
+ var isRunningAsMain = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/donut") === true || process.argv[1]?.endsWith("\\donut") === true || process.argv[1]?.endsWith("cli/src/index.ts") === true || process.argv[1]?.endsWith("cli/dist/index.js") === true;
1718
+ if (isRunningAsMain) {
1719
+ program.parseAsync(process.argv);
1720
+ }
1721
+
1722
+ // src/donut.ts
1723
+ await program.parseAsync(process.argv);
1724
+ //# sourceMappingURL=donut.js.map