@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.
@@ -0,0 +1,2654 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../mcp/dist/index.js
4
+ import path10 from "path";
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
8
+
9
+ // ../mcp/dist/tools/component-system/components.js
10
+ import path2 from "path";
11
+ import fileSystemExtra from "fs-extra";
12
+ import typescript from "typescript";
13
+ import { z } from "zod";
14
+ import { zodToJsonSchema } from "zod-to-json-schema";
15
+
16
+ // ../mcp/dist/tools/component-system/naming.js
17
+ function toPascalCase(rawName) {
18
+ const trimmed = rawName.trim();
19
+ if (trimmed.length === 0) {
20
+ throw new Error("Name must not be empty");
21
+ }
22
+ const parts = trimmed.replace(/[_\-\s]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ").filter((part) => part.length > 0);
23
+ return parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
24
+ }
25
+ function toKebabCase(rawName) {
26
+ const pascal = toPascalCase(rawName);
27
+ return pascal.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
28
+ }
29
+ function stripSuffix(pascalName, suffix) {
30
+ return pascalName.endsWith(suffix) ? pascalName.slice(0, pascalName.length - suffix.length) : pascalName;
31
+ }
32
+
33
+ // ../mcp/dist/tools/component-system/path-guard.js
34
+ import path from "path";
35
+ function resolveWithinProject(projectDirectory, relativePath) {
36
+ const absoluteProjectDirectory = path.resolve(projectDirectory);
37
+ const absoluteResolved = path.resolve(absoluteProjectDirectory, relativePath);
38
+ const projectDirectoryWithSeparator = absoluteProjectDirectory.endsWith(path.sep) ? absoluteProjectDirectory : absoluteProjectDirectory + path.sep;
39
+ if (absoluteResolved !== absoluteProjectDirectory && !absoluteResolved.startsWith(projectDirectoryWithSeparator)) {
40
+ throw new Error(`Path "${relativePath}" escapes project directory "${absoluteProjectDirectory}"`);
41
+ }
42
+ return absoluteResolved;
43
+ }
44
+
45
+ // ../mcp/dist/tools/component-system/components.js
46
+ var stateFieldTypeSchema = z.enum(["number", "string", "boolean", "Vector2", "Vector3"]);
47
+ var stateFieldSchema = z.object({
48
+ name: z.string().min(1),
49
+ type: stateFieldTypeSchema,
50
+ defaultValue: z.union([z.string(), z.number(), z.boolean()]).optional()
51
+ });
52
+ var lifecycleHookSchema = z.enum(["onAdd", "onRemove"]);
53
+ var createComponentInputSchema = z.object({
54
+ name: z.string().min(1),
55
+ stateFields: z.array(stateFieldSchema).default([]),
56
+ lifecycleHooks: z.array(lifecycleHookSchema).optional()
57
+ });
58
+ var readComponentInputSchema = z.object({ name: z.string().min(1) });
59
+ var updateComponentInputSchema = z.object({
60
+ name: z.string().min(1),
61
+ changes: z.object({
62
+ addFields: z.array(stateFieldSchema).optional(),
63
+ removeFields: z.array(z.string()).optional(),
64
+ renameFields: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
65
+ addHook: lifecycleHookSchema.optional(),
66
+ removeHook: lifecycleHookSchema.optional()
67
+ }).default({})
68
+ });
69
+ var deleteComponentInputSchema = z.object({ name: z.string().min(1) });
70
+ var listComponentsInputSchema = z.object({}).default({});
71
+ function renderDefaultLiteral(field) {
72
+ if (field.defaultValue !== void 0) {
73
+ if (field.type === "string") {
74
+ return JSON.stringify(String(field.defaultValue));
75
+ }
76
+ return String(field.defaultValue);
77
+ }
78
+ switch (field.type) {
79
+ case "number":
80
+ return "0";
81
+ case "string":
82
+ return "''";
83
+ case "boolean":
84
+ return "false";
85
+ case "Vector2":
86
+ return "new Vector2(0, 0)";
87
+ case "Vector3":
88
+ return "new Vector3(0, 0, 0)";
89
+ }
90
+ }
91
+ function renderComponentSource(className, stateFields, lifecycleHooks) {
92
+ const needsVector2 = stateFields.some((field) => field.type === "Vector2");
93
+ const needsVector3 = stateFields.some((field) => field.type === "Vector3");
94
+ const mathImports = [];
95
+ if (needsVector2) {
96
+ mathImports.push("Vector2");
97
+ }
98
+ if (needsVector3) {
99
+ mathImports.push("Vector3");
100
+ }
101
+ const lines = [];
102
+ lines.push(`import { Component, type Entity } from '@donut/core';`);
103
+ if (mathImports.length > 0) {
104
+ lines.push(`import { ${mathImports.join(", ")} } from '@donut/math';`);
105
+ }
106
+ lines.push("");
107
+ lines.push("/**");
108
+ lines.push(` * ${className} \u2014 generated by the Donut MCP \`create_component\` tool.`);
109
+ lines.push(" *");
110
+ lines.push(" * Holds authored state for entities that participate in the related system.");
111
+ lines.push(" */");
112
+ lines.push(`export class ${className} extends Component {`);
113
+ for (const field of stateFields) {
114
+ lines.push(` /** Authored state field. */`);
115
+ lines.push(` ${field.name}: ${field.type} = ${renderDefaultLiteral(field)};`);
116
+ }
117
+ if (lifecycleHooks.includes("onAdd")) {
118
+ lines.push("");
119
+ lines.push(" /** Lifecycle hook fired when this component is attached to an entity. */");
120
+ lines.push(" onAdd(owner: Entity): void {");
121
+ lines.push(" void owner;");
122
+ lines.push(" }");
123
+ }
124
+ if (lifecycleHooks.includes("onRemove")) {
125
+ lines.push("");
126
+ lines.push(" /** Lifecycle hook fired when this component is detached from an entity. */");
127
+ lines.push(" onRemove(previousOwner: Entity): void {");
128
+ lines.push(" void previousOwner;");
129
+ lines.push(" }");
130
+ }
131
+ lines.push("");
132
+ lines.push(" /** Deep-copy this component for entity duplication. */");
133
+ lines.push(` clone(): ${className} {`);
134
+ lines.push(` const copy = new ${className}();`);
135
+ for (const field of stateFields) {
136
+ if (field.type === "Vector2" || field.type === "Vector3") {
137
+ lines.push(` copy.${field.name} = this.${field.name}.clone();`);
138
+ } else {
139
+ lines.push(` copy.${field.name} = this.${field.name};`);
140
+ }
141
+ }
142
+ lines.push(" return copy;");
143
+ lines.push(" }");
144
+ lines.push("");
145
+ lines.push(" /** Convert this component to a plain-object snapshot. */");
146
+ lines.push(" serialize(): Record<string, unknown> {");
147
+ lines.push(" return {");
148
+ for (const field of stateFields) {
149
+ if (field.type === "Vector2" || field.type === "Vector3") {
150
+ lines.push(` ${field.name}: this.${field.name}.serialize(),`);
151
+ } else {
152
+ lines.push(` ${field.name}: this.${field.name},`);
153
+ }
154
+ }
155
+ lines.push(" };");
156
+ lines.push(" }");
157
+ lines.push("");
158
+ lines.push(" /** Restore this component's state from a plain-object snapshot. */");
159
+ lines.push(" deserialize(data: Record<string, unknown>): void {");
160
+ for (const field of stateFields) {
161
+ lines.push(` if (data['${field.name}'] !== undefined) {`);
162
+ if (field.type === "Vector2") {
163
+ lines.push(` this.${field.name} = Vector2.deserialize(data['${field.name}'] as Record<string, unknown>);`);
164
+ } else if (field.type === "Vector3") {
165
+ lines.push(` this.${field.name} = Vector3.deserialize(data['${field.name}'] as Record<string, unknown>);`);
166
+ } else {
167
+ lines.push(` this.${field.name} = data['${field.name}'] as ${field.type};`);
168
+ }
169
+ lines.push(" }");
170
+ }
171
+ lines.push(" }");
172
+ lines.push("}");
173
+ lines.push("");
174
+ return lines.join("\n");
175
+ }
176
+ function parseComponentSource(sourceText, filePath) {
177
+ const sourceFile = typescript.createSourceFile(filePath, sourceText, typescript.ScriptTarget.ES2022, true);
178
+ let className;
179
+ const stateFields = [];
180
+ const dependencies = [];
181
+ typescript.forEachChild(sourceFile, (node) => {
182
+ if (!typescript.isClassDeclaration(node) || node.name === void 0) {
183
+ return;
184
+ }
185
+ className = node.name.text;
186
+ for (const member of node.members) {
187
+ if (typescript.isPropertyDeclaration(member) && typescript.isIdentifier(member.name)) {
188
+ const propertyName = member.name.text;
189
+ if (propertyName === "dependencies" && member.initializer !== void 0) {
190
+ if (typescript.isArrayLiteralExpression(member.initializer)) {
191
+ for (const element of member.initializer.elements) {
192
+ if (typescript.isIdentifier(element)) {
193
+ dependencies.push(element.text);
194
+ }
195
+ }
196
+ }
197
+ continue;
198
+ }
199
+ if (propertyName === "owner") {
200
+ continue;
201
+ }
202
+ const typeText = member.type !== void 0 ? member.type.getText(sourceFile) : "unknown";
203
+ const defaultText = member.initializer !== void 0 ? member.initializer.getText(sourceFile) : void 0;
204
+ stateFields.push({
205
+ name: propertyName,
206
+ type: typeText,
207
+ ...defaultText !== void 0 ? { defaultText } : {}
208
+ });
209
+ }
210
+ }
211
+ });
212
+ if (className === void 0) {
213
+ throw new Error(`No class declaration found in ${filePath}`);
214
+ }
215
+ return { className, stateFields, dependencies };
216
+ }
217
+ function resolveComponentPaths(projectDirectory, rawName) {
218
+ const pascal = toPascalCase(rawName);
219
+ const baseName = stripSuffix(pascal, "Component");
220
+ const className = `${baseName}Component`;
221
+ const kebabName = toKebabCase(baseName);
222
+ const relativePath = path2.posix.join("src", "components", `${kebabName}-component.ts`);
223
+ const absoluteFilePath = resolveWithinProject(projectDirectory, relativePath);
224
+ return { absoluteFilePath, className, kebabName };
225
+ }
226
+ function formatToolResult(payload) {
227
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
228
+ }
229
+ async function collectTypeScriptFiles(directory) {
230
+ if (!await fileSystemExtra.pathExists(directory)) {
231
+ return [];
232
+ }
233
+ const collected = [];
234
+ const walk = async (current) => {
235
+ const entries = await fileSystemExtra.readdir(current, { withFileTypes: true });
236
+ for (const entry of entries) {
237
+ const entryAbsolute = path2.join(current, entry.name);
238
+ if (entry.isDirectory()) {
239
+ await walk(entryAbsolute);
240
+ continue;
241
+ }
242
+ if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
243
+ collected.push(entryAbsolute);
244
+ }
245
+ }
246
+ };
247
+ await walk(directory);
248
+ return collected;
249
+ }
250
+ function registerComponentTools(context) {
251
+ const projectDirectory = context.projectDirectory;
252
+ const createComponentTool = {
253
+ name: "create_component",
254
+ description: "Scaffold a new Donut component file under src/components/. State fields generate typed properties with sane defaults plus clone/serialize/deserialize. Fails if the file already exists.",
255
+ inputSchema: zodToJsonSchema(createComponentInputSchema),
256
+ handler: async (input) => {
257
+ const parsed = createComponentInputSchema.parse(input);
258
+ const { absoluteFilePath, className } = resolveComponentPaths(projectDirectory, parsed.name);
259
+ if (await fileSystemExtra.pathExists(absoluteFilePath)) {
260
+ throw new Error(`Component file already exists: ${absoluteFilePath}`);
261
+ }
262
+ const lifecycleHooks = parsed.lifecycleHooks ?? [];
263
+ const source = renderComponentSource(className, parsed.stateFields, lifecycleHooks);
264
+ await fileSystemExtra.ensureDir(path2.dirname(absoluteFilePath));
265
+ await fileSystemExtra.writeFile(absoluteFilePath, source, "utf8");
266
+ return formatToolResult({ className, filePath: absoluteFilePath });
267
+ }
268
+ };
269
+ const readComponentTool = {
270
+ name: "read_component",
271
+ description: "Return the source text of an existing component file.",
272
+ inputSchema: zodToJsonSchema(readComponentInputSchema),
273
+ handler: async (input) => {
274
+ const parsed = readComponentInputSchema.parse(input);
275
+ const { absoluteFilePath, className } = resolveComponentPaths(projectDirectory, parsed.name);
276
+ if (!await fileSystemExtra.pathExists(absoluteFilePath)) {
277
+ throw new Error(`Component file not found: ${absoluteFilePath}`);
278
+ }
279
+ const source = await fileSystemExtra.readFile(absoluteFilePath, "utf8");
280
+ return formatToolResult({ className, filePath: absoluteFilePath, source });
281
+ }
282
+ };
283
+ const updateComponentTool = {
284
+ name: "update_component",
285
+ description: "Apply structural changes to an existing component: add/remove/rename state fields and add/remove onAdd/onRemove lifecycle hooks. File is re-rendered from the merged field list to keep clone/serialize/deserialize in sync.",
286
+ inputSchema: zodToJsonSchema(updateComponentInputSchema),
287
+ handler: async (input) => {
288
+ const parsed = updateComponentInputSchema.parse(input);
289
+ const { absoluteFilePath, className } = resolveComponentPaths(projectDirectory, parsed.name);
290
+ if (!await fileSystemExtra.pathExists(absoluteFilePath)) {
291
+ throw new Error(`Component file not found: ${absoluteFilePath}`);
292
+ }
293
+ const existingSource = await fileSystemExtra.readFile(absoluteFilePath, "utf8");
294
+ const existing = parseComponentSource(existingSource, absoluteFilePath);
295
+ const supportedTypes = ["number", "string", "boolean", "Vector2", "Vector3"];
296
+ const isSupportedType = (value) => supportedTypes.includes(value);
297
+ const currentFields = existing.stateFields.filter((field) => isSupportedType(field.type)).map((field) => ({
298
+ name: field.name,
299
+ type: field.type
300
+ }));
301
+ const changes = parsed.changes;
302
+ const removeFields = new Set(changes.removeFields ?? []);
303
+ const renameMap = new Map((changes.renameFields ?? []).map((rename) => [rename.from, rename.to]));
304
+ const rebuiltFields = [];
305
+ for (const field of currentFields) {
306
+ if (removeFields.has(field.name)) {
307
+ continue;
308
+ }
309
+ const renamedTo = renameMap.get(field.name);
310
+ rebuiltFields.push({
311
+ name: renamedTo ?? field.name,
312
+ type: field.type
313
+ });
314
+ }
315
+ for (const added of changes.addFields ?? []) {
316
+ rebuiltFields.push(added);
317
+ }
318
+ const hadOnAdd = /\bonAdd\s*\(/.test(existingSource);
319
+ const hadOnRemove = /\bonRemove\s*\(/.test(existingSource);
320
+ const lifecycleHooksSet = /* @__PURE__ */ new Set();
321
+ if (hadOnAdd) {
322
+ lifecycleHooksSet.add("onAdd");
323
+ }
324
+ if (hadOnRemove) {
325
+ lifecycleHooksSet.add("onRemove");
326
+ }
327
+ if (changes.addHook !== void 0) {
328
+ lifecycleHooksSet.add(changes.addHook);
329
+ }
330
+ if (changes.removeHook !== void 0) {
331
+ lifecycleHooksSet.delete(changes.removeHook);
332
+ }
333
+ const nextSource = renderComponentSource(className, rebuiltFields, Array.from(lifecycleHooksSet));
334
+ await fileSystemExtra.writeFile(absoluteFilePath, nextSource, "utf8");
335
+ return formatToolResult({
336
+ className,
337
+ filePath: absoluteFilePath,
338
+ stateFields: rebuiltFields,
339
+ lifecycleHooks: Array.from(lifecycleHooksSet)
340
+ });
341
+ }
342
+ };
343
+ const deleteComponentTool = {
344
+ name: "delete_component",
345
+ description: "Delete a component file and report warnings for any references to the class name found under src/systems/ or src/scenes/.",
346
+ inputSchema: zodToJsonSchema(deleteComponentInputSchema),
347
+ handler: async (input) => {
348
+ const parsed = deleteComponentInputSchema.parse(input);
349
+ const { absoluteFilePath, className } = resolveComponentPaths(projectDirectory, parsed.name);
350
+ if (!await fileSystemExtra.pathExists(absoluteFilePath)) {
351
+ throw new Error(`Component file not found: ${absoluteFilePath}`);
352
+ }
353
+ const warnings = [];
354
+ const scanRoots = [
355
+ resolveWithinProject(projectDirectory, "src/systems"),
356
+ resolveWithinProject(projectDirectory, "src/scenes")
357
+ ];
358
+ for (const root of scanRoots) {
359
+ const files = await collectTypeScriptFiles(root);
360
+ for (const filePath of files) {
361
+ const content = await fileSystemExtra.readFile(filePath, "utf8");
362
+ const lines = content.split("\n");
363
+ lines.forEach((lineText, lineIndex) => {
364
+ const pattern = new RegExp(`\\b${className}\\b`);
365
+ if (pattern.test(lineText)) {
366
+ warnings.push({
367
+ filePath,
368
+ line: lineIndex + 1,
369
+ text: lineText.trim()
370
+ });
371
+ }
372
+ });
373
+ }
374
+ }
375
+ await fileSystemExtra.remove(absoluteFilePath);
376
+ return formatToolResult({ deleted: absoluteFilePath, className, warnings });
377
+ }
378
+ };
379
+ const listComponentsTool = {
380
+ name: "list_components",
381
+ description: "List all component files under src/components/ with their class names, state fields, and declared dependencies.",
382
+ inputSchema: zodToJsonSchema(listComponentsInputSchema),
383
+ handler: async (input) => {
384
+ listComponentsInputSchema.parse(input ?? {});
385
+ const componentsDirectory = resolveWithinProject(projectDirectory, "src/components");
386
+ const files = await collectTypeScriptFiles(componentsDirectory);
387
+ const components = [];
388
+ for (const filePath of files) {
389
+ const content = await fileSystemExtra.readFile(filePath, "utf8");
390
+ try {
391
+ const parsedFile = parseComponentSource(content, filePath);
392
+ components.push({
393
+ name: parsedFile.className,
394
+ filePath,
395
+ stateFields: parsedFile.stateFields.map((field) => ({
396
+ name: field.name,
397
+ type: field.type
398
+ })),
399
+ dependencies: parsedFile.dependencies
400
+ });
401
+ } catch {
402
+ }
403
+ }
404
+ return formatToolResult({ components });
405
+ }
406
+ };
407
+ context.registerTool(createComponentTool);
408
+ context.registerTool(readComponentTool);
409
+ context.registerTool(updateComponentTool);
410
+ context.registerTool(deleteComponentTool);
411
+ context.registerTool(listComponentsTool);
412
+ }
413
+
414
+ // ../mcp/dist/tools/component-system/systems.js
415
+ import path3 from "path";
416
+ import fileSystemExtra2 from "fs-extra";
417
+ import typescript2 from "typescript";
418
+ import { z as z2 } from "zod";
419
+ import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
420
+ var priorityEnum = z2.enum(["Highest", "Higher", "Average", "Lower", "Lowest"]);
421
+ var systemTypeEnum = z2.enum(["Update", "Draw"]);
422
+ var createSystemInputSchema = z2.object({
423
+ name: z2.string().min(1),
424
+ queryComponents: z2.array(z2.string().min(1)).default([]),
425
+ priority: priorityEnum.optional(),
426
+ systemType: systemTypeEnum.optional()
427
+ });
428
+ var readSystemInputSchema = z2.object({ name: z2.string().min(1) });
429
+ var updateSystemInputSchema = z2.object({
430
+ name: z2.string().min(1),
431
+ changes: z2.object({
432
+ setQueryComponents: z2.array(z2.string().min(1)).optional(),
433
+ setPriority: priorityEnum.optional(),
434
+ setSystemType: systemTypeEnum.optional()
435
+ }).default({})
436
+ });
437
+ var deleteSystemInputSchema = z2.object({ name: z2.string().min(1) });
438
+ var listSystemsInputSchema = z2.object({}).default({});
439
+ function renderSystemSource(className, queryComponents, priority, systemType) {
440
+ const queryComponentList = queryComponents.join(", ");
441
+ const coreImports = [
442
+ "System",
443
+ "SystemPriority",
444
+ "SystemType",
445
+ "Query",
446
+ "type World"
447
+ ];
448
+ for (const componentName of queryComponents) {
449
+ if (!coreImports.includes(componentName)) {
450
+ coreImports.push(componentName);
451
+ }
452
+ }
453
+ const lines = [];
454
+ lines.push(`import {`);
455
+ for (const importName of coreImports) {
456
+ lines.push(` ${importName},`);
457
+ }
458
+ lines.push(`} from '@donut/core';`);
459
+ lines.push("");
460
+ lines.push("/**");
461
+ lines.push(` * ${className} \u2014 generated by the Donut MCP \`create_system\` tool.`);
462
+ lines.push(" *");
463
+ lines.push(" * Processes entities matching its declared component query each frame.");
464
+ lines.push(" */");
465
+ lines.push(`export class ${className} extends System {`);
466
+ lines.push(` /** Indicates whether this system runs in the update or draw phase. */`);
467
+ lines.push(` readonly systemType = SystemType.${systemType};`);
468
+ lines.push("");
469
+ lines.push(` /** Execution priority relative to sibling systems. */`);
470
+ lines.push(` static priority = SystemPriority.${priority};`);
471
+ lines.push("");
472
+ lines.push(` /** Reactive query matching entities this system operates on. */`);
473
+ lines.push(` private readonly entityQuery: Query<${queryComponents.length > 0 ? `typeof ${queryComponents.join(", typeof ")}` : "never"}>;`);
474
+ lines.push("");
475
+ lines.push(" /** Build the query once the world is available. */");
476
+ lines.push(" constructor(world: World) {");
477
+ lines.push(" super();");
478
+ lines.push(` this.entityQuery = world.query([${queryComponentList}]) as Query<${queryComponents.length > 0 ? `typeof ${queryComponents.join(", typeof ")}` : "never"}>;`);
479
+ lines.push(" }");
480
+ lines.push("");
481
+ lines.push(" /**");
482
+ lines.push(" * Frame update. Iterates matching entities and applies system logic.");
483
+ lines.push(" * @param elapsedMilliseconds time since previous frame, in milliseconds");
484
+ lines.push(" */");
485
+ lines.push(" update(elapsedMilliseconds: number): void {");
486
+ lines.push(" void elapsedMilliseconds;");
487
+ lines.push(" for (const entity of this.entityQuery.entities) {");
488
+ lines.push(" void entity;");
489
+ lines.push(" }");
490
+ lines.push(" }");
491
+ lines.push("}");
492
+ lines.push("");
493
+ return lines.join("\n");
494
+ }
495
+ function parseSystemSource(sourceText, filePath) {
496
+ const sourceFile = typescript2.createSourceFile(filePath, sourceText, typescript2.ScriptTarget.ES2022, true);
497
+ let className;
498
+ let priority = "Average";
499
+ let systemType = "Update";
500
+ const queryComponents = [];
501
+ typescript2.forEachChild(sourceFile, (node) => {
502
+ if (!typescript2.isClassDeclaration(node) || node.name === void 0) {
503
+ return;
504
+ }
505
+ className = node.name.text;
506
+ for (const member of node.members) {
507
+ if (typescript2.isPropertyDeclaration(member) && typescript2.isIdentifier(member.name)) {
508
+ const propertyName = member.name.text;
509
+ if (propertyName === "systemType" && member.initializer !== void 0) {
510
+ const text = member.initializer.getText(sourceFile);
511
+ const match = /SystemType\.(\w+)/.exec(text);
512
+ if (match) {
513
+ systemType = match[1];
514
+ }
515
+ }
516
+ if (propertyName === "priority" && member.initializer !== void 0) {
517
+ const text = member.initializer.getText(sourceFile);
518
+ const match = /SystemPriority\.(\w+)/.exec(text);
519
+ if (match) {
520
+ priority = match[1];
521
+ }
522
+ }
523
+ }
524
+ if (typescript2.isConstructorDeclaration(member) && member.body !== void 0) {
525
+ const bodyText = member.body.getText(sourceFile);
526
+ const queryMatch = /world\.query\(\s*\[([^\]]*)\]/m.exec(bodyText);
527
+ if (queryMatch) {
528
+ const inside = queryMatch[1].trim();
529
+ if (inside.length > 0) {
530
+ for (const part of inside.split(",")) {
531
+ const trimmed = part.trim();
532
+ if (trimmed.length > 0) {
533
+ queryComponents.push(trimmed);
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ });
541
+ if (className === void 0) {
542
+ throw new Error(`No class declaration found in ${filePath}`);
543
+ }
544
+ return { className, queryComponents, priority, systemType };
545
+ }
546
+ function resolveSystemPaths(projectDirectory, rawName) {
547
+ const pascal = toPascalCase(rawName);
548
+ const baseName = stripSuffix(pascal, "System");
549
+ const className = `${baseName}System`;
550
+ const kebabName = toKebabCase(baseName);
551
+ const relativePath = path3.posix.join("src", "systems", `${kebabName}-system.ts`);
552
+ const absoluteFilePath = resolveWithinProject(projectDirectory, relativePath);
553
+ return { absoluteFilePath, className, kebabName };
554
+ }
555
+ function formatToolResult2(payload) {
556
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
557
+ }
558
+ async function collectTypeScriptFiles2(directory) {
559
+ if (!await fileSystemExtra2.pathExists(directory)) {
560
+ return [];
561
+ }
562
+ const collected = [];
563
+ const walk = async (current) => {
564
+ const entries = await fileSystemExtra2.readdir(current, { withFileTypes: true });
565
+ for (const entry of entries) {
566
+ const entryAbsolute = path3.join(current, entry.name);
567
+ if (entry.isDirectory()) {
568
+ await walk(entryAbsolute);
569
+ continue;
570
+ }
571
+ if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
572
+ collected.push(entryAbsolute);
573
+ }
574
+ }
575
+ };
576
+ await walk(directory);
577
+ return collected;
578
+ }
579
+ function registerSystemTools(context) {
580
+ const projectDirectory = context.projectDirectory;
581
+ const createSystemTool = {
582
+ name: "create_system",
583
+ description: "Scaffold a new Donut system file under src/systems/. Declares a reactive query over the named components and an empty update loop. Imports only from @donut/core to preserve the portability guarantee for game logic.",
584
+ inputSchema: zodToJsonSchema2(createSystemInputSchema),
585
+ handler: async (input) => {
586
+ const parsed = createSystemInputSchema.parse(input);
587
+ const { absoluteFilePath, className } = resolveSystemPaths(projectDirectory, parsed.name);
588
+ if (await fileSystemExtra2.pathExists(absoluteFilePath)) {
589
+ throw new Error(`System file already exists: ${absoluteFilePath}`);
590
+ }
591
+ const priority = parsed.priority ?? "Average";
592
+ const systemType = parsed.systemType ?? "Update";
593
+ const source = renderSystemSource(className, parsed.queryComponents, priority, systemType);
594
+ await fileSystemExtra2.ensureDir(path3.dirname(absoluteFilePath));
595
+ await fileSystemExtra2.writeFile(absoluteFilePath, source, "utf8");
596
+ return formatToolResult2({ className, filePath: absoluteFilePath });
597
+ }
598
+ };
599
+ const readSystemTool = {
600
+ name: "read_system",
601
+ description: "Return the source text of an existing system file.",
602
+ inputSchema: zodToJsonSchema2(readSystemInputSchema),
603
+ handler: async (input) => {
604
+ const parsed = readSystemInputSchema.parse(input);
605
+ const { absoluteFilePath, className } = resolveSystemPaths(projectDirectory, parsed.name);
606
+ if (!await fileSystemExtra2.pathExists(absoluteFilePath)) {
607
+ throw new Error(`System file not found: ${absoluteFilePath}`);
608
+ }
609
+ const source = await fileSystemExtra2.readFile(absoluteFilePath, "utf8");
610
+ return formatToolResult2({ className, filePath: absoluteFilePath, source });
611
+ }
612
+ };
613
+ const updateSystemTool = {
614
+ name: "update_system",
615
+ description: "Apply structural changes to an existing system: replace the query component list, priority, or system-type. File is re-rendered from the merged configuration.",
616
+ inputSchema: zodToJsonSchema2(updateSystemInputSchema),
617
+ handler: async (input) => {
618
+ const parsed = updateSystemInputSchema.parse(input);
619
+ const { absoluteFilePath, className } = resolveSystemPaths(projectDirectory, parsed.name);
620
+ if (!await fileSystemExtra2.pathExists(absoluteFilePath)) {
621
+ throw new Error(`System file not found: ${absoluteFilePath}`);
622
+ }
623
+ const existingSource = await fileSystemExtra2.readFile(absoluteFilePath, "utf8");
624
+ const existing = parseSystemSource(existingSource, absoluteFilePath);
625
+ const nextQueryComponents = parsed.changes.setQueryComponents ?? existing.queryComponents;
626
+ const nextPriority = parsed.changes.setPriority ?? existing.priority;
627
+ const nextSystemType = parsed.changes.setSystemType ?? existing.systemType;
628
+ const nextSource = renderSystemSource(className, nextQueryComponents, nextPriority, nextSystemType);
629
+ await fileSystemExtra2.writeFile(absoluteFilePath, nextSource, "utf8");
630
+ return formatToolResult2({
631
+ className,
632
+ filePath: absoluteFilePath,
633
+ queryComponents: nextQueryComponents,
634
+ priority: nextPriority,
635
+ systemType: nextSystemType
636
+ });
637
+ }
638
+ };
639
+ const deleteSystemTool = {
640
+ name: "delete_system",
641
+ description: "Delete a system file and report warnings for any references to the class name found under src/scenes/.",
642
+ inputSchema: zodToJsonSchema2(deleteSystemInputSchema),
643
+ handler: async (input) => {
644
+ const parsed = deleteSystemInputSchema.parse(input);
645
+ const { absoluteFilePath, className } = resolveSystemPaths(projectDirectory, parsed.name);
646
+ if (!await fileSystemExtra2.pathExists(absoluteFilePath)) {
647
+ throw new Error(`System file not found: ${absoluteFilePath}`);
648
+ }
649
+ const warnings = [];
650
+ const scenesDirectory = resolveWithinProject(projectDirectory, "src/scenes");
651
+ const files = await collectTypeScriptFiles2(scenesDirectory);
652
+ for (const filePath of files) {
653
+ const content = await fileSystemExtra2.readFile(filePath, "utf8");
654
+ const lines = content.split("\n");
655
+ lines.forEach((lineText, lineIndex) => {
656
+ const pattern = new RegExp(`\\b${className}\\b`);
657
+ if (pattern.test(lineText)) {
658
+ warnings.push({ filePath, line: lineIndex + 1, text: lineText.trim() });
659
+ }
660
+ });
661
+ }
662
+ await fileSystemExtra2.remove(absoluteFilePath);
663
+ return formatToolResult2({ deleted: absoluteFilePath, className, warnings });
664
+ }
665
+ };
666
+ const listSystemsTool = {
667
+ name: "list_systems",
668
+ description: "List all system files under src/systems/ with their class names, query components, priority, and system-type.",
669
+ inputSchema: zodToJsonSchema2(listSystemsInputSchema),
670
+ handler: async (input) => {
671
+ listSystemsInputSchema.parse(input ?? {});
672
+ const systemsDirectory = resolveWithinProject(projectDirectory, "src/systems");
673
+ const files = await collectTypeScriptFiles2(systemsDirectory);
674
+ const systems = [];
675
+ for (const filePath of files) {
676
+ const content = await fileSystemExtra2.readFile(filePath, "utf8");
677
+ try {
678
+ const parsedFile = parseSystemSource(content, filePath);
679
+ systems.push({
680
+ name: parsedFile.className,
681
+ filePath,
682
+ queryComponents: parsedFile.queryComponents,
683
+ priority: parsedFile.priority,
684
+ systemType: parsedFile.systemType
685
+ });
686
+ } catch {
687
+ }
688
+ }
689
+ return formatToolResult2({ systems });
690
+ }
691
+ };
692
+ context.registerTool(createSystemTool);
693
+ context.registerTool(readSystemTool);
694
+ context.registerTool(updateSystemTool);
695
+ context.registerTool(deleteSystemTool);
696
+ context.registerTool(listSystemsTool);
697
+ }
698
+
699
+ // ../mcp/dist/tools/component-system/index.js
700
+ function registerComponentSystemTools(_server, context) {
701
+ registerComponentTools(context);
702
+ registerSystemTools(context);
703
+ }
704
+
705
+ // ../mcp/dist/tools/entity-scene/assets.js
706
+ import path4 from "path";
707
+ import fileSystemExtra3 from "fs-extra";
708
+ import { z as z3 } from "zod";
709
+ import { zodToJsonSchema as zodToJsonSchema3 } from "zod-to-json-schema";
710
+ var assetTypeSchema = z3.enum(["texture", "audio", "font", "other"]);
711
+ var addAssetInputSchema = z3.object({
712
+ type: assetTypeSchema,
713
+ sourcePath: z3.string().min(1),
714
+ name: z3.string().min(1).optional()
715
+ });
716
+ var getAssetInputSchema = z3.object({ name: z3.string().min(1) });
717
+ var replaceAssetInputSchema = z3.object({
718
+ name: z3.string().min(1),
719
+ newSourcePath: z3.string().min(1)
720
+ });
721
+ var deleteAssetInputSchema = z3.object({ name: z3.string().min(1) });
722
+ var listAssetsInputSchema = z3.object({ type: assetTypeSchema.optional() }).default({});
723
+ var ASSET_TYPE_DIRECTORIES = Object.freeze({
724
+ texture: "textures",
725
+ audio: "audio",
726
+ font: "fonts",
727
+ other: "other"
728
+ });
729
+ function formatToolResult3(payload) {
730
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
731
+ }
732
+ function isHttpUrl(sourcePath) {
733
+ return /^https?:\/\//i.test(sourcePath);
734
+ }
735
+ async function copyOrDownload(sourcePath, destinationAbsolute) {
736
+ await fileSystemExtra3.ensureDir(path4.dirname(destinationAbsolute));
737
+ if (isHttpUrl(sourcePath)) {
738
+ const response = await fetch(sourcePath);
739
+ if (!response.ok) {
740
+ throw new Error(`Failed to download asset from ${sourcePath}: ${response.status}`);
741
+ }
742
+ const buffer = Buffer.from(await response.arrayBuffer());
743
+ await fileSystemExtra3.writeFile(destinationAbsolute, buffer);
744
+ return;
745
+ }
746
+ if (!await fileSystemExtra3.pathExists(sourcePath)) {
747
+ throw new Error(`Asset source file not found: ${sourcePath}`);
748
+ }
749
+ await fileSystemExtra3.copy(sourcePath, destinationAbsolute, { overwrite: true });
750
+ }
751
+ async function scanAssets(projectDirectory) {
752
+ const assetsRoot = resolveWithinProject(projectDirectory, "assets");
753
+ if (!await fileSystemExtra3.pathExists(assetsRoot)) {
754
+ return [];
755
+ }
756
+ const located = [];
757
+ for (const [typeKey, typeDirectory] of Object.entries(ASSET_TYPE_DIRECTORIES)) {
758
+ const absoluteTypeDirectory = path4.join(assetsRoot, typeDirectory);
759
+ if (!await fileSystemExtra3.pathExists(absoluteTypeDirectory)) {
760
+ continue;
761
+ }
762
+ const entries = await fileSystemExtra3.readdir(absoluteTypeDirectory);
763
+ for (const entryName of entries) {
764
+ const absolutePath = path4.join(absoluteTypeDirectory, entryName);
765
+ const stats = await fileSystemExtra3.stat(absolutePath);
766
+ if (!stats.isFile()) {
767
+ continue;
768
+ }
769
+ located.push({
770
+ name: entryName,
771
+ type: typeKey,
772
+ absolutePath,
773
+ relativePath: path4.posix.join("assets", typeDirectory, entryName)
774
+ });
775
+ }
776
+ }
777
+ return located;
778
+ }
779
+ async function findAssetByName(projectDirectory, assetName) {
780
+ const assets = await scanAssets(projectDirectory);
781
+ const match = assets.find((candidate) => candidate.name === assetName);
782
+ if (match === void 0) {
783
+ throw new Error(`Asset not found: ${assetName}`);
784
+ }
785
+ return match;
786
+ }
787
+ async function findReferencesInSource(projectDirectory, searchText) {
788
+ const sourceRoot = resolveWithinProject(projectDirectory, "src");
789
+ const references = [];
790
+ if (!await fileSystemExtra3.pathExists(sourceRoot)) {
791
+ return references;
792
+ }
793
+ const walk = async (current) => {
794
+ const entries = await fileSystemExtra3.readdir(current, { withFileTypes: true });
795
+ for (const entry of entries) {
796
+ const entryAbsolute = path4.join(current, entry.name);
797
+ if (entry.isDirectory()) {
798
+ await walk(entryAbsolute);
799
+ continue;
800
+ }
801
+ if (!entry.isFile() || !entry.name.endsWith(".ts")) {
802
+ continue;
803
+ }
804
+ const content = await fileSystemExtra3.readFile(entryAbsolute, "utf8");
805
+ const lines = content.split("\n");
806
+ lines.forEach((lineText, lineIndex) => {
807
+ if (lineText.includes(searchText)) {
808
+ references.push({
809
+ filePath: entryAbsolute,
810
+ line: lineIndex + 1,
811
+ text: lineText.trim()
812
+ });
813
+ }
814
+ });
815
+ }
816
+ };
817
+ await walk(sourceRoot);
818
+ return references;
819
+ }
820
+ function registerAssetTools(context) {
821
+ const projectDirectory = context.projectDirectory;
822
+ const addAssetTool = {
823
+ name: "add_asset",
824
+ description: "Copy (or download, if sourcePath is http(s)) a file into assets/<type>/. If `name` is provided it is used as the destination filename; otherwise the source basename is used.",
825
+ inputSchema: zodToJsonSchema3(addAssetInputSchema),
826
+ handler: async (input) => {
827
+ const parsedInput = addAssetInputSchema.parse(input);
828
+ const destinationFilename = parsedInput.name ?? path4.basename(parsedInput.sourcePath.split("?")[0]);
829
+ const relativePath = path4.posix.join("assets", ASSET_TYPE_DIRECTORIES[parsedInput.type], destinationFilename);
830
+ const destinationAbsolute = resolveWithinProject(projectDirectory, relativePath);
831
+ await copyOrDownload(parsedInput.sourcePath, destinationAbsolute);
832
+ const stats = await fileSystemExtra3.stat(destinationAbsolute);
833
+ return formatToolResult3({
834
+ name: destinationFilename,
835
+ type: parsedInput.type,
836
+ path: relativePath,
837
+ absolutePath: destinationAbsolute,
838
+ sizeBytes: stats.size
839
+ });
840
+ }
841
+ };
842
+ const getAssetTool = {
843
+ name: "get_asset",
844
+ description: "Return metadata (name, type, relative path, size in bytes) for an asset by filename. Dimensions are omitted because they would require a native image decoder.",
845
+ inputSchema: zodToJsonSchema3(getAssetInputSchema),
846
+ handler: async (input) => {
847
+ const parsedInput = getAssetInputSchema.parse(input);
848
+ const located = await findAssetByName(projectDirectory, parsedInput.name);
849
+ const stats = await fileSystemExtra3.stat(located.absolutePath);
850
+ return formatToolResult3({
851
+ name: located.name,
852
+ type: located.type,
853
+ path: located.relativePath,
854
+ absolutePath: located.absolutePath,
855
+ sizeBytes: stats.size
856
+ });
857
+ }
858
+ };
859
+ const replaceAssetTool = {
860
+ name: "replace_asset",
861
+ description: "Overwrite an existing asset file with a new source (local path or http(s) URL). The destination filename and type folder are preserved.",
862
+ inputSchema: zodToJsonSchema3(replaceAssetInputSchema),
863
+ handler: async (input) => {
864
+ const parsedInput = replaceAssetInputSchema.parse(input);
865
+ const located = await findAssetByName(projectDirectory, parsedInput.name);
866
+ await copyOrDownload(parsedInput.newSourcePath, located.absolutePath);
867
+ const stats = await fileSystemExtra3.stat(located.absolutePath);
868
+ return formatToolResult3({
869
+ name: located.name,
870
+ type: located.type,
871
+ path: located.relativePath,
872
+ sizeBytes: stats.size
873
+ });
874
+ }
875
+ };
876
+ const deleteAssetTool = {
877
+ name: "delete_asset",
878
+ description: "Delete an asset file. Before deletion, scans src/ for string references to the asset filename and returns them as warnings.",
879
+ inputSchema: zodToJsonSchema3(deleteAssetInputSchema),
880
+ handler: async (input) => {
881
+ const parsedInput = deleteAssetInputSchema.parse(input);
882
+ const located = await findAssetByName(projectDirectory, parsedInput.name);
883
+ const warnings = await findReferencesInSource(projectDirectory, located.name);
884
+ await fileSystemExtra3.remove(located.absolutePath);
885
+ return formatToolResult3({
886
+ deleted: located.absolutePath,
887
+ name: located.name,
888
+ warnings
889
+ });
890
+ }
891
+ };
892
+ const listAssetsTool = {
893
+ name: "list_assets",
894
+ description: "List all assets, optionally filtered by type.",
895
+ inputSchema: zodToJsonSchema3(listAssetsInputSchema),
896
+ handler: async (input) => {
897
+ const parsedInput = listAssetsInputSchema.parse(input ?? {});
898
+ const assets = await scanAssets(projectDirectory);
899
+ const filtered = parsedInput.type === void 0 ? assets : assets.filter((asset) => asset.type === parsedInput.type);
900
+ return formatToolResult3({
901
+ assets: filtered.map((asset) => ({
902
+ name: asset.name,
903
+ type: asset.type,
904
+ path: asset.relativePath
905
+ }))
906
+ });
907
+ }
908
+ };
909
+ context.registerTool(addAssetTool);
910
+ context.registerTool(getAssetTool);
911
+ context.registerTool(replaceAssetTool);
912
+ context.registerTool(deleteAssetTool);
913
+ context.registerTool(listAssetsTool);
914
+ }
915
+
916
+ // ../mcp/dist/tools/entity-scene/entities.js
917
+ import { z as z4 } from "zod";
918
+ import { zodToJsonSchema as zodToJsonSchema4 } from "zod-to-json-schema";
919
+
920
+ // ../mcp/dist/tools/entity-scene/scene-file.js
921
+ import path5 from "path";
922
+ import fileSystemExtra4 from "fs-extra";
923
+ var ENGINE_COMPONENT_PACKAGE = Object.freeze({
924
+ TransformComponent: "@donut/core",
925
+ SpriteComponent: "@donut/pixi",
926
+ AnimatedSpriteComponent: "@donut/pixi",
927
+ TilemapComponent: "@donut/pixi",
928
+ Camera2dComponent: "@donut/pixi",
929
+ CtrllrInputComponent: "@donut/ctrllr"
930
+ });
931
+ var MARKER_ENTITIES_OPEN = "@donut-entities";
932
+ var MARKER_SYSTEMS_OPEN = "@donut-systems";
933
+ function resolveScenePaths(projectDirectory, rawName) {
934
+ const pascal = toPascalCase(rawName);
935
+ const sceneBaseName = stripSuffix(pascal, "Scene");
936
+ const sceneClassName = `${sceneBaseName}Scene`;
937
+ const sceneKebabName = `${toKebabCase(sceneBaseName)}-scene`;
938
+ const relativePath = path5.posix.join("src", "scenes", `${sceneKebabName}.ts`);
939
+ const absoluteFilePath = resolveWithinProject(projectDirectory, relativePath);
940
+ return { absoluteFilePath, sceneBaseName, sceneClassName, sceneKebabName, relativePath };
941
+ }
942
+ function extractMarkerJson(source, markerTag) {
943
+ const markerIndex = source.indexOf(markerTag);
944
+ if (markerIndex === -1) {
945
+ return void 0;
946
+ }
947
+ const closingIndex = source.indexOf("*/", markerIndex);
948
+ if (closingIndex === -1) {
949
+ throw new Error(`Marker ${markerTag} has no closing */`);
950
+ }
951
+ const rawBlock = source.slice(markerIndex + markerTag.length, closingIndex);
952
+ const stripped = rawBlock.split("\n").map((line) => line.replace(/^\s*\*\s?/, "")).join("\n").trim();
953
+ return stripped;
954
+ }
955
+ function parseSceneSource(source, sceneBaseName, sceneKebabName) {
956
+ const sceneClassName = `${sceneBaseName}Scene`;
957
+ const entitiesJson = extractMarkerJson(source, MARKER_ENTITIES_OPEN);
958
+ const systemsJson = extractMarkerJson(source, MARKER_SYSTEMS_OPEN);
959
+ const entities = entitiesJson !== void 0 && entitiesJson.length > 0 ? JSON.parse(entitiesJson) : [];
960
+ const systems = systemsJson !== void 0 && systemsJson.length > 0 ? JSON.parse(systemsJson) : [];
961
+ return { sceneBaseName, sceneClassName, sceneKebabName, entities, systems };
962
+ }
963
+ function renderConfigLiteral(value) {
964
+ if (Array.isArray(value) && value.every((element) => typeof element === "number")) {
965
+ if (value.length === 2) {
966
+ return `new Vector2(${value[0]}, ${value[1]})`;
967
+ }
968
+ if (value.length === 3) {
969
+ return `new Vector3(${value[0]}, ${value[1]}, ${value[2]})`;
970
+ }
971
+ if (value.length === 4) {
972
+ return `new Quaternion(${value[0]}, ${value[1]}, ${value[2]}, ${value[3]})`;
973
+ }
974
+ }
975
+ return JSON.stringify(value);
976
+ }
977
+ async function resolveComponentImportSource(projectDirectory, componentType) {
978
+ const baseName = stripSuffix(componentType, "Component");
979
+ const kebabName = toKebabCase(baseName);
980
+ const localRelative = path5.posix.join("src", "components", `${kebabName}-component.ts`);
981
+ const localAbsolute = resolveWithinProject(projectDirectory, localRelative);
982
+ if (await fileSystemExtra4.pathExists(localAbsolute)) {
983
+ return `../components/${kebabName}-component.js`;
984
+ }
985
+ const enginePackage = ENGINE_COMPONENT_PACKAGE[componentType];
986
+ if (enginePackage !== void 0) {
987
+ return enginePackage;
988
+ }
989
+ return void 0;
990
+ }
991
+ async function resolveSystemImportSource(projectDirectory, systemType) {
992
+ const baseName = stripSuffix(systemType, "System");
993
+ const kebabName = toKebabCase(baseName);
994
+ const localRelative = path5.posix.join("src", "systems", `${kebabName}-system.ts`);
995
+ const localAbsolute = resolveWithinProject(projectDirectory, localRelative);
996
+ if (await fileSystemExtra4.pathExists(localAbsolute)) {
997
+ return `../systems/${kebabName}-system.js`;
998
+ }
999
+ return void 0;
1000
+ }
1001
+ async function renderSceneSource(projectDirectory, parsedScene) {
1002
+ const componentImports = /* @__PURE__ */ new Map();
1003
+ componentImports.set("@donut/core", /* @__PURE__ */ new Set(["Entity", "type World"]));
1004
+ const todoComments = [];
1005
+ const usedMathTypes = /* @__PURE__ */ new Set();
1006
+ for (const entity of parsedScene.entities) {
1007
+ for (const componentDescriptor of entity.components) {
1008
+ const importSource = await resolveComponentImportSource(projectDirectory, componentDescriptor.type);
1009
+ if (importSource === void 0) {
1010
+ todoComments.push(`// TODO: resolve import for unknown component "${componentDescriptor.type}"`);
1011
+ continue;
1012
+ }
1013
+ const existing = componentImports.get(importSource) ?? /* @__PURE__ */ new Set();
1014
+ existing.add(componentDescriptor.type);
1015
+ componentImports.set(importSource, existing);
1016
+ if (componentDescriptor.config !== void 0) {
1017
+ for (const configValue of Object.values(componentDescriptor.config)) {
1018
+ if (Array.isArray(configValue) && configValue.every((element) => typeof element === "number")) {
1019
+ if (configValue.length === 2) {
1020
+ usedMathTypes.add("Vector2");
1021
+ } else if (configValue.length === 3) {
1022
+ usedMathTypes.add("Vector3");
1023
+ } else if (configValue.length === 4) {
1024
+ usedMathTypes.add("Quaternion");
1025
+ }
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+ const systemImports = /* @__PURE__ */ new Map();
1032
+ for (const systemType of parsedScene.systems) {
1033
+ const importSource = await resolveSystemImportSource(projectDirectory, systemType);
1034
+ if (importSource === void 0) {
1035
+ todoComments.push(`// TODO: resolve import for unknown system "${systemType}"`);
1036
+ continue;
1037
+ }
1038
+ const existing = systemImports.get(importSource) ?? /* @__PURE__ */ new Set();
1039
+ existing.add(systemType);
1040
+ systemImports.set(importSource, existing);
1041
+ }
1042
+ const lines = [];
1043
+ const externalModules = [];
1044
+ const relativeModules = [];
1045
+ for (const moduleSpecifier of componentImports.keys()) {
1046
+ if (moduleSpecifier.startsWith(".")) {
1047
+ relativeModules.push(moduleSpecifier);
1048
+ } else {
1049
+ externalModules.push(moduleSpecifier);
1050
+ }
1051
+ }
1052
+ for (const moduleSpecifier of systemImports.keys()) {
1053
+ if (moduleSpecifier.startsWith(".")) {
1054
+ relativeModules.push(moduleSpecifier);
1055
+ } else {
1056
+ externalModules.push(moduleSpecifier);
1057
+ }
1058
+ }
1059
+ externalModules.sort();
1060
+ relativeModules.sort();
1061
+ for (const moduleSpecifier of externalModules) {
1062
+ const names = /* @__PURE__ */ new Set([
1063
+ ...componentImports.get(moduleSpecifier) ?? /* @__PURE__ */ new Set(),
1064
+ ...systemImports.get(moduleSpecifier) ?? /* @__PURE__ */ new Set()
1065
+ ]);
1066
+ lines.push(`import { ${Array.from(names).sort().join(", ")} } from '${moduleSpecifier}';`);
1067
+ }
1068
+ if (usedMathTypes.size > 0) {
1069
+ lines.push(`import { ${Array.from(usedMathTypes).sort().join(", ")} } from '@donut/math';`);
1070
+ }
1071
+ for (const moduleSpecifier of relativeModules) {
1072
+ const names = /* @__PURE__ */ new Set([
1073
+ ...componentImports.get(moduleSpecifier) ?? /* @__PURE__ */ new Set(),
1074
+ ...systemImports.get(moduleSpecifier) ?? /* @__PURE__ */ new Set()
1075
+ ]);
1076
+ lines.push(`import { ${Array.from(names).sort().join(", ")} } from '${moduleSpecifier}';`);
1077
+ }
1078
+ lines.push("");
1079
+ const entitiesJsonPayload = JSON.stringify(parsedScene.entities, null, 2);
1080
+ const systemsJsonPayload = JSON.stringify(parsedScene.systems, null, 2);
1081
+ lines.push("/**");
1082
+ lines.push(` * ${MARKER_ENTITIES_OPEN}`);
1083
+ for (const jsonLine of entitiesJsonPayload.split("\n")) {
1084
+ lines.push(` * ${jsonLine}`);
1085
+ }
1086
+ lines.push(" */");
1087
+ lines.push("/**");
1088
+ lines.push(` * ${MARKER_SYSTEMS_OPEN}`);
1089
+ for (const jsonLine of systemsJsonPayload.split("\n")) {
1090
+ lines.push(` * ${jsonLine}`);
1091
+ }
1092
+ lines.push(" */");
1093
+ lines.push("");
1094
+ for (const todoComment of todoComments) {
1095
+ lines.push(todoComment);
1096
+ }
1097
+ if (todoComments.length > 0) {
1098
+ lines.push("");
1099
+ }
1100
+ lines.push("/**");
1101
+ lines.push(` * Construct the ${parsedScene.sceneClassName} entities and register its systems on`);
1102
+ lines.push(" * the provided world. Generated by the Donut MCP scene tools \u2014 do not edit the");
1103
+ lines.push(" * body directly; edit the declarative marker blocks above instead.");
1104
+ lines.push(" */");
1105
+ lines.push(`export function create${parsedScene.sceneClassName}(world: World): Entity[] {`);
1106
+ lines.push(" const createdEntities: Entity[] = [];");
1107
+ for (let entityIndex = 0; entityIndex < parsedScene.entities.length; entityIndex += 1) {
1108
+ const entity = parsedScene.entities[entityIndex];
1109
+ const entityVariable = `entity${entityIndex}`;
1110
+ lines.push("");
1111
+ lines.push(` const ${entityVariable} = new Entity({ name: ${JSON.stringify(entity.name)} });`);
1112
+ for (const tagName of entity.tags ?? []) {
1113
+ lines.push(` ${entityVariable}.addTag(${JSON.stringify(tagName)});`);
1114
+ }
1115
+ for (let componentIndex = 0; componentIndex < entity.components.length; componentIndex += 1) {
1116
+ const componentDescriptor = entity.components[componentIndex];
1117
+ const componentVariable = `${entityVariable}Component${componentIndex}`;
1118
+ lines.push(` const ${componentVariable} = new ${componentDescriptor.type}();`);
1119
+ for (const [fieldName, fieldValue] of Object.entries(componentDescriptor.config ?? {})) {
1120
+ lines.push(` ${componentVariable}.${fieldName} = ${renderConfigLiteral(fieldValue)};`);
1121
+ }
1122
+ lines.push(` ${entityVariable}.addComponent(${componentVariable});`);
1123
+ }
1124
+ lines.push(` world.add(${entityVariable});`);
1125
+ lines.push(` createdEntities.push(${entityVariable});`);
1126
+ }
1127
+ if (parsedScene.systems.length > 0) {
1128
+ lines.push("");
1129
+ for (const systemType of parsedScene.systems) {
1130
+ lines.push(` world.addSystem(new ${systemType}(world));`);
1131
+ }
1132
+ }
1133
+ lines.push("");
1134
+ lines.push(" return createdEntities;");
1135
+ lines.push("}");
1136
+ lines.push("");
1137
+ return lines.join("\n");
1138
+ }
1139
+ async function readSceneFile(projectDirectory, rawName) {
1140
+ const { absoluteFilePath, sceneBaseName, sceneKebabName } = resolveScenePaths(projectDirectory, rawName);
1141
+ if (!await fileSystemExtra4.pathExists(absoluteFilePath)) {
1142
+ throw new Error(`Scene file not found: ${absoluteFilePath}`);
1143
+ }
1144
+ const source = await fileSystemExtra4.readFile(absoluteFilePath, "utf8");
1145
+ const parsed = parseSceneSource(source, sceneBaseName, sceneKebabName);
1146
+ return { parsed, absoluteFilePath, source };
1147
+ }
1148
+ async function writeSceneFile(projectDirectory, parsedScene) {
1149
+ const { absoluteFilePath } = resolveScenePaths(projectDirectory, parsedScene.sceneBaseName);
1150
+ const source = await renderSceneSource(projectDirectory, parsedScene);
1151
+ await fileSystemExtra4.ensureDir(path5.dirname(absoluteFilePath));
1152
+ await fileSystemExtra4.writeFile(absoluteFilePath, source, "utf8");
1153
+ return absoluteFilePath;
1154
+ }
1155
+ async function listSceneFiles(projectDirectory) {
1156
+ const scenesDirectory = resolveWithinProject(projectDirectory, "src/scenes");
1157
+ if (!await fileSystemExtra4.pathExists(scenesDirectory)) {
1158
+ return [];
1159
+ }
1160
+ const entries = await fileSystemExtra4.readdir(scenesDirectory);
1161
+ const scenes = [];
1162
+ for (const entryName of entries) {
1163
+ if (!entryName.endsWith("-scene.ts") || entryName.endsWith(".test.ts")) {
1164
+ continue;
1165
+ }
1166
+ const sceneKebabName = entryName.slice(0, entryName.length - ".ts".length);
1167
+ const sceneBaseName = toPascalCase(sceneKebabName.slice(0, sceneKebabName.length - "-scene".length));
1168
+ try {
1169
+ const source = await fileSystemExtra4.readFile(path5.join(scenesDirectory, entryName), "utf8");
1170
+ scenes.push(parseSceneSource(source, sceneBaseName, sceneKebabName));
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ return scenes;
1175
+ }
1176
+
1177
+ // ../mcp/dist/tools/entity-scene/entities.js
1178
+ var createEntityInputSchema = z4.object({
1179
+ sceneName: z4.string().min(1),
1180
+ entityName: z4.string().min(1),
1181
+ components: z4.array(z4.string().min(1)).default([]),
1182
+ tags: z4.array(z4.string().min(1)).optional(),
1183
+ componentConfigs: z4.record(z4.record(z4.unknown())).optional()
1184
+ });
1185
+ var getEntityInputSchema = z4.object({
1186
+ sceneName: z4.string().min(1),
1187
+ entityName: z4.string().min(1)
1188
+ });
1189
+ var updateEntityInputSchema = z4.object({
1190
+ sceneName: z4.string().min(1),
1191
+ entityName: z4.string().min(1),
1192
+ addComponents: z4.array(z4.string().min(1)).optional(),
1193
+ removeComponents: z4.array(z4.string().min(1)).optional(),
1194
+ setConfig: z4.record(z4.record(z4.unknown())).optional(),
1195
+ addTags: z4.array(z4.string().min(1)).optional(),
1196
+ removeTags: z4.array(z4.string().min(1)).optional()
1197
+ });
1198
+ var deleteEntityInputSchema = z4.object({
1199
+ sceneName: z4.string().min(1),
1200
+ entityName: z4.string().min(1)
1201
+ });
1202
+ var listEntitiesInputSchema = z4.object({ sceneName: z4.string().min(1).optional() }).default({});
1203
+ var cloneEntityInputSchema = z4.object({
1204
+ sceneName: z4.string().min(1),
1205
+ sourceEntityName: z4.string().min(1),
1206
+ newEntityName: z4.string().min(1),
1207
+ overrides: z4.object({
1208
+ tags: z4.array(z4.string().min(1)).optional(),
1209
+ componentConfigs: z4.record(z4.record(z4.unknown())).optional()
1210
+ }).optional()
1211
+ });
1212
+ function formatToolResult4(payload) {
1213
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1214
+ }
1215
+ function deepCloneJson(value) {
1216
+ return JSON.parse(JSON.stringify(value));
1217
+ }
1218
+ function registerEntityTools(context) {
1219
+ const projectDirectory = context.projectDirectory;
1220
+ const createEntityTool = {
1221
+ name: "create_entity",
1222
+ description: "Append a new entity to the @donut-entities marker block of a scene file. The scene body is regenerated. Fails if the entity name is already used in that scene or the scene file does not exist.",
1223
+ inputSchema: zodToJsonSchema4(createEntityInputSchema),
1224
+ handler: async (input) => {
1225
+ const parsedInput = createEntityInputSchema.parse(input);
1226
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1227
+ if (parsedScene.entities.some((entity) => entity.name === parsedInput.entityName)) {
1228
+ throw new Error(`Entity "${parsedInput.entityName}" already exists in scene "${parsedInput.sceneName}"`);
1229
+ }
1230
+ const components = parsedInput.components.map((componentType) => {
1231
+ const descriptor = { type: componentType };
1232
+ const config = parsedInput.componentConfigs?.[componentType];
1233
+ if (config !== void 0) {
1234
+ descriptor.config = config;
1235
+ }
1236
+ return descriptor;
1237
+ });
1238
+ const newEntity = { name: parsedInput.entityName, components };
1239
+ if (parsedInput.tags !== void 0 && parsedInput.tags.length > 0) {
1240
+ newEntity.tags = parsedInput.tags;
1241
+ }
1242
+ parsedScene.entities.push(newEntity);
1243
+ const absoluteFilePath = await writeSceneFile(projectDirectory, parsedScene);
1244
+ return formatToolResult4({
1245
+ sceneName: parsedScene.sceneBaseName,
1246
+ entity: newEntity,
1247
+ filePath: absoluteFilePath
1248
+ });
1249
+ }
1250
+ };
1251
+ const getEntityTool = {
1252
+ name: "get_entity",
1253
+ description: "Return the declarative record for a single entity in a scene.",
1254
+ inputSchema: zodToJsonSchema4(getEntityInputSchema),
1255
+ handler: async (input) => {
1256
+ const parsedInput = getEntityInputSchema.parse(input);
1257
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1258
+ const entity = parsedScene.entities.find((candidate) => candidate.name === parsedInput.entityName);
1259
+ if (entity === void 0) {
1260
+ throw new Error(`Entity "${parsedInput.entityName}" not found in scene "${parsedInput.sceneName}"`);
1261
+ }
1262
+ return formatToolResult4({ sceneName: parsedScene.sceneBaseName, entity });
1263
+ }
1264
+ };
1265
+ const updateEntityTool = {
1266
+ name: "update_entity",
1267
+ description: "Add/remove components, replace component config, and add/remove tags on an existing entity. The scene body is regenerated on every call.",
1268
+ inputSchema: zodToJsonSchema4(updateEntityInputSchema),
1269
+ handler: async (input) => {
1270
+ const parsedInput = updateEntityInputSchema.parse(input);
1271
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1272
+ const entity = parsedScene.entities.find((candidate) => candidate.name === parsedInput.entityName);
1273
+ if (entity === void 0) {
1274
+ throw new Error(`Entity "${parsedInput.entityName}" not found in scene "${parsedInput.sceneName}"`);
1275
+ }
1276
+ if (parsedInput.removeComponents !== void 0) {
1277
+ const toRemove = new Set(parsedInput.removeComponents);
1278
+ entity.components = entity.components.filter((component) => !toRemove.has(component.type));
1279
+ }
1280
+ if (parsedInput.addComponents !== void 0) {
1281
+ for (const componentType of parsedInput.addComponents) {
1282
+ if (!entity.components.some((component) => component.type === componentType)) {
1283
+ entity.components.push({ type: componentType });
1284
+ }
1285
+ }
1286
+ }
1287
+ if (parsedInput.setConfig !== void 0) {
1288
+ for (const [componentType, config] of Object.entries(parsedInput.setConfig)) {
1289
+ let component = entity.components.find((candidate) => candidate.type === componentType);
1290
+ if (component === void 0) {
1291
+ component = { type: componentType };
1292
+ entity.components.push(component);
1293
+ }
1294
+ component.config = { ...component.config ?? {}, ...config };
1295
+ }
1296
+ }
1297
+ if (parsedInput.addTags !== void 0) {
1298
+ const nextTags = new Set(entity.tags ?? []);
1299
+ for (const tag of parsedInput.addTags) {
1300
+ nextTags.add(tag);
1301
+ }
1302
+ entity.tags = Array.from(nextTags);
1303
+ }
1304
+ if (parsedInput.removeTags !== void 0) {
1305
+ const removalSet = new Set(parsedInput.removeTags);
1306
+ entity.tags = (entity.tags ?? []).filter((tag) => !removalSet.has(tag));
1307
+ if (entity.tags.length === 0) {
1308
+ delete entity.tags;
1309
+ }
1310
+ }
1311
+ const absoluteFilePath = await writeSceneFile(projectDirectory, parsedScene);
1312
+ return formatToolResult4({
1313
+ sceneName: parsedScene.sceneBaseName,
1314
+ entity,
1315
+ filePath: absoluteFilePath
1316
+ });
1317
+ }
1318
+ };
1319
+ const deleteEntityTool = {
1320
+ name: "delete_entity",
1321
+ description: "Remove an entity from a scene and regenerate the scene body.",
1322
+ inputSchema: zodToJsonSchema4(deleteEntityInputSchema),
1323
+ handler: async (input) => {
1324
+ const parsedInput = deleteEntityInputSchema.parse(input);
1325
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1326
+ const entityIndex = parsedScene.entities.findIndex((candidate) => candidate.name === parsedInput.entityName);
1327
+ if (entityIndex === -1) {
1328
+ throw new Error(`Entity "${parsedInput.entityName}" not found in scene "${parsedInput.sceneName}"`);
1329
+ }
1330
+ parsedScene.entities.splice(entityIndex, 1);
1331
+ const absoluteFilePath = await writeSceneFile(projectDirectory, parsedScene);
1332
+ return formatToolResult4({
1333
+ sceneName: parsedScene.sceneBaseName,
1334
+ deletedEntityName: parsedInput.entityName,
1335
+ filePath: absoluteFilePath
1336
+ });
1337
+ }
1338
+ };
1339
+ const listEntitiesTool = {
1340
+ name: "list_entities",
1341
+ description: "List all entities in a single scene, or across every scene under src/scenes/ when sceneName is omitted.",
1342
+ inputSchema: zodToJsonSchema4(listEntitiesInputSchema),
1343
+ handler: async (input) => {
1344
+ const parsedInput = listEntitiesInputSchema.parse(input ?? {});
1345
+ if (parsedInput.sceneName !== void 0) {
1346
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1347
+ return formatToolResult4({
1348
+ entities: parsedScene.entities.map((entity) => ({
1349
+ sceneName: parsedScene.sceneBaseName,
1350
+ ...entity
1351
+ }))
1352
+ });
1353
+ }
1354
+ const scenes = await listSceneFiles(projectDirectory);
1355
+ const entities = scenes.flatMap((scene) => scene.entities.map((entity) => ({
1356
+ sceneName: scene.sceneBaseName,
1357
+ ...entity
1358
+ })));
1359
+ return formatToolResult4({ entities });
1360
+ }
1361
+ };
1362
+ const cloneEntityTool = {
1363
+ name: "clone_entity",
1364
+ description: "Deep-clone an entity within the same scene under a new name. Overrides can replace tags and merge component configs on the clone.",
1365
+ inputSchema: zodToJsonSchema4(cloneEntityInputSchema),
1366
+ handler: async (input) => {
1367
+ const parsedInput = cloneEntityInputSchema.parse(input);
1368
+ const { parsed: parsedScene } = await readSceneFile(projectDirectory, parsedInput.sceneName);
1369
+ const sourceEntity = parsedScene.entities.find((candidate) => candidate.name === parsedInput.sourceEntityName);
1370
+ if (sourceEntity === void 0) {
1371
+ throw new Error(`Source entity "${parsedInput.sourceEntityName}" not found in scene "${parsedInput.sceneName}"`);
1372
+ }
1373
+ if (parsedScene.entities.some((candidate) => candidate.name === parsedInput.newEntityName)) {
1374
+ throw new Error(`Entity "${parsedInput.newEntityName}" already exists in scene "${parsedInput.sceneName}"`);
1375
+ }
1376
+ const clone = deepCloneJson(sourceEntity);
1377
+ clone.name = parsedInput.newEntityName;
1378
+ if (parsedInput.overrides?.tags !== void 0) {
1379
+ clone.tags = parsedInput.overrides.tags;
1380
+ }
1381
+ if (parsedInput.overrides?.componentConfigs !== void 0) {
1382
+ for (const [componentType, config] of Object.entries(parsedInput.overrides.componentConfigs)) {
1383
+ let component = clone.components.find((candidate) => candidate.type === componentType);
1384
+ if (component === void 0) {
1385
+ component = { type: componentType };
1386
+ clone.components.push(component);
1387
+ }
1388
+ component.config = { ...component.config ?? {}, ...config };
1389
+ }
1390
+ }
1391
+ parsedScene.entities.push(clone);
1392
+ const absoluteFilePath = await writeSceneFile(projectDirectory, parsedScene);
1393
+ return formatToolResult4({
1394
+ sceneName: parsedScene.sceneBaseName,
1395
+ entity: clone,
1396
+ filePath: absoluteFilePath
1397
+ });
1398
+ }
1399
+ };
1400
+ context.registerTool(createEntityTool);
1401
+ context.registerTool(getEntityTool);
1402
+ context.registerTool(updateEntityTool);
1403
+ context.registerTool(deleteEntityTool);
1404
+ context.registerTool(listEntitiesTool);
1405
+ context.registerTool(cloneEntityTool);
1406
+ }
1407
+
1408
+ // ../mcp/dist/tools/entity-scene/project.js
1409
+ import fileSystemExtra5 from "fs-extra";
1410
+ import { z as z5 } from "zod";
1411
+ import { zodToJsonSchema as zodToJsonSchema5 } from "zod-to-json-schema";
1412
+ var readProjectConfigInputSchema = z5.object({}).default({});
1413
+ var updateProjectConfigInputSchema = z5.object({
1414
+ changes: z5.record(z5.unknown())
1415
+ });
1416
+ function formatToolResult5(payload) {
1417
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1418
+ }
1419
+ function registerProjectTools(context) {
1420
+ const projectDirectory = context.projectDirectory;
1421
+ const readProjectConfigTool = {
1422
+ name: "read_project_config",
1423
+ description: "Return the parsed contents of donut.json.",
1424
+ inputSchema: zodToJsonSchema5(readProjectConfigInputSchema),
1425
+ handler: async (input) => {
1426
+ readProjectConfigInputSchema.parse(input ?? {});
1427
+ const manifestPath = resolveWithinProject(projectDirectory, "donut.json");
1428
+ if (!await fileSystemExtra5.pathExists(manifestPath)) {
1429
+ throw new Error(`donut.json not found at ${manifestPath}`);
1430
+ }
1431
+ const raw = await fileSystemExtra5.readFile(manifestPath, "utf8");
1432
+ const config = JSON.parse(raw);
1433
+ return formatToolResult5({ filePath: manifestPath, config });
1434
+ }
1435
+ };
1436
+ const updateProjectConfigTool = {
1437
+ name: "update_project_config",
1438
+ description: "Shallow-merge `changes` into donut.json and write back the result. Top-level keys in `changes` overwrite existing values.",
1439
+ inputSchema: zodToJsonSchema5(updateProjectConfigInputSchema),
1440
+ handler: async (input) => {
1441
+ const parsedInput = updateProjectConfigInputSchema.parse(input);
1442
+ const manifestPath = resolveWithinProject(projectDirectory, "donut.json");
1443
+ if (!await fileSystemExtra5.pathExists(manifestPath)) {
1444
+ throw new Error(`donut.json not found at ${manifestPath}`);
1445
+ }
1446
+ const raw = await fileSystemExtra5.readFile(manifestPath, "utf8");
1447
+ const existing = JSON.parse(raw);
1448
+ const merged = { ...existing, ...parsedInput.changes };
1449
+ await fileSystemExtra5.writeFile(manifestPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
1450
+ return formatToolResult5({ filePath: manifestPath, config: merged });
1451
+ }
1452
+ };
1453
+ context.registerTool(readProjectConfigTool);
1454
+ context.registerTool(updateProjectConfigTool);
1455
+ }
1456
+
1457
+ // ../mcp/dist/tools/entity-scene/scenes.js
1458
+ import fileSystemExtra6 from "fs-extra";
1459
+ import { z as z6 } from "zod";
1460
+ import { zodToJsonSchema as zodToJsonSchema6 } from "zod-to-json-schema";
1461
+ var createSceneInputSchema = z6.object({
1462
+ name: z6.string().min(1),
1463
+ systems: z6.array(z6.string().min(1)).optional()
1464
+ });
1465
+ var readSceneInputSchema = z6.object({ name: z6.string().min(1) });
1466
+ var updateSceneInputSchema = z6.object({
1467
+ name: z6.string().min(1),
1468
+ setSystems: z6.array(z6.string().min(1)).optional()
1469
+ });
1470
+ var deleteSceneInputSchema = z6.object({ name: z6.string().min(1) });
1471
+ var listScenesInputSchema = z6.object({}).default({});
1472
+ var setActiveSceneInputSchema = z6.object({ name: z6.string().min(1) });
1473
+ function formatToolResult6(payload) {
1474
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1475
+ }
1476
+ function registerSceneTools(context) {
1477
+ const projectDirectory = context.projectDirectory;
1478
+ const createSceneTool = {
1479
+ name: "create_scene",
1480
+ description: "Create a new scene file under src/scenes/ with empty @donut-entities and @donut-systems marker blocks. Fails if the file already exists.",
1481
+ inputSchema: zodToJsonSchema6(createSceneInputSchema),
1482
+ handler: async (input) => {
1483
+ const parsedInput = createSceneInputSchema.parse(input);
1484
+ const { absoluteFilePath, sceneBaseName, sceneClassName, sceneKebabName } = resolveScenePaths(projectDirectory, parsedInput.name);
1485
+ if (await fileSystemExtra6.pathExists(absoluteFilePath)) {
1486
+ throw new Error(`Scene file already exists: ${absoluteFilePath}`);
1487
+ }
1488
+ const parsedScene = {
1489
+ sceneBaseName,
1490
+ sceneClassName,
1491
+ sceneKebabName,
1492
+ entities: [],
1493
+ systems: parsedInput.systems ?? []
1494
+ };
1495
+ const written = await writeSceneFile(projectDirectory, parsedScene);
1496
+ return formatToolResult6({
1497
+ sceneName: sceneBaseName,
1498
+ className: sceneClassName,
1499
+ filePath: written
1500
+ });
1501
+ }
1502
+ };
1503
+ const readSceneTool = {
1504
+ name: "read_scene",
1505
+ description: "Return the current source text of a scene file plus its parsed data.",
1506
+ inputSchema: zodToJsonSchema6(readSceneInputSchema),
1507
+ handler: async (input) => {
1508
+ const parsedInput = readSceneInputSchema.parse(input);
1509
+ const { parsed, absoluteFilePath, source } = await readSceneFile(projectDirectory, parsedInput.name);
1510
+ return formatToolResult6({
1511
+ sceneName: parsed.sceneBaseName,
1512
+ className: parsed.sceneClassName,
1513
+ filePath: absoluteFilePath,
1514
+ source,
1515
+ entities: parsed.entities,
1516
+ systems: parsed.systems
1517
+ });
1518
+ }
1519
+ };
1520
+ const updateSceneTool = {
1521
+ name: "update_scene",
1522
+ description: "Replace the list of systems registered by this scene. Marker block and generated body are both regenerated.",
1523
+ inputSchema: zodToJsonSchema6(updateSceneInputSchema),
1524
+ handler: async (input) => {
1525
+ const parsedInput = updateSceneInputSchema.parse(input);
1526
+ const { parsed } = await readSceneFile(projectDirectory, parsedInput.name);
1527
+ if (parsedInput.setSystems !== void 0) {
1528
+ parsed.systems = parsedInput.setSystems;
1529
+ }
1530
+ const written = await writeSceneFile(projectDirectory, parsed);
1531
+ return formatToolResult6({
1532
+ sceneName: parsed.sceneBaseName,
1533
+ systems: parsed.systems,
1534
+ filePath: written
1535
+ });
1536
+ }
1537
+ };
1538
+ const deleteSceneTool = {
1539
+ name: "delete_scene",
1540
+ description: "Delete a scene file from src/scenes/.",
1541
+ inputSchema: zodToJsonSchema6(deleteSceneInputSchema),
1542
+ handler: async (input) => {
1543
+ const parsedInput = deleteSceneInputSchema.parse(input);
1544
+ const { absoluteFilePath, sceneBaseName } = resolveScenePaths(projectDirectory, parsedInput.name);
1545
+ if (!await fileSystemExtra6.pathExists(absoluteFilePath)) {
1546
+ throw new Error(`Scene file not found: ${absoluteFilePath}`);
1547
+ }
1548
+ await fileSystemExtra6.remove(absoluteFilePath);
1549
+ return formatToolResult6({ sceneName: sceneBaseName, deleted: absoluteFilePath });
1550
+ }
1551
+ };
1552
+ const listScenesTool = {
1553
+ name: "list_scenes",
1554
+ description: "List all scene files found under src/scenes/.",
1555
+ inputSchema: zodToJsonSchema6(listScenesInputSchema),
1556
+ handler: async (input) => {
1557
+ listScenesInputSchema.parse(input ?? {});
1558
+ const scenes = await listSceneFiles(projectDirectory);
1559
+ return formatToolResult6({
1560
+ scenes: scenes.map((scene) => ({
1561
+ name: scene.sceneBaseName,
1562
+ className: scene.sceneClassName,
1563
+ entityCount: scene.entities.length,
1564
+ systems: scene.systems
1565
+ }))
1566
+ });
1567
+ }
1568
+ };
1569
+ const setActiveSceneTool = {
1570
+ name: "set_active_scene",
1571
+ description: "Set the activeScene field in donut.json to the given scene name. Creates the field if missing.",
1572
+ inputSchema: zodToJsonSchema6(setActiveSceneInputSchema),
1573
+ handler: async (input) => {
1574
+ const parsedInput = setActiveSceneInputSchema.parse(input);
1575
+ const { sceneBaseName } = resolveScenePaths(projectDirectory, parsedInput.name);
1576
+ const manifestPath = resolveWithinProject(projectDirectory, "donut.json");
1577
+ if (!await fileSystemExtra6.pathExists(manifestPath)) {
1578
+ throw new Error(`donut.json not found at ${manifestPath}`);
1579
+ }
1580
+ const raw = await fileSystemExtra6.readFile(manifestPath, "utf8");
1581
+ const manifest = JSON.parse(raw);
1582
+ manifest["activeScene"] = sceneBaseName;
1583
+ await fileSystemExtra6.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
1584
+ return formatToolResult6({ activeScene: sceneBaseName, filePath: manifestPath });
1585
+ }
1586
+ };
1587
+ context.registerTool(createSceneTool);
1588
+ context.registerTool(readSceneTool);
1589
+ context.registerTool(updateSceneTool);
1590
+ context.registerTool(deleteSceneTool);
1591
+ context.registerTool(listScenesTool);
1592
+ context.registerTool(setActiveSceneTool);
1593
+ }
1594
+
1595
+ // ../mcp/dist/tools/entity-scene/index.js
1596
+ function registerEntitySceneTools(_server, context) {
1597
+ registerEntityTools(context);
1598
+ registerSceneTools(context);
1599
+ registerAssetTools(context);
1600
+ registerProjectTools(context);
1601
+ }
1602
+
1603
+ // ../mcp/dist/tools/quality/type-check.js
1604
+ import path6 from "path";
1605
+ import { spawn } from "child_process";
1606
+ import { createRequire } from "module";
1607
+ import fileSystemExtra7 from "fs-extra";
1608
+ import { z as z7 } from "zod";
1609
+ import { zodToJsonSchema as zodToJsonSchema7 } from "zod-to-json-schema";
1610
+ var compilerOutputOverride;
1611
+ function resolveTypeScriptCompiler(projectDirectory) {
1612
+ const projectLocal = path6.join(projectDirectory, "node_modules", "typescript", "bin", "tsc");
1613
+ if (fileSystemExtra7.existsSync(projectLocal)) {
1614
+ return projectLocal;
1615
+ }
1616
+ try {
1617
+ const requireFromHere = createRequire(import.meta.url);
1618
+ const typeScriptPackageJsonPath = requireFromHere.resolve("typescript/package.json");
1619
+ const typeScriptPackageDirectory = path6.dirname(typeScriptPackageJsonPath);
1620
+ const fallback = path6.join(typeScriptPackageDirectory, "bin", "tsc");
1621
+ if (fileSystemExtra7.existsSync(fallback)) {
1622
+ return fallback;
1623
+ }
1624
+ } catch {
1625
+ }
1626
+ return void 0;
1627
+ }
1628
+ function runProcess(command, argumentList, workingDirectory) {
1629
+ return new Promise((resolve) => {
1630
+ const child = spawn(command, argumentList, {
1631
+ cwd: workingDirectory,
1632
+ env: process.env
1633
+ });
1634
+ let stdout = "";
1635
+ let stderr = "";
1636
+ child.stdout.on("data", (chunk) => {
1637
+ stdout += chunk.toString("utf8");
1638
+ });
1639
+ child.stderr.on("data", (chunk) => {
1640
+ stderr += chunk.toString("utf8");
1641
+ });
1642
+ child.on("close", () => {
1643
+ resolve({ stdout, stderr });
1644
+ });
1645
+ child.on("error", () => {
1646
+ resolve({ stdout, stderr });
1647
+ });
1648
+ });
1649
+ }
1650
+ async function runTypeScriptCompiler(projectDirectory) {
1651
+ if (compilerOutputOverride !== void 0) {
1652
+ return { output: compilerOutputOverride };
1653
+ }
1654
+ const tsconfigPath = path6.join(projectDirectory, "tsconfig.json");
1655
+ if (!await fileSystemExtra7.pathExists(tsconfigPath)) {
1656
+ return { output: "", skippedReason: "tsconfig.json not found" };
1657
+ }
1658
+ const compilerPath = resolveTypeScriptCompiler(projectDirectory);
1659
+ if (compilerPath === void 0) {
1660
+ return { output: "", skippedReason: "unable to locate the TypeScript compiler" };
1661
+ }
1662
+ const { stdout, stderr } = await runProcess(process.execPath, [compilerPath, "--noEmit", "-p", tsconfigPath], projectDirectory);
1663
+ return { output: `${stdout}
1664
+ ${stderr}` };
1665
+ }
1666
+ function parseTypeScriptDiagnostics(output, projectDirectory) {
1667
+ const diagnosticLineRegex = /^([^\s(][^(]*?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm;
1668
+ const diagnostics = [];
1669
+ let match;
1670
+ while ((match = diagnosticLineRegex.exec(output)) !== null) {
1671
+ const [, rawFilePath, lineText, columnText, typeScriptCode, messageText] = match;
1672
+ const absoluteFilePath = path6.isAbsolute(rawFilePath) ? rawFilePath : path6.join(projectDirectory, rawFilePath);
1673
+ diagnostics.push({
1674
+ filePath: absoluteFilePath,
1675
+ line: Number.parseInt(lineText, 10),
1676
+ column: Number.parseInt(columnText, 10),
1677
+ message: `${typeScriptCode}: ${messageText.trim()}`,
1678
+ code: typeScriptCode
1679
+ });
1680
+ }
1681
+ return diagnostics;
1682
+ }
1683
+ var checkTypesFileInputSchema = z7.object({ filePath: z7.string().min(1) });
1684
+ var emptyInputSchema = z7.object({}).default({});
1685
+ function formatToolResult7(payload) {
1686
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1687
+ }
1688
+ function registerTypeCheckTools(context) {
1689
+ const projectDirectory = context.projectDirectory;
1690
+ const checkTypesTool = {
1691
+ name: "check_types",
1692
+ description: "Run `tsc --noEmit` against the project and return structured diagnostics for every type error discovered.",
1693
+ inputSchema: zodToJsonSchema7(emptyInputSchema),
1694
+ handler: async (input) => {
1695
+ emptyInputSchema.parse(input ?? {});
1696
+ const { output, skippedReason } = await runTypeScriptCompiler(projectDirectory);
1697
+ const errors = parseTypeScriptDiagnostics(output, projectDirectory);
1698
+ return formatToolResult7({
1699
+ errors,
1700
+ errorCount: errors.length,
1701
+ ...skippedReason !== void 0 ? { skippedReason } : {}
1702
+ });
1703
+ }
1704
+ };
1705
+ const checkTypesFileTool = {
1706
+ name: "check_types_file",
1707
+ description: "Run a project-wide type-check and filter diagnostics down to a single file (preserves tsconfig.json settings for accurate results).",
1708
+ inputSchema: zodToJsonSchema7(checkTypesFileInputSchema),
1709
+ handler: async (input) => {
1710
+ const parsed = checkTypesFileInputSchema.parse(input);
1711
+ const absoluteTargetPath = path6.isAbsolute(parsed.filePath) ? parsed.filePath : path6.resolve(projectDirectory, parsed.filePath);
1712
+ const { output, skippedReason } = await runTypeScriptCompiler(projectDirectory);
1713
+ const allDiagnostics = parseTypeScriptDiagnostics(output, projectDirectory);
1714
+ const errors = allDiagnostics.filter((diagnostic) => path6.resolve(diagnostic.filePath) === path6.resolve(absoluteTargetPath));
1715
+ return formatToolResult7({
1716
+ filePath: absoluteTargetPath,
1717
+ errors,
1718
+ errorCount: errors.length,
1719
+ ...skippedReason !== void 0 ? { skippedReason } : {}
1720
+ });
1721
+ }
1722
+ };
1723
+ context.registerTool(checkTypesTool);
1724
+ context.registerTool(checkTypesFileTool);
1725
+ }
1726
+
1727
+ // ../mcp/dist/tools/quality/lint.js
1728
+ import path7 from "path";
1729
+ import fileSystemExtra8 from "fs-extra";
1730
+ import { z as z8 } from "zod";
1731
+ import { zodToJsonSchema as zodToJsonSchema8 } from "zod-to-json-schema";
1732
+ var lintInputSchema = z8.object({ filePath: z8.string().min(1).optional() }).default({});
1733
+ var CONFIG_FILE_NAMES = [
1734
+ "eslint.config.js",
1735
+ "eslint.config.mjs",
1736
+ "eslint.config.cjs",
1737
+ "eslint.config.ts",
1738
+ ".eslintrc",
1739
+ ".eslintrc.js",
1740
+ ".eslintrc.cjs",
1741
+ ".eslintrc.json",
1742
+ ".eslintrc.yaml",
1743
+ ".eslintrc.yml"
1744
+ ];
1745
+ async function hasEslintConfig(projectDirectory) {
1746
+ for (const fileName of CONFIG_FILE_NAMES) {
1747
+ if (await fileSystemExtra8.pathExists(path7.join(projectDirectory, fileName))) {
1748
+ return true;
1749
+ }
1750
+ }
1751
+ const packageJsonPath = path7.join(projectDirectory, "package.json");
1752
+ if (await fileSystemExtra8.pathExists(packageJsonPath)) {
1753
+ try {
1754
+ const packageJson = await fileSystemExtra8.readJson(packageJsonPath);
1755
+ if (packageJson && typeof packageJson === "object" && "eslintConfig" in packageJson) {
1756
+ return true;
1757
+ }
1758
+ } catch {
1759
+ }
1760
+ }
1761
+ return false;
1762
+ }
1763
+ async function tryLoadEslint() {
1764
+ try {
1765
+ const moduleName = "eslint";
1766
+ const mod = await import(moduleName);
1767
+ return mod;
1768
+ } catch {
1769
+ return void 0;
1770
+ }
1771
+ }
1772
+ function normalizeResults(rawResults) {
1773
+ const errors = [];
1774
+ const warnings = [];
1775
+ const results = [];
1776
+ for (const entry of rawResults) {
1777
+ results.push({
1778
+ filePath: entry.filePath,
1779
+ errorCount: entry.errorCount,
1780
+ warningCount: entry.warningCount
1781
+ });
1782
+ for (const message of entry.messages) {
1783
+ const normalized = {
1784
+ filePath: entry.filePath,
1785
+ line: message.line ?? 1,
1786
+ column: message.column ?? 1,
1787
+ ruleId: message.ruleId,
1788
+ message: message.message,
1789
+ severity: message.severity === 2 ? "error" : "warning"
1790
+ };
1791
+ if (normalized.severity === "error") {
1792
+ errors.push(normalized);
1793
+ } else {
1794
+ warnings.push(normalized);
1795
+ }
1796
+ }
1797
+ }
1798
+ return { errors, warnings, results };
1799
+ }
1800
+ function formatToolResult8(payload) {
1801
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1802
+ }
1803
+ function resolveLintTargets(projectDirectory, filePath) {
1804
+ if (filePath === void 0) {
1805
+ return [path7.join(projectDirectory, "src")];
1806
+ }
1807
+ const absolute = path7.isAbsolute(filePath) ? filePath : path7.resolve(projectDirectory, filePath);
1808
+ return [absolute];
1809
+ }
1810
+ function registerLintTools(context) {
1811
+ const projectDirectory = context.projectDirectory;
1812
+ const lintTool = {
1813
+ name: "lint",
1814
+ description: "Run ESLint programmatically against the project (or a single file). Returns structured errors and warnings. If the project has no ESLint configuration or ESLint is not installed, the tool returns a skip warning.",
1815
+ inputSchema: zodToJsonSchema8(lintInputSchema),
1816
+ handler: async (input) => {
1817
+ const parsed = lintInputSchema.parse(input ?? {});
1818
+ if (!await hasEslintConfig(projectDirectory)) {
1819
+ return formatToolResult8({
1820
+ skipped: true,
1821
+ reason: "No ESLint configuration found at the project root",
1822
+ errors: [],
1823
+ warnings: [],
1824
+ results: []
1825
+ });
1826
+ }
1827
+ const eslintModule = await tryLoadEslint();
1828
+ if (eslintModule === void 0) {
1829
+ return formatToolResult8({
1830
+ skipped: true,
1831
+ reason: "ESLint package is not installed",
1832
+ errors: [],
1833
+ warnings: [],
1834
+ results: []
1835
+ });
1836
+ }
1837
+ const eslintInstance = new eslintModule.ESLint({ cwd: projectDirectory });
1838
+ const rawResults = await eslintInstance.lintFiles(resolveLintTargets(projectDirectory, parsed.filePath));
1839
+ const normalized = normalizeResults(rawResults);
1840
+ return formatToolResult8(normalized);
1841
+ }
1842
+ };
1843
+ const lintFixTool = {
1844
+ name: "lint_fix",
1845
+ description: "Run ESLint with auto-fix enabled. Writes fixes to disk and returns the number of auto-fixed messages plus any remaining errors and warnings.",
1846
+ inputSchema: zodToJsonSchema8(lintInputSchema),
1847
+ handler: async (input) => {
1848
+ const parsed = lintInputSchema.parse(input ?? {});
1849
+ if (!await hasEslintConfig(projectDirectory)) {
1850
+ return formatToolResult8({
1851
+ skipped: true,
1852
+ reason: "No ESLint configuration found at the project root",
1853
+ fixed: 0,
1854
+ remainingErrors: [],
1855
+ remainingWarnings: []
1856
+ });
1857
+ }
1858
+ const eslintModule = await tryLoadEslint();
1859
+ if (eslintModule === void 0) {
1860
+ return formatToolResult8({
1861
+ skipped: true,
1862
+ reason: "ESLint package is not installed",
1863
+ fixed: 0,
1864
+ remainingErrors: [],
1865
+ remainingWarnings: []
1866
+ });
1867
+ }
1868
+ const eslintInstance = new eslintModule.ESLint({
1869
+ cwd: projectDirectory,
1870
+ fix: true
1871
+ });
1872
+ const rawResults = await eslintInstance.lintFiles(resolveLintTargets(projectDirectory, parsed.filePath));
1873
+ let fixed = 0;
1874
+ for (const result of rawResults) {
1875
+ if (typeof result.output === "string") {
1876
+ await fileSystemExtra8.writeFile(result.filePath, result.output, "utf8");
1877
+ fixed += 1;
1878
+ }
1879
+ }
1880
+ const normalized = normalizeResults(rawResults);
1881
+ return formatToolResult8({
1882
+ fixed,
1883
+ remainingErrors: normalized.errors,
1884
+ remainingWarnings: normalized.warnings
1885
+ });
1886
+ }
1887
+ };
1888
+ context.registerTool(lintTool);
1889
+ context.registerTool(lintFixTool);
1890
+ }
1891
+
1892
+ // ../mcp/dist/tools/quality/imports-world.js
1893
+ import { z as z9 } from "zod";
1894
+ import { zodToJsonSchema as zodToJsonSchema9 } from "zod-to-json-schema";
1895
+
1896
+ // ../cli/src/commands/validate.ts
1897
+ import path8 from "path";
1898
+ import { spawn as spawn2 } from "child_process";
1899
+ import { createRequire as createRequire2 } from "module";
1900
+ import { fileURLToPath } from "url";
1901
+ import fileSystemExtra9 from "fs-extra";
1902
+ var REQUIRED_MANIFEST_FIELDS = ["name", "version", "rendererTarget", "donutEngineVersion"];
1903
+ var ALLOWED_RENDERER_TARGETS = /* @__PURE__ */ new Set(["pixi-2d", "three-3d"]);
1904
+ var GAME_LOGIC_FORBIDDEN_RENDERER_IMPORT_PREFIXES = [
1905
+ "@donut-games/engine/pixi",
1906
+ "@donut-games/engine/three",
1907
+ "@donut/pixi",
1908
+ "@donut/three"
1909
+ ];
1910
+ var WRONG_RENDERER_FORBIDDEN_IMPORT_BY_TARGET = {
1911
+ "pixi-2d": "@donut-games/engine/three",
1912
+ "three-3d": "@donut-games/engine/pixi"
1913
+ };
1914
+ var WRONG_RENDERER_ALLOWED_IMPORT_BY_TARGET = {
1915
+ "pixi-2d": "@donut-games/engine/pixi",
1916
+ "three-3d": "@donut-games/engine/three"
1917
+ };
1918
+ async function runValidation(options) {
1919
+ const { projectDirectory } = options;
1920
+ const errors = [];
1921
+ const warnings = [];
1922
+ const parsedManifest = await validateManifest(projectDirectory, errors);
1923
+ if (options.skipTypeCheck !== true) {
1924
+ await validateTypes(projectDirectory, errors);
1925
+ }
1926
+ const rendererTarget = parsedManifest?.rendererTarget;
1927
+ await validateImportGraph(projectDirectory, rendererTarget, errors);
1928
+ await validateAssetReferences(projectDirectory, errors);
1929
+ return { errors, warnings };
1930
+ }
1931
+ async function validateManifest(projectDirectory, errors) {
1932
+ const manifestPath = path8.join(projectDirectory, "donut.json");
1933
+ if (!await fileSystemExtra9.pathExists(manifestPath)) {
1934
+ errors.push({
1935
+ filePath: manifestPath,
1936
+ message: "donut.json is missing at the project root",
1937
+ code: "MANIFEST_INVALID"
1938
+ });
1939
+ return null;
1940
+ }
1941
+ let raw;
1942
+ try {
1943
+ raw = await fileSystemExtra9.readFile(manifestPath, "utf8");
1944
+ } catch (readError) {
1945
+ errors.push({
1946
+ filePath: manifestPath,
1947
+ message: `Unable to read donut.json: ${readError.message}`,
1948
+ code: "MANIFEST_INVALID"
1949
+ });
1950
+ return null;
1951
+ }
1952
+ let parsed;
1953
+ try {
1954
+ parsed = JSON.parse(raw);
1955
+ } catch (parseError) {
1956
+ errors.push({
1957
+ filePath: manifestPath,
1958
+ message: `donut.json is not valid JSON: ${parseError.message}`,
1959
+ code: "MANIFEST_INVALID"
1960
+ });
1961
+ return null;
1962
+ }
1963
+ let hasRequiredFieldError = false;
1964
+ for (const fieldName of REQUIRED_MANIFEST_FIELDS) {
1965
+ const value = parsed[fieldName];
1966
+ if (typeof value !== "string" || value.trim().length === 0) {
1967
+ errors.push({
1968
+ filePath: manifestPath,
1969
+ message: `donut.json missing required string field "${fieldName}"`,
1970
+ code: "MANIFEST_INVALID"
1971
+ });
1972
+ hasRequiredFieldError = true;
1973
+ }
1974
+ }
1975
+ const rendererTargetValue = parsed.rendererTarget;
1976
+ if (typeof rendererTargetValue === "string" && !ALLOWED_RENDERER_TARGETS.has(rendererTargetValue)) {
1977
+ errors.push({
1978
+ filePath: manifestPath,
1979
+ message: `donut.json "rendererTarget" must be one of pixi-2d, three-3d (got "${rendererTargetValue}")`,
1980
+ code: "MANIFEST_INVALID"
1981
+ });
1982
+ }
1983
+ if (hasRequiredFieldError) {
1984
+ return null;
1985
+ }
1986
+ return {
1987
+ name: parsed.name,
1988
+ version: parsed.version,
1989
+ rendererTarget: parsed.rendererTarget,
1990
+ donutEngineVersion: parsed.donutEngineVersion
1991
+ };
1992
+ }
1993
+ async function validateTypes(projectDirectory, errors) {
1994
+ const tsconfigPath = path8.join(projectDirectory, "tsconfig.json");
1995
+ if (!await fileSystemExtra9.pathExists(tsconfigPath)) {
1996
+ return;
1997
+ }
1998
+ const typeScriptCompilerPath = resolveTypeScriptCompiler2(projectDirectory);
1999
+ if (typeScriptCompilerPath === void 0) {
2000
+ errors.push({
2001
+ filePath: tsconfigPath,
2002
+ message: "Unable to locate a TypeScript compiler (tsc) to run the type-check",
2003
+ code: "TYPE_ERROR"
2004
+ });
2005
+ return;
2006
+ }
2007
+ const { stdout, stderr } = await runProcess2(
2008
+ process.execPath,
2009
+ [typeScriptCompilerPath, "--noEmit", "-p", tsconfigPath],
2010
+ projectDirectory
2011
+ );
2012
+ const combined = `${stdout}
2013
+ ${stderr}`;
2014
+ const diagnosticLineRegex = /^([^\s(][^(]*?)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/gm;
2015
+ let match;
2016
+ while ((match = diagnosticLineRegex.exec(combined)) !== null) {
2017
+ const [, rawFilePath, lineText, columnText, typeScriptCode, messageText] = match;
2018
+ const absoluteFilePath = path8.isAbsolute(rawFilePath) ? rawFilePath : path8.join(projectDirectory, rawFilePath);
2019
+ errors.push({
2020
+ filePath: absoluteFilePath,
2021
+ line: Number.parseInt(lineText, 10),
2022
+ column: Number.parseInt(columnText, 10),
2023
+ message: `${typeScriptCode}: ${messageText.trim()}`,
2024
+ code: "TYPE_ERROR"
2025
+ });
2026
+ }
2027
+ }
2028
+ function resolveTypeScriptCompiler2(projectDirectory) {
2029
+ const projectLocal = path8.join(
2030
+ projectDirectory,
2031
+ "node_modules",
2032
+ "typescript",
2033
+ "bin",
2034
+ "tsc"
2035
+ );
2036
+ if (fileSystemExtra9.existsSync(projectLocal)) {
2037
+ return projectLocal;
2038
+ }
2039
+ try {
2040
+ const requireFromHere = createRequire2(import.meta.url);
2041
+ const typeScriptPackageJsonPath = requireFromHere.resolve("typescript/package.json");
2042
+ const typeScriptPackageDirectory = path8.dirname(typeScriptPackageJsonPath);
2043
+ const fallback = path8.join(typeScriptPackageDirectory, "bin", "tsc");
2044
+ if (fileSystemExtra9.existsSync(fallback)) {
2045
+ return fallback;
2046
+ }
2047
+ } catch {
2048
+ }
2049
+ return void 0;
2050
+ }
2051
+ function runProcess2(command, argumentList, workingDirectory) {
2052
+ return new Promise((resolve) => {
2053
+ const child = spawn2(command, argumentList, {
2054
+ cwd: workingDirectory,
2055
+ env: process.env
2056
+ });
2057
+ let stdout = "";
2058
+ let stderr = "";
2059
+ child.stdout.on("data", (chunk) => {
2060
+ stdout += chunk.toString("utf8");
2061
+ });
2062
+ child.stderr.on("data", (chunk) => {
2063
+ stderr += chunk.toString("utf8");
2064
+ });
2065
+ child.on("close", (exitCode) => {
2066
+ resolve({ stdout, stderr, exitCode });
2067
+ });
2068
+ child.on("error", () => {
2069
+ resolve({ stdout, stderr, exitCode: null });
2070
+ });
2071
+ });
2072
+ }
2073
+ async function validateImportGraph(projectDirectory, rendererTarget, errors) {
2074
+ await validateGameLogicImports(projectDirectory, errors);
2075
+ await validateWrongRendererImports(projectDirectory, rendererTarget, errors);
2076
+ }
2077
+ async function validateGameLogicImports(projectDirectory, errors) {
2078
+ const gameLogicDirectories = [
2079
+ path8.join(projectDirectory, "src", "components"),
2080
+ path8.join(projectDirectory, "src", "systems")
2081
+ ];
2082
+ const importLineRegex = /^\s*import\s+[^;]*?from\s+['"]([^'"]+)['"]/;
2083
+ for (const directoryPath of gameLogicDirectories) {
2084
+ if (!await fileSystemExtra9.pathExists(directoryPath)) {
2085
+ continue;
2086
+ }
2087
+ const typeScriptFiles = await collectTypeScriptFiles3(directoryPath);
2088
+ for (const filePath of typeScriptFiles) {
2089
+ const contents = await fileSystemExtra9.readFile(filePath, "utf8");
2090
+ const lines = contents.split(/\r?\n/);
2091
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
2092
+ const lineText = lines[lineIndex];
2093
+ const matched = importLineRegex.exec(lineText);
2094
+ if (matched === null) {
2095
+ continue;
2096
+ }
2097
+ const importedSpecifier = matched[1];
2098
+ if (GAME_LOGIC_FORBIDDEN_RENDERER_IMPORT_PREFIXES.some(
2099
+ (disallowedPrefix) => importedSpecifier === disallowedPrefix || importedSpecifier.startsWith(`${disallowedPrefix}/`)
2100
+ )) {
2101
+ errors.push({
2102
+ filePath,
2103
+ line: lineIndex + 1,
2104
+ 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)`,
2105
+ code: "RENDERER_IMPORT_IN_GAME_LOGIC"
2106
+ });
2107
+ }
2108
+ }
2109
+ }
2110
+ }
2111
+ }
2112
+ async function validateWrongRendererImports(projectDirectory, rendererTarget, errors) {
2113
+ if (rendererTarget === void 0) {
2114
+ return;
2115
+ }
2116
+ const wrongRendererImportSpecifierPrefix = WRONG_RENDERER_FORBIDDEN_IMPORT_BY_TARGET[rendererTarget];
2117
+ const allowedRendererImportSpecifierPrefix = WRONG_RENDERER_ALLOWED_IMPORT_BY_TARGET[rendererTarget];
2118
+ if (wrongRendererImportSpecifierPrefix === void 0 || allowedRendererImportSpecifierPrefix === void 0) {
2119
+ return;
2120
+ }
2121
+ const sourceRoot = path8.join(projectDirectory, "src");
2122
+ if (!await fileSystemExtra9.pathExists(sourceRoot)) {
2123
+ return;
2124
+ }
2125
+ const importLineRegex = /^\s*import\s+[^;]*?from\s+['"]([^'"]+)['"]/;
2126
+ const typeScriptFiles = await collectTypeScriptFiles3(sourceRoot);
2127
+ for (const filePath of typeScriptFiles) {
2128
+ const contents = await fileSystemExtra9.readFile(filePath, "utf8");
2129
+ const lines = contents.split(/\r?\n/);
2130
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
2131
+ const lineText = lines[lineIndex];
2132
+ const matched = importLineRegex.exec(lineText);
2133
+ if (matched === null) {
2134
+ continue;
2135
+ }
2136
+ const importedSpecifier = matched[1];
2137
+ if (importedSpecifier === wrongRendererImportSpecifierPrefix || importedSpecifier.startsWith(`${wrongRendererImportSpecifierPrefix}/`)) {
2138
+ errors.push({
2139
+ filePath,
2140
+ line: lineIndex + 1,
2141
+ 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.`,
2142
+ code: "WRONG_RENDERER_IMPORT"
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ }
2148
+ async function collectTypeScriptFiles3(directoryPath) {
2149
+ const collected = [];
2150
+ const entries = await fileSystemExtra9.readdir(directoryPath, { withFileTypes: true });
2151
+ for (const entry of entries) {
2152
+ const entryPath = path8.join(directoryPath, entry.name);
2153
+ if (entry.isDirectory()) {
2154
+ collected.push(...await collectTypeScriptFiles3(entryPath));
2155
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) {
2156
+ collected.push(entryPath);
2157
+ }
2158
+ }
2159
+ return collected;
2160
+ }
2161
+ async function validateAssetReferences(projectDirectory, errors) {
2162
+ const sourceRoot = path8.join(projectDirectory, "src");
2163
+ const assetsRoot = path8.join(projectDirectory, "assets");
2164
+ if (!await fileSystemExtra9.pathExists(sourceRoot)) {
2165
+ return;
2166
+ }
2167
+ const typeScriptFiles = await collectTypeScriptFiles3(sourceRoot);
2168
+ const texturePathRegex = /texturePath\s*(?::[^='"]*)?=?\s*['"]([^'"]*)['"]/g;
2169
+ const assetsLiteralRegex = /['"]assets\/([^'"]+)['"]/g;
2170
+ for (const filePath of typeScriptFiles) {
2171
+ const contents = await fileSystemExtra9.readFile(filePath, "utf8");
2172
+ const lines = contents.split(/\r?\n/);
2173
+ const referencedAssets = [];
2174
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
2175
+ const lineText = lines[lineIndex];
2176
+ texturePathRegex.lastIndex = 0;
2177
+ let textureMatch;
2178
+ while ((textureMatch = texturePathRegex.exec(lineText)) !== null) {
2179
+ const referenced = textureMatch[1];
2180
+ if (referenced.length === 0) {
2181
+ continue;
2182
+ }
2183
+ referencedAssets.push({ relativePath: referenced, line: lineIndex + 1 });
2184
+ }
2185
+ assetsLiteralRegex.lastIndex = 0;
2186
+ let assetsMatch;
2187
+ while ((assetsMatch = assetsLiteralRegex.exec(lineText)) !== null) {
2188
+ referencedAssets.push({
2189
+ relativePath: assetsMatch[1],
2190
+ line: lineIndex + 1
2191
+ });
2192
+ }
2193
+ }
2194
+ for (const { relativePath, line } of referencedAssets) {
2195
+ const normalized = relativePath.startsWith("assets/") ? relativePath.slice("assets/".length) : relativePath;
2196
+ const absoluteAssetPath = path8.join(assetsRoot, normalized);
2197
+ if (!await fileSystemExtra9.pathExists(absoluteAssetPath)) {
2198
+ errors.push({
2199
+ filePath,
2200
+ line,
2201
+ message: `Referenced asset "${relativePath}" not found under assets/`,
2202
+ code: "MISSING_ASSET"
2203
+ });
2204
+ }
2205
+ }
2206
+ }
2207
+ }
2208
+
2209
+ // ../mcp/dist/tools/quality/imports-world.js
2210
+ var emptyInputSchema2 = z9.object({}).default({});
2211
+ function formatToolResult9(payload) {
2212
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2213
+ }
2214
+ function registerImportsAndWorldTools(context) {
2215
+ const projectDirectory = context.projectDirectory;
2216
+ const checkImportsTool = {
2217
+ name: "check_imports",
2218
+ description: "Scan src/components and src/systems for renderer imports that break the game-logic portability contract. Returns findings with the RENDERER_IMPORT_IN_GAME_LOGIC code.",
2219
+ inputSchema: zodToJsonSchema9(emptyInputSchema2),
2220
+ handler: async (input) => {
2221
+ emptyInputSchema2.parse(input ?? {});
2222
+ const report = await runValidation({
2223
+ projectDirectory,
2224
+ skipTypeCheck: true
2225
+ });
2226
+ const findings = report.errors.filter((error) => error.code === "RENDERER_IMPORT_IN_GAME_LOGIC");
2227
+ return formatToolResult9({
2228
+ findings,
2229
+ findingCount: findings.length
2230
+ });
2231
+ }
2232
+ };
2233
+ const validateWorldTool = {
2234
+ name: "validate_world",
2235
+ description: "Run the full project validation pipeline (manifest, types, imports, asset references) via @donut/cli and return the complete report grouped by error code.",
2236
+ inputSchema: zodToJsonSchema9(z9.object({ skipTypeCheck: z9.boolean().optional() }).default({})),
2237
+ handler: async (input) => {
2238
+ const parsed = z9.object({ skipTypeCheck: z9.boolean().optional() }).default({}).parse(input ?? {});
2239
+ const report = await runValidation({
2240
+ projectDirectory,
2241
+ ...parsed.skipTypeCheck !== void 0 ? { skipTypeCheck: parsed.skipTypeCheck } : {}
2242
+ });
2243
+ const groupByCode = (findings) => {
2244
+ const grouped = {};
2245
+ for (const finding of findings) {
2246
+ const bucket = grouped[finding.code] ?? [];
2247
+ bucket.push(finding);
2248
+ grouped[finding.code] = bucket;
2249
+ }
2250
+ return grouped;
2251
+ };
2252
+ return formatToolResult9({
2253
+ errors: report.errors,
2254
+ warnings: report.warnings,
2255
+ errorsByCode: groupByCode(report.errors),
2256
+ warningsByCode: groupByCode(report.warnings),
2257
+ errorCount: report.errors.length,
2258
+ warningCount: report.warnings.length
2259
+ });
2260
+ }
2261
+ };
2262
+ context.registerTool(checkImportsTool);
2263
+ context.registerTool(validateWorldTool);
2264
+ }
2265
+
2266
+ // ../mcp/dist/tools/quality/index.js
2267
+ function registerQualityTools(_server, context) {
2268
+ registerTypeCheckTools(context);
2269
+ registerLintTools(context);
2270
+ registerImportsAndWorldTools(context);
2271
+ }
2272
+
2273
+ // ../mcp/dist/tools/runtime/tools.js
2274
+ import { z as z10 } from "zod";
2275
+ import { zodToJsonSchema as zodToJsonSchema10 } from "zod-to-json-schema";
2276
+
2277
+ // ../mcp/dist/tools/runtime/runtime-session.js
2278
+ import { spawn as spawn3 } from "child_process";
2279
+ import fileSystem from "fs";
2280
+ import path9 from "path";
2281
+ var currentSession;
2282
+ async function defaultLaunchBrowser() {
2283
+ const playwright = await import("playwright");
2284
+ const browser = await playwright.chromium.launch({ headless: true });
2285
+ const page = await browser.newPage();
2286
+ return { browser, page };
2287
+ }
2288
+ function detectMonorepoRoot(startDirectory) {
2289
+ let currentDirectory = path9.resolve(startDirectory);
2290
+ for (let depth = 0; depth < 8; depth++) {
2291
+ if (fileSystem.existsSync(path9.join(currentDirectory, "pnpm-workspace.yaml"))) {
2292
+ return currentDirectory;
2293
+ }
2294
+ const parentDirectory = path9.dirname(currentDirectory);
2295
+ if (parentDirectory === currentDirectory) {
2296
+ return void 0;
2297
+ }
2298
+ currentDirectory = parentDirectory;
2299
+ }
2300
+ return void 0;
2301
+ }
2302
+ async function startRuntimeSession(options) {
2303
+ if (currentSession !== void 0) {
2304
+ throw new Error("Runtime session is already running; call stop_game first.");
2305
+ }
2306
+ const monorepoRoot = detectMonorepoRoot(options.projectDirectory);
2307
+ const cliDistEntry = monorepoRoot !== void 0 ? path9.join(monorepoRoot, "packages", "cli", "dist", "index.js") : void 0;
2308
+ const cliSourceEntry = monorepoRoot !== void 0 ? path9.join(monorepoRoot, "packages", "cli", "src", "index.ts") : void 0;
2309
+ let childProcess;
2310
+ if (cliDistEntry !== void 0 && fileSystem.existsSync(cliDistEntry)) {
2311
+ childProcess = spawn3("node", [cliDistEntry, "dev", "--no-open", "--port", String(options.port)], { cwd: options.projectDirectory, stdio: ["ignore", "pipe", "pipe"] });
2312
+ } else if (cliSourceEntry !== void 0) {
2313
+ childProcess = spawn3("node", [
2314
+ "--import",
2315
+ "tsx",
2316
+ cliSourceEntry,
2317
+ "dev",
2318
+ "--no-open",
2319
+ "--port",
2320
+ String(options.port)
2321
+ ], { cwd: monorepoRoot, stdio: ["ignore", "pipe", "pipe"] });
2322
+ } else {
2323
+ childProcess = spawn3("donut", ["dev", "--no-open", "--port", String(options.port)], { cwd: options.projectDirectory, stdio: ["ignore", "pipe", "pipe"] });
2324
+ }
2325
+ const url = await new Promise((resolvePromise, rejectPromise) => {
2326
+ const timeoutHandle = setTimeout(() => {
2327
+ rejectPromise(new Error("Timed out waiting for dev server to start (20s)."));
2328
+ }, 2e4);
2329
+ const handleData = (chunk) => {
2330
+ const text = chunk.toString();
2331
+ const match = text.match(/Dev server ready at (https?:\/\/[^\s]+)/);
2332
+ if (match !== null && match[1] !== void 0) {
2333
+ clearTimeout(timeoutHandle);
2334
+ resolvePromise(match[1].replace(/\/$/, ""));
2335
+ }
2336
+ };
2337
+ childProcess.stdout?.on("data", handleData);
2338
+ childProcess.stderr?.on("data", handleData);
2339
+ childProcess.on("error", (error) => {
2340
+ clearTimeout(timeoutHandle);
2341
+ rejectPromise(error);
2342
+ });
2343
+ childProcess.on("exit", (code) => {
2344
+ clearTimeout(timeoutHandle);
2345
+ rejectPromise(new Error(`Dev server exited before becoming ready (code ${code}).`));
2346
+ });
2347
+ });
2348
+ const launchBrowser = options.launchBrowser ?? defaultLaunchBrowser;
2349
+ const { browser, page } = await launchBrowser();
2350
+ await page.goto(url);
2351
+ const session = {
2352
+ url,
2353
+ pid: childProcess.pid ?? -1,
2354
+ process: childProcess,
2355
+ browser,
2356
+ page
2357
+ };
2358
+ currentSession = session;
2359
+ return session;
2360
+ }
2361
+ async function stopRuntimeSession() {
2362
+ const session = currentSession;
2363
+ if (session === void 0) {
2364
+ return false;
2365
+ }
2366
+ currentSession = void 0;
2367
+ try {
2368
+ await session.browser.close();
2369
+ } catch {
2370
+ }
2371
+ if (!session.process.killed) {
2372
+ session.process.kill("SIGTERM");
2373
+ }
2374
+ return true;
2375
+ }
2376
+ function requireRuntimeSession() {
2377
+ const session = currentSession;
2378
+ if (session === void 0) {
2379
+ throw new Error("No runtime session is running. Call start_game first.");
2380
+ }
2381
+ return session;
2382
+ }
2383
+
2384
+ // ../mcp/dist/tools/runtime/tools.js
2385
+ var startGameInputSchema = z10.object({
2386
+ port: z10.number().int().positive().max(65535).default(5173)
2387
+ });
2388
+ var restartGameInputSchema = z10.object({
2389
+ port: z10.number().int().positive().max(65535).default(5173)
2390
+ });
2391
+ var emptyInputSchema3 = z10.object({}).default({});
2392
+ var takeScreenshotAfterInputSchema = z10.object({
2393
+ delayMilliseconds: z10.number().int().nonnegative().max(6e4)
2394
+ });
2395
+ var readConsoleLogsInputSchema = z10.object({
2396
+ count: z10.number().int().positive().max(200).default(50),
2397
+ level: z10.enum(["log", "warn", "error"]).optional()
2398
+ });
2399
+ var sendInputActionInputSchema = z10.union([
2400
+ z10.object({
2401
+ actionName: z10.string().min(1),
2402
+ value: z10.number().default(1)
2403
+ }),
2404
+ z10.object({
2405
+ actionName: z10.string().min(1),
2406
+ phase: z10.enum(["start", "end"]),
2407
+ value: z10.number().default(1)
2408
+ })
2409
+ ]);
2410
+ var getEntityStateInputSchema = z10.object({ entityName: z10.string().min(1) });
2411
+ function formatTextResult(payload) {
2412
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2413
+ }
2414
+ function delay(milliseconds) {
2415
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
2416
+ }
2417
+ function buildStartGameTool(context) {
2418
+ return {
2419
+ name: "start_game",
2420
+ description: "Spawn the Donut dev server as a child process and open a headless browser pointed at it. Fails if a session is already running.",
2421
+ inputSchema: zodToJsonSchema10(startGameInputSchema),
2422
+ handler: async (input) => {
2423
+ const parsed = startGameInputSchema.parse(input ?? {});
2424
+ const session = await startRuntimeSession({
2425
+ projectDirectory: context.projectDirectory,
2426
+ port: parsed.port
2427
+ });
2428
+ return formatTextResult({ url: session.url, pid: session.pid });
2429
+ }
2430
+ };
2431
+ }
2432
+ function buildStopGameTool() {
2433
+ return {
2434
+ name: "stop_game",
2435
+ description: "Close the headless browser and terminate the dev server process.",
2436
+ inputSchema: zodToJsonSchema10(emptyInputSchema3),
2437
+ handler: async () => {
2438
+ const stopped = await stopRuntimeSession();
2439
+ return formatTextResult({ stopped });
2440
+ }
2441
+ };
2442
+ }
2443
+ function buildRestartGameTool(context) {
2444
+ return {
2445
+ name: "restart_game",
2446
+ description: "Stop any running dev session, then start a fresh one.",
2447
+ inputSchema: zodToJsonSchema10(restartGameInputSchema),
2448
+ handler: async (input) => {
2449
+ const parsed = restartGameInputSchema.parse(input ?? {});
2450
+ await stopRuntimeSession();
2451
+ const session = await startRuntimeSession({
2452
+ projectDirectory: context.projectDirectory,
2453
+ port: parsed.port
2454
+ });
2455
+ return formatTextResult({ url: session.url, pid: session.pid });
2456
+ }
2457
+ };
2458
+ }
2459
+ function buildTakeScreenshotTool() {
2460
+ return {
2461
+ name: "take_screenshot",
2462
+ description: "Capture the contents of the #game-canvas element as a base64 PNG. Requires a running runtime session.",
2463
+ inputSchema: zodToJsonSchema10(emptyInputSchema3),
2464
+ handler: async () => {
2465
+ const session = requireRuntimeSession();
2466
+ const buffer = await session.page.locator("#game-canvas").screenshot();
2467
+ const base64 = buffer.toString("base64");
2468
+ return {
2469
+ content: [{ type: "image", data: base64, mimeType: "image/png" }]
2470
+ };
2471
+ }
2472
+ };
2473
+ }
2474
+ function buildTakeScreenshotAfterTool() {
2475
+ return {
2476
+ name: "take_screenshot_after",
2477
+ description: "Wait `delayMilliseconds`, then capture a screenshot of #game-canvas.",
2478
+ inputSchema: zodToJsonSchema10(takeScreenshotAfterInputSchema),
2479
+ handler: async (input) => {
2480
+ const parsed = takeScreenshotAfterInputSchema.parse(input);
2481
+ const session = requireRuntimeSession();
2482
+ await delay(parsed.delayMilliseconds);
2483
+ const buffer = await session.page.locator("#game-canvas").screenshot();
2484
+ const base64 = buffer.toString("base64");
2485
+ return {
2486
+ content: [{ type: "image", data: base64, mimeType: "image/png" }]
2487
+ };
2488
+ }
2489
+ };
2490
+ }
2491
+ function buildReadConsoleLogsTool() {
2492
+ return {
2493
+ name: "read_console_logs",
2494
+ description: "Return the most recent console entries captured by the runtime bridge, optionally filtered by level.",
2495
+ inputSchema: zodToJsonSchema10(readConsoleLogsInputSchema),
2496
+ handler: async (input) => {
2497
+ const parsed = readConsoleLogsInputSchema.parse(input ?? {});
2498
+ const session = requireRuntimeSession();
2499
+ const buffer = await session.page.evaluate((() => {
2500
+ const runtime = window.__donutRuntime;
2501
+ return runtime?.consoleBuffer ?? [];
2502
+ }));
2503
+ const filtered = parsed.level === void 0 ? buffer : buffer.filter((entry) => entry.level === parsed.level);
2504
+ const tail = filtered.slice(Math.max(0, filtered.length - parsed.count));
2505
+ return formatTextResult({ entries: tail });
2506
+ }
2507
+ };
2508
+ }
2509
+ function buildReadPerformanceMetricsTool() {
2510
+ return {
2511
+ name: "read_performance_metrics",
2512
+ description: "Return live FPS, last-frame duration, entity count, and per-system update timings captured by the runtime bridge.",
2513
+ inputSchema: zodToJsonSchema10(emptyInputSchema3),
2514
+ handler: async () => {
2515
+ const session = requireRuntimeSession();
2516
+ const metrics = await session.page.evaluate((() => {
2517
+ const runtime = window.__donutRuntime;
2518
+ if (runtime === void 0) {
2519
+ return {
2520
+ fps: 0,
2521
+ frameTimeMilliseconds: 0,
2522
+ entityCount: 0,
2523
+ systemTimings: {}
2524
+ };
2525
+ }
2526
+ return runtime.getMetrics();
2527
+ }));
2528
+ return formatTextResult(metrics);
2529
+ }
2530
+ };
2531
+ }
2532
+ function buildSendInputActionTool() {
2533
+ return {
2534
+ name: "send_input_action",
2535
+ description: "Dispatch a synthetic CTRLLR action to the running game. Without a `phase`, sends a start event and an end event one frame later. With a `phase`, dispatches just that single event.",
2536
+ inputSchema: zodToJsonSchema10(sendInputActionInputSchema),
2537
+ handler: async (input) => {
2538
+ const parsed = sendInputActionInputSchema.parse(input);
2539
+ const session = requireRuntimeSession();
2540
+ await session.page.evaluate(((argument) => {
2541
+ const runtime = window.__donutRuntime;
2542
+ runtime?.triggerAction(argument.actionName, argument.value, argument.phase);
2543
+ }), {
2544
+ actionName: parsed.actionName,
2545
+ value: parsed.value,
2546
+ phase: "phase" in parsed ? parsed.phase : void 0
2547
+ });
2548
+ return formatTextResult({ dispatched: parsed.actionName });
2549
+ }
2550
+ };
2551
+ }
2552
+ function buildGetEntityStateTool() {
2553
+ return {
2554
+ name: "get_entity_state",
2555
+ description: "Look up an entity by name and return its tags plus each component serialized via `Component.serialize()`.",
2556
+ inputSchema: zodToJsonSchema10(getEntityStateInputSchema),
2557
+ handler: async (input) => {
2558
+ const parsed = getEntityStateInputSchema.parse(input);
2559
+ const session = requireRuntimeSession();
2560
+ const state = await session.page.evaluate(((entityName) => {
2561
+ const runtime = window.__donutRuntime;
2562
+ return runtime?.getEntityState(entityName);
2563
+ }), parsed.entityName);
2564
+ if (state === void 0 || state === null) {
2565
+ throw new Error(`Entity not found: ${parsed.entityName}`);
2566
+ }
2567
+ return formatTextResult(state);
2568
+ }
2569
+ };
2570
+ }
2571
+ function buildListActiveEntitiesTool() {
2572
+ return {
2573
+ name: "list_active_entities",
2574
+ description: "List every entity currently in the world, with identifier, name, tags, and component type names.",
2575
+ inputSchema: zodToJsonSchema10(emptyInputSchema3),
2576
+ handler: async () => {
2577
+ const session = requireRuntimeSession();
2578
+ const entities = await session.page.evaluate((() => {
2579
+ const runtime = window.__donutRuntime;
2580
+ return runtime?.getEntities() ?? [];
2581
+ }));
2582
+ return formatTextResult({ entities });
2583
+ }
2584
+ };
2585
+ }
2586
+ function registerRuntimeControlTools(context) {
2587
+ context.registerTool(buildStartGameTool(context));
2588
+ context.registerTool(buildStopGameTool());
2589
+ context.registerTool(buildRestartGameTool(context));
2590
+ context.registerTool(buildTakeScreenshotTool());
2591
+ context.registerTool(buildTakeScreenshotAfterTool());
2592
+ context.registerTool(buildReadConsoleLogsTool());
2593
+ context.registerTool(buildReadPerformanceMetricsTool());
2594
+ context.registerTool(buildSendInputActionTool());
2595
+ context.registerTool(buildGetEntityStateTool());
2596
+ context.registerTool(buildListActiveEntitiesTool());
2597
+ }
2598
+
2599
+ // ../mcp/dist/tools/runtime/index.js
2600
+ function registerRuntimeTools(_server, context) {
2601
+ registerRuntimeControlTools(context);
2602
+ }
2603
+
2604
+ // ../mcp/dist/index.js
2605
+ async function startMcpServer() {
2606
+ const projectDirectory = path10.resolve(process.env["DONUT_PROJECT_DIRECTORY"] ?? process.cwd());
2607
+ const registeredTools = [];
2608
+ const toolContext = {
2609
+ projectDirectory,
2610
+ registerTool(definition) {
2611
+ const alreadyRegistered = registeredTools.some((existing) => existing.name === definition.name);
2612
+ if (alreadyRegistered) {
2613
+ throw new Error(`Duplicate MCP tool registration: ${definition.name}`);
2614
+ }
2615
+ registeredTools.push(definition);
2616
+ }
2617
+ };
2618
+ const server = new Server({ name: "donut-mcp", version: "0.0.1" }, { capabilities: { tools: {} } });
2619
+ registerComponentSystemTools(server, toolContext);
2620
+ registerEntitySceneTools(server, toolContext);
2621
+ registerQualityTools(server, toolContext);
2622
+ registerRuntimeTools(server, toolContext);
2623
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2624
+ tools: registeredTools.map((tool) => ({
2625
+ name: tool.name,
2626
+ description: tool.description,
2627
+ inputSchema: tool.inputSchema
2628
+ }))
2629
+ }));
2630
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2631
+ const toolName = request.params.name;
2632
+ const tool = registeredTools.find((candidate) => candidate.name === toolName);
2633
+ if (tool === void 0) {
2634
+ throw new Error(`Unknown tool: ${toolName}`);
2635
+ }
2636
+ return tool.handler(request.params.arguments ?? {});
2637
+ });
2638
+ const transport = new StdioServerTransport();
2639
+ await server.connect(transport);
2640
+ }
2641
+ var isRunningAsMain = import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("/donut-mcp") === true || process.argv[1]?.endsWith("\\donut-mcp") === true || process.argv[1]?.endsWith("mcp/src/index.ts") === true || process.argv[1]?.endsWith("mcp/dist/index.js") === true;
2642
+ if (isRunningAsMain) {
2643
+ startMcpServer().catch((error) => {
2644
+ console.error(error);
2645
+ process.exit(1);
2646
+ });
2647
+ }
2648
+
2649
+ // src/donut-mcp.ts
2650
+ startMcpServer().catch((error) => {
2651
+ console.error(error);
2652
+ process.exit(1);
2653
+ });
2654
+ //# sourceMappingURL=donut-mcp.js.map