@waica/cli 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +42 -37
  2. package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-DfLN2qUF.js} +1 -1
  3. package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BM-wWcXR.js} +1 -1
  4. package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-C68SV5M9.js} +1 -1
  5. package/dist/editor/assets/{html-BrjX1TJ1.js → html-dQgLbIki.js} +1 -1
  6. package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-Dx91fUdd.js} +1 -1
  7. package/dist/editor/assets/{index-eTEBznqW.js → index-592ABIaz.js} +7154 -3186
  8. package/dist/editor/assets/{index-Btzc6zxr.css → index-Cy5pmBhN.css} +1 -1
  9. package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BmsSq5ka.js} +1 -1
  10. package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CWoPtX2M.js} +1 -1
  11. package/dist/editor/assets/{liquid-UM67NCct.js → liquid-DwnJlNJZ.js} +1 -1
  12. package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DaLkvlR1.js} +1 -1
  13. package/dist/editor/assets/{python-BhLrcAva.js → python-Cs5X5zFp.js} +1 -1
  14. package/dist/editor/assets/{razor-DcG373mQ.js → razor-PTwWoRk8.js} +1 -1
  15. package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-BqUQdgsR.js} +1 -1
  16. package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BsJ7r2RJ.js} +1 -1
  17. package/dist/editor/assets/{xml-22dL0oCq.js → xml-DyyoPey4.js} +1 -1
  18. package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-Cjqtlp29.js} +1 -1
  19. package/dist/editor/index.html +2 -2
  20. package/dist/mcp/component-metadata.js +23 -0
  21. package/dist/mcp/introspection.js +4 -14
  22. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/prefabs.js +7 -1
  23. package/dist/mcp/node_modules/@waica/archetype-platformer/dist/registry-data.js +3 -1
  24. package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
  25. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.d.ts +1 -0
  26. package/dist/mcp/node_modules/@waica/behaviors/dist/chaser.js +1 -0
  27. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.d.ts +1 -0
  28. package/dist/mcp/node_modules/@waica/behaviors/dist/collectible.js +1 -1
  29. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.d.ts +42 -1
  30. package/dist/mcp/node_modules/@waica/behaviors/dist/hazard.js +49 -3
  31. package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +82 -0
  32. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +147 -0
  33. package/dist/mcp/node_modules/@waica/behaviors/dist/index.d.ts +2 -0
  34. package/dist/mcp/node_modules/@waica/behaviors/dist/index.js +2 -0
  35. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.d.ts +1 -0
  36. package/dist/mcp/node_modules/@waica/behaviors/dist/lifetime.js +1 -0
  37. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +30 -0
  38. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +38 -0
  39. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.d.ts +1 -0
  40. package/dist/mcp/node_modules/@waica/behaviors/dist/patrol.js +1 -0
  41. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.d.ts +1 -0
  42. package/dist/mcp/node_modules/@waica/behaviors/dist/platformer-motor.js +10 -0
  43. package/dist/mcp/node_modules/@waica/behaviors/dist/player-states.js +26 -0
  44. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.d.ts +5 -13
  45. package/dist/mcp/node_modules/@waica/behaviors/dist/respawnable.js +5 -11
  46. package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
  47. package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
  48. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.d.ts +11 -0
  49. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
  50. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
  51. package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
  52. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +15 -1
  53. package/dist/mcp/node_modules/@waica/engine/dist/component.js +2 -0
  54. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +2 -0
  55. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +11 -0
  56. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
  57. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
  58. package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
  59. package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
  60. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +8 -1
  61. package/dist/mcp/node_modules/@waica/engine/dist/index.js +4 -0
  62. package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
  63. package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
  64. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
  65. package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
  66. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
  67. package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
  68. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
  69. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
  70. package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
  71. package/dist/mcp/package-resolver.js +10 -2
  72. package/dist/mcp/project-component-loader.js +553 -0
  73. package/dist/mcp/project-component-runner.js +237 -0
  74. package/dist/mcp/runtime-browser.js +336 -0
  75. package/dist/mcp/runtime-dev-server.js +273 -0
  76. package/dist/mcp/runtime-preflight.js +249 -0
  77. package/dist/mcp/runtime-service.js +8 -0
  78. package/dist/mcp/runtime-session-manager.js +265 -0
  79. package/dist/mcp/scaffolds.js +126 -6
  80. package/dist/mcp/server.js +330 -9
  81. package/dist/mcp/template/src/characters/player.character.json +29 -2
  82. package/dist/mcp/template/src/characters/slime.character.json +6 -0
  83. package/dist/mcp/template/src/main.ts +6 -1
  84. package/dist/mcp/validation.js +240 -24
  85. package/package.json +3 -2
@@ -15,12 +15,18 @@ export class PackageUnavailableError extends Error {
15
15
  const BUNDLED_SPECIFIERS = new Set([
16
16
  '@waica/engine',
17
17
  '@waica/behaviors',
18
+ '@waica/archetype-platformer',
18
19
  '@waica/archetype-platformer/manifest',
19
20
  ]);
20
21
  const requireBundled = createRequire(import.meta.url);
22
+ /** Node resolution anchored at a game project rather than the MCP host process. */
23
+ export function projectAnchoredRequire(projectPath) {
24
+ return createRequire(path.join(projectPath, 'package.json'));
25
+ }
21
26
  const BUILT_ENTRY_BY_SPECIFIER = {
22
27
  '@waica/engine': 'dist/index.js',
23
28
  '@waica/behaviors': 'dist/index.js',
29
+ '@waica/archetype-platformer': 'dist/index.js',
24
30
  '@waica/archetype-platformer/manifest': 'dist/manifest.js',
25
31
  };
26
32
  function specifierFor(packageName, subpath) {
@@ -117,13 +123,15 @@ async function loadBundledModule(specifier, packageRoot) {
117
123
  return import('@waica/engine');
118
124
  case '@waica/behaviors':
119
125
  return import('@waica/behaviors');
126
+ case '@waica/archetype-platformer':
127
+ return import('@waica/archetype-platformer');
120
128
  case '@waica/archetype-platformer/manifest':
121
129
  return import('@waica/archetype-platformer/manifest');
122
130
  default:
123
131
  throw new PackageUnavailableError(specifier);
124
132
  }
125
133
  }
126
- function causeText(error) {
134
+ export function causeText(error) {
127
135
  if (!(error instanceof Error))
128
136
  return String(error);
129
137
  const code = error.code;
@@ -150,7 +158,7 @@ export class PackageResolver {
150
158
  constructor(projectPath) {
151
159
  this.projectPath = projectPath;
152
160
  if (projectPath) {
153
- this.projectRequire = createRequire(path.join(projectPath, 'package.json'));
161
+ this.projectRequire = projectAnchoredRequire(projectPath);
154
162
  this.projectManifestUsable = projectPackageJsonUsable(projectPath);
155
163
  }
156
164
  }
@@ -0,0 +1,553 @@
1
+ import { execFile, fork, } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, rmSync } from 'node:fs';
4
+ import { copyFile, cp, mkdir, mkdtemp } from 'node:fs/promises';
5
+ import * as nodeModule from 'node:module';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { promisify } from 'node:util';
10
+ import { directFiles } from './project-path.js';
11
+ export const PROJECT_COMPONENT_PROTOCOL_VERSION = 1;
12
+ export const PROJECT_COMPONENT_DEADLINE_MS = 5_000;
13
+ export const PROJECT_COMPONENT_DIAGNOSTIC_BYTES = 64 * 1_024;
14
+ const MODULE_HOOKS_MIN_NODE = '22.15';
15
+ const REF_KINDS = new Set(['prefab', 'clip', 'action', 'stat']);
16
+ const FALLBACK_PACKAGE_DIRECTORIES = [
17
+ ['@waica/engine', 'engine'],
18
+ ['@waica/behaviors', 'behaviors'],
19
+ ['@waica/archetype-platformer', 'archetype-platformer'],
20
+ ];
21
+ const execFileAsync = promisify(execFile);
22
+ let sourceFallbackEntries;
23
+ export class ProjectComponentRunnerUnavailableError extends Error {
24
+ constructor(message, options) {
25
+ super(message, options);
26
+ this.name = 'ProjectComponentRunnerUnavailableError';
27
+ }
28
+ }
29
+ /** Pure feature probe retained for callers that report the native Node capability. */
30
+ export function nodeSupportsModuleHooks(moduleApi = nodeModule) {
31
+ return typeof moduleApi.registerHooks === 'function';
32
+ }
33
+ /** Compatibility diagnostic for hosts below the package's declared Node floor. */
34
+ export function unsupportedNodeFailure(nodeVersion = process.version) {
35
+ return {
36
+ code: 'component-load-unsupported',
37
+ file: 'src',
38
+ message: `Deep component validation requires Node >= ${MODULE_HOOKS_MIN_NODE} (node:module registerHooks) ` +
39
+ `to run isolated project-entry children; this host runs Node ${nodeVersion}. ` +
40
+ 'Skipping validate_project component metadata loading until the host upgrades.',
41
+ };
42
+ }
43
+ function record(value) {
44
+ return value && typeof value === 'object' && !Array.isArray(value)
45
+ ? value
46
+ : undefined;
47
+ }
48
+ function exactKeys(value, required, optional = []) {
49
+ const allowed = new Set([...required, ...optional]);
50
+ return (required.every((key) => Object.hasOwn(value, key)) &&
51
+ Object.keys(value).every((key) => allowed.has(key)));
52
+ }
53
+ function validReady(value) {
54
+ const candidate = record(value);
55
+ return (candidate !== undefined &&
56
+ exactKeys(candidate, ['kind', 'version']) &&
57
+ candidate.kind === 'project-entry-ready' &&
58
+ candidate.version === PROJECT_COMPONENT_PROTOCOL_VERSION);
59
+ }
60
+ function parseParam(value) {
61
+ const candidate = record(value);
62
+ if (!candidate ||
63
+ !exactKeys(candidate, ['name', 'ref', 'hasOptions'], ['default']) ||
64
+ typeof candidate.name !== 'string' ||
65
+ candidate.name.length === 0 ||
66
+ typeof candidate.ref !== 'string' ||
67
+ !REF_KINDS.has(candidate.ref) ||
68
+ typeof candidate.hasOptions !== 'boolean' ||
69
+ (Object.hasOwn(candidate, 'default') && typeof candidate.default !== 'string')) {
70
+ return undefined;
71
+ }
72
+ return {
73
+ name: candidate.name,
74
+ ref: candidate.ref,
75
+ hasOptions: candidate.hasOptions,
76
+ ...(Object.hasOwn(candidate, 'default')
77
+ ? { default: candidate.default }
78
+ : {}),
79
+ };
80
+ }
81
+ function parseComponent(value, expectedFile) {
82
+ const candidate = record(value);
83
+ if (!candidate ||
84
+ !exactKeys(candidate, [
85
+ 'name',
86
+ 'file',
87
+ 'params',
88
+ 'hasOnUpdate',
89
+ 'hasUpdateAfter',
90
+ 'updateAfter',
91
+ ]) ||
92
+ typeof candidate.name !== 'string' ||
93
+ candidate.name.length === 0 ||
94
+ candidate.file !== expectedFile ||
95
+ !Array.isArray(candidate.params) ||
96
+ typeof candidate.hasOnUpdate !== 'boolean' ||
97
+ typeof candidate.hasUpdateAfter !== 'boolean' ||
98
+ !Array.isArray(candidate.updateAfter) ||
99
+ candidate.updateAfter.some((target) => typeof target !== 'string') ||
100
+ (!candidate.hasUpdateAfter && candidate.updateAfter.length > 0)) {
101
+ return undefined;
102
+ }
103
+ const params = candidate.params.map(parseParam);
104
+ if (params.some((param) => param === undefined))
105
+ return undefined;
106
+ return {
107
+ name: candidate.name,
108
+ file: expectedFile,
109
+ params: params,
110
+ hasOnUpdate: candidate.hasOnUpdate,
111
+ hasUpdateAfter: candidate.hasUpdateAfter,
112
+ updateAfter: [...candidate.updateAfter],
113
+ };
114
+ }
115
+ function parseTerminal(value, token, expectedFile) {
116
+ const candidate = record(value);
117
+ if (!candidate ||
118
+ candidate.kind !== 'project-entry-result' ||
119
+ candidate.version !== PROJECT_COMPONENT_PROTOCOL_VERSION ||
120
+ candidate.token !== token ||
121
+ typeof candidate.ok !== 'boolean') {
122
+ return undefined;
123
+ }
124
+ if (candidate.ok) {
125
+ if (!exactKeys(candidate, ['kind', 'version', 'token', 'ok', 'components'])) {
126
+ return undefined;
127
+ }
128
+ if (!Array.isArray(candidate.components))
129
+ return undefined;
130
+ const components = candidate.components.map((row) => parseComponent(row, expectedFile));
131
+ if (components.some((row) => row === undefined))
132
+ return undefined;
133
+ return { ok: true, components: components };
134
+ }
135
+ if (!exactKeys(candidate, ['kind', 'version', 'token', 'ok', 'code', 'message']) ||
136
+ (candidate.code !== 'component-load-failed' &&
137
+ candidate.code !== 'component-load-unsupported') ||
138
+ typeof candidate.message !== 'string') {
139
+ return undefined;
140
+ }
141
+ return {
142
+ ok: false,
143
+ code: candidate.code,
144
+ message: candidate.message,
145
+ };
146
+ }
147
+ function appendTail(current, chunk) {
148
+ const next = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
149
+ if (next.length >= PROJECT_COMPONENT_DIAGNOSTIC_BYTES) {
150
+ return next.subarray(next.length - PROJECT_COMPONENT_DIAGNOSTIC_BYTES);
151
+ }
152
+ const combined = Buffer.concat([current, next]);
153
+ return combined.length <= PROJECT_COMPONENT_DIAGNOSTIC_BYTES
154
+ ? combined
155
+ : combined.subarray(combined.length - PROJECT_COMPONENT_DIAGNOSTIC_BYTES);
156
+ }
157
+ function diagnosticMessage(base, stdout, stderr) {
158
+ const diagnostics = [];
159
+ if (stdout.length > 0)
160
+ diagnostics.push(`stdout tail:\n${stdout.toString('utf8')}`);
161
+ if (stderr.length > 0)
162
+ diagnostics.push(`stderr tail:\n${stderr.toString('utf8')}`);
163
+ return diagnostics.length > 0 ? `${base}\n${diagnostics.join('\n')}` : base;
164
+ }
165
+ function schedulingAdapter(row) {
166
+ class SchedulingAdapter {
167
+ }
168
+ Object.defineProperty(SchedulingAdapter, 'componentName', { value: row.name });
169
+ if (row.hasUpdateAfter) {
170
+ Object.defineProperty(SchedulingAdapter, 'updateAfter', {
171
+ value: Object.freeze([...row.updateAfter]),
172
+ });
173
+ }
174
+ if (row.hasOnUpdate) {
175
+ Object.defineProperty(SchedulingAdapter.prototype, 'onUpdate', {
176
+ value: () => undefined,
177
+ });
178
+ }
179
+ return SchedulingAdapter;
180
+ }
181
+ function description(row) {
182
+ const params = {};
183
+ const defaults = {};
184
+ for (const param of row.params) {
185
+ params[param.name] = {
186
+ ref: param.ref,
187
+ ...(param.hasOptions ? { options: [] } : {}),
188
+ };
189
+ if (param.default !== undefined)
190
+ defaults[param.name] = param.default;
191
+ }
192
+ return {
193
+ file: row.file,
194
+ Class: schedulingAdapter(row),
195
+ params,
196
+ defaults,
197
+ };
198
+ }
199
+ function runnerFromModule() {
200
+ const javascript = fileURLToPath(new URL('./project-component-runner.js', import.meta.url));
201
+ if (existsSync(javascript))
202
+ return javascript;
203
+ const typescript = fileURLToPath(new URL('./project-component-runner.ts', import.meta.url));
204
+ if (existsSync(typescript))
205
+ return typescript;
206
+ throw new ProjectComponentRunnerUnavailableError(`Project component runner is missing beside ${fileURLToPath(import.meta.url)}.`);
207
+ }
208
+ function checkoutPackagesRoot() {
209
+ const candidate = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
210
+ return FALLBACK_PACKAGE_DIRECTORIES.every(([, directory]) => existsSync(path.join(candidate, directory, 'package.json')))
211
+ ? candidate
212
+ : undefined;
213
+ }
214
+ async function compileSourceFallbacks(packagesRoot) {
215
+ const repositoryRoot = path.dirname(packagesRoot);
216
+ const requireFromLoader = nodeModule.createRequire(import.meta.url);
217
+ const typescriptRoot = path.dirname(requireFromLoader.resolve('typescript/package.json'));
218
+ const compiler = path.join(typescriptRoot, 'bin', 'tsc');
219
+ const outputRoot = await mkdtemp(path.join(tmpdir(), 'waica-mcp-fallback-'));
220
+ try {
221
+ await Promise.all(FALLBACK_PACKAGE_DIRECTORIES.map(async ([, directory]) => {
222
+ const sourceRoot = path.join(packagesRoot, directory);
223
+ const destinationRoot = path.join(outputRoot, directory);
224
+ await mkdir(destinationRoot, { recursive: true });
225
+ await Promise.all([
226
+ execFileAsync(process.execPath, [
227
+ compiler,
228
+ '-p',
229
+ path.join(sourceRoot, 'tsconfig.build.json'),
230
+ '--outDir',
231
+ path.join(destinationRoot, 'dist'),
232
+ '--declaration',
233
+ 'false',
234
+ ], { cwd: repositoryRoot }),
235
+ copyFile(path.join(sourceRoot, 'package.json'), path.join(destinationRoot, 'package.json')),
236
+ ...(existsSync(path.join(sourceRoot, 'assets'))
237
+ ? [
238
+ cp(path.join(sourceRoot, 'assets'), path.join(destinationRoot, 'assets'), {
239
+ recursive: true,
240
+ }),
241
+ ]
242
+ : []),
243
+ ]);
244
+ }));
245
+ const entries = Object.fromEntries(FALLBACK_PACKAGE_DIRECTORIES.map(([specifier, directory]) => [
246
+ specifier,
247
+ path.join(outputRoot, directory, 'dist', 'index.js'),
248
+ ]));
249
+ entries.three = nodeModule
250
+ .createRequire(path.join(packagesRoot, 'engine', 'package.json'))
251
+ .resolve('three');
252
+ if (Object.values(entries).some((entry) => !existsSync(entry))) {
253
+ throw new Error('TypeScript fallback compilation did not emit every package entry.');
254
+ }
255
+ process.once('exit', () => rmSync(outputRoot, { recursive: true, force: true }));
256
+ return entries;
257
+ }
258
+ catch (error) {
259
+ rmSync(outputRoot, { recursive: true, force: true });
260
+ throw error;
261
+ }
262
+ }
263
+ async function fallbackEntriesFor(runnerPath) {
264
+ const packagesRoot = checkoutPackagesRoot();
265
+ if (!packagesRoot)
266
+ return {};
267
+ const built = Object.fromEntries(FALLBACK_PACKAGE_DIRECTORIES.map(([specifier, directory]) => [
268
+ specifier,
269
+ path.join(packagesRoot, directory, 'dist', 'index.js'),
270
+ ]));
271
+ built.three = nodeModule
272
+ .createRequire(path.join(packagesRoot, 'engine', 'package.json'))
273
+ .resolve('three');
274
+ if (Object.values(built).every((entry) => existsSync(entry)))
275
+ return built;
276
+ if (!runnerPath.endsWith('.ts'))
277
+ return {};
278
+ sourceFallbackEntries ??= compileSourceFallbacks(packagesRoot);
279
+ return sourceFallbackEntries;
280
+ }
281
+ async function projectModuleFiles(projectPath) {
282
+ const groups = await Promise.all(['components', 'roles', 'states'].map(async (directory) => (await directFiles(path.join(projectPath, 'src', directory), '.ts')).map((file) => `src/${directory}/${file}`)));
283
+ return groups.flat();
284
+ }
285
+ function cancellationReason(signal) {
286
+ return signal.reason ?? new DOMException('The operation was aborted.', 'AbortError');
287
+ }
288
+ function unavailable(message, cause) {
289
+ return new ProjectComponentRunnerUnavailableError(message, cause === undefined ? undefined : { cause });
290
+ }
291
+ export class ProjectComponentLoader {
292
+ active = new Set();
293
+ defaults;
294
+ closed = false;
295
+ constructor(options = {}) {
296
+ this.defaults = options;
297
+ }
298
+ async load(projectPath, _resolver, options = {}) {
299
+ this.assertOpen();
300
+ options.signal?.throwIfAborted();
301
+ const files = await projectModuleFiles(projectPath);
302
+ this.assertOpen();
303
+ options.signal?.throwIfAborted();
304
+ if (files.length === 0)
305
+ return { components: {}, failures: [] };
306
+ const runnerPath = options.runnerPath ?? this.defaults.runnerPath ?? runnerFromModule();
307
+ if (!existsSync(runnerPath)) {
308
+ throw unavailable(`Project component runner is missing: ${runnerPath}`);
309
+ }
310
+ const deadlineMs = options.deadlineMs ?? this.defaults.deadlineMs ?? PROJECT_COMPONENT_DEADLINE_MS;
311
+ let fallbackEntries;
312
+ try {
313
+ fallbackEntries = await fallbackEntriesFor(runnerPath);
314
+ }
315
+ catch (error) {
316
+ throw unavailable('Cannot prepare source fallback packages for the project component runner.', error);
317
+ }
318
+ this.assertOpen();
319
+ options.signal?.throwIfAborted();
320
+ const components = {};
321
+ const failures = [];
322
+ for (const relativeFile of files) {
323
+ this.assertOpen();
324
+ options.signal?.throwIfAborted();
325
+ const outcome = await this.runEntry({
326
+ projectPath,
327
+ relativeFile,
328
+ runnerPath,
329
+ deadlineMs,
330
+ fallbackEntries,
331
+ signal: options.signal,
332
+ });
333
+ if ('failure' in outcome) {
334
+ failures.push(outcome.failure);
335
+ }
336
+ else {
337
+ for (const row of outcome.components)
338
+ components[row.name] = description(row);
339
+ }
340
+ }
341
+ return { components, failures };
342
+ }
343
+ assertOpen() {
344
+ if (this.closed) {
345
+ throw unavailable('Project component runner is unavailable because its owner is closed.');
346
+ }
347
+ }
348
+ async close() {
349
+ if (this.closed && this.active.size === 0)
350
+ return;
351
+ this.closed = true;
352
+ const reason = new DOMException('The MCP server is closing.', 'AbortError');
353
+ await Promise.all([...this.active].map((execution) => execution.terminate(reason)));
354
+ }
355
+ runEntry(input) {
356
+ const token = randomUUID();
357
+ let child;
358
+ try {
359
+ child = (this.defaults.launcher ?? fork)(input.runnerPath, [], {
360
+ stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
361
+ serialization: 'json',
362
+ execArgv: [],
363
+ });
364
+ }
365
+ catch (error) {
366
+ return Promise.reject(unavailable(`Cannot launch project component runner ${input.runnerPath}.`, error));
367
+ }
368
+ let stdout = Buffer.alloc(0);
369
+ let stderr = Buffer.alloc(0);
370
+ let ready = false;
371
+ let terminalCount = 0;
372
+ let terminal;
373
+ let protocolFailure;
374
+ let infrastructureFailure;
375
+ let timeout = false;
376
+ let aborted;
377
+ let terminationFailure;
378
+ let closeObservedResolve;
379
+ const closeObserved = new Promise((resolve) => {
380
+ closeObservedResolve = resolve;
381
+ });
382
+ const forceTerminate = () => {
383
+ if (child.exitCode !== null || child.signalCode !== null)
384
+ return;
385
+ try {
386
+ if (!child.kill('SIGKILL')) {
387
+ terminationFailure ??= new Error(`Could not force-terminate validation child ${child.pid ?? '(unknown pid)'}.`);
388
+ }
389
+ }
390
+ catch (error) {
391
+ terminationFailure ??= error instanceof Error ? error : new Error(String(error));
392
+ }
393
+ };
394
+ const execution = {
395
+ terminate: async (reason) => {
396
+ aborted ??= reason;
397
+ forceTerminate();
398
+ await closeObserved;
399
+ if (terminationFailure)
400
+ throw terminationFailure;
401
+ },
402
+ };
403
+ this.active.add(execution);
404
+ return new Promise((resolve, reject) => {
405
+ const clear = () => {
406
+ clearTimeout(timer);
407
+ input.signal?.removeEventListener('abort', onAbort);
408
+ this.active.delete(execution);
409
+ closeObservedResolve();
410
+ };
411
+ const onAbort = () => {
412
+ aborted ??= cancellationReason(input.signal);
413
+ forceTerminate();
414
+ };
415
+ if (input.signal)
416
+ input.signal.addEventListener('abort', onAbort, { once: true });
417
+ child.stdout?.on('data', (chunk) => {
418
+ stdout = appendTail(stdout, chunk);
419
+ });
420
+ child.stderr?.on('data', (chunk) => {
421
+ stderr = appendTail(stderr, chunk);
422
+ });
423
+ child.on('error', (error) => {
424
+ infrastructureFailure ??= unavailable(`Project component runner failed to launch for ${input.relativeFile}.`, error);
425
+ forceTerminate();
426
+ });
427
+ child.on('message', (message) => {
428
+ if (!ready) {
429
+ if (!validReady(message)) {
430
+ infrastructureFailure ??= unavailable(`Project component runner handshake is incompatible for ${input.relativeFile}.`);
431
+ forceTerminate();
432
+ return;
433
+ }
434
+ ready = true;
435
+ child.send({
436
+ kind: 'load-project-entry',
437
+ version: PROJECT_COMPONENT_PROTOCOL_VERSION,
438
+ token,
439
+ projectPath: input.projectPath,
440
+ entryFile: path.join(input.projectPath, input.relativeFile),
441
+ relativeFile: input.relativeFile,
442
+ fallbackEntries: input.fallbackEntries,
443
+ }, (error) => {
444
+ if (!error)
445
+ return;
446
+ infrastructureFailure ??= unavailable(`Cannot send a request to the project component runner for ${input.relativeFile}.`, error);
447
+ forceTerminate();
448
+ });
449
+ return;
450
+ }
451
+ const candidate = record(message);
452
+ if (candidate?.kind === 'project-entry-result')
453
+ terminalCount += 1;
454
+ if (terminalCount > 1) {
455
+ protocolFailure = 'Runner sent more than one terminal message.';
456
+ forceTerminate();
457
+ return;
458
+ }
459
+ const parsed = parseTerminal(message, token, input.relativeFile);
460
+ if (!parsed) {
461
+ protocolFailure = 'Runner returned a malformed or unbound terminal payload.';
462
+ forceTerminate();
463
+ return;
464
+ }
465
+ terminal = parsed;
466
+ });
467
+ child.once('close', (code, signal) => {
468
+ clear();
469
+ if (aborted !== undefined) {
470
+ reject(aborted);
471
+ return;
472
+ }
473
+ if (terminationFailure) {
474
+ reject(terminationFailure);
475
+ return;
476
+ }
477
+ if (infrastructureFailure) {
478
+ reject(infrastructureFailure);
479
+ return;
480
+ }
481
+ if (!ready) {
482
+ reject(unavailable(`Project component runner exited before its handshake for ${input.relativeFile}.`));
483
+ return;
484
+ }
485
+ if (timeout) {
486
+ resolve({
487
+ failure: {
488
+ code: 'component-load-failed',
489
+ file: input.relativeFile,
490
+ message: diagnosticMessage(`Timed out after ${input.deadlineMs} ms; the direct child was force-terminated and its close was observed.`, stdout, stderr),
491
+ },
492
+ });
493
+ return;
494
+ }
495
+ if (protocolFailure || terminalCount !== 1 || !terminal) {
496
+ const outcome = signal
497
+ ? `signal ${signal}`
498
+ : code === null
499
+ ? 'without an exit status'
500
+ : `exit ${code}`;
501
+ resolve({
502
+ failure: {
503
+ code: 'component-load-failed',
504
+ file: input.relativeFile,
505
+ message: diagnosticMessage(protocolFailure ?? `Child ended with ${outcome} before one terminal payload.`, stdout, stderr),
506
+ },
507
+ });
508
+ return;
509
+ }
510
+ if (signal || code !== 0) {
511
+ resolve({
512
+ failure: {
513
+ code: 'component-load-failed',
514
+ file: input.relativeFile,
515
+ message: diagnosticMessage(signal
516
+ ? `Child exited with signal ${signal} after returning metadata.`
517
+ : `Child exited with status ${code} after returning metadata.`, stdout, stderr),
518
+ },
519
+ });
520
+ return;
521
+ }
522
+ if (!terminal.ok) {
523
+ resolve({
524
+ failure: {
525
+ code: terminal.code,
526
+ file: input.relativeFile,
527
+ message: diagnosticMessage(terminal.message, stdout, stderr),
528
+ },
529
+ });
530
+ return;
531
+ }
532
+ resolve({ components: terminal.components });
533
+ });
534
+ const timer = setTimeout(() => {
535
+ if (!ready) {
536
+ infrastructureFailure ??= unavailable(`Project component runner did not complete its handshake within ${input.deadlineMs} ms for ${input.relativeFile}.`);
537
+ }
538
+ else {
539
+ timeout = true;
540
+ }
541
+ forceTerminate();
542
+ }, input.deadlineMs);
543
+ });
544
+ }
545
+ }
546
+ const defaultLoader = new ProjectComponentLoader();
547
+ /**
548
+ * Executes every direct Project component, role and state entry in its own
549
+ * short-lived child. File-attributable failures remain result data.
550
+ */
551
+ export function loadProjectComponents(projectPath, resolver, options) {
552
+ return defaultLoader.load(projectPath, resolver, options);
553
+ }