@waica/cli 0.5.0 → 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.
- package/README.md +42 -37
- package/dist/editor/assets/{cssMode-2_8FrcPt.js → cssMode-DfLN2qUF.js} +1 -1
- package/dist/editor/assets/{freemarker2-BgSLfjNS.js → freemarker2-BM-wWcXR.js} +1 -1
- package/dist/editor/assets/{handlebars-CZIkUhwZ.js → handlebars-C68SV5M9.js} +1 -1
- package/dist/editor/assets/{html-B66ooLhD.js → html-dQgLbIki.js} +1 -1
- package/dist/editor/assets/{htmlMode-BiIT--Mv.js → htmlMode-Dx91fUdd.js} +1 -1
- package/dist/editor/assets/{index-Br-M9Tdo.js → index-592ABIaz.js} +2787 -406
- package/dist/editor/assets/{index-q7IkLe-P.css → index-Cy5pmBhN.css} +1 -1
- package/dist/editor/assets/{javascript-BZrbbzWQ.js → javascript-BmsSq5ka.js} +1 -1
- package/dist/editor/assets/{jsonMode-CABkbZgt.js → jsonMode-CWoPtX2M.js} +1 -1
- package/dist/editor/assets/{liquid-CT7pyNYe.js → liquid-DwnJlNJZ.js} +1 -1
- package/dist/editor/assets/{mdx-DW7MPqdm.js → mdx-DaLkvlR1.js} +1 -1
- package/dist/editor/assets/{python-CgBwEaNf.js → python-Cs5X5zFp.js} +1 -1
- package/dist/editor/assets/{razor-Dvggr_Q1.js → razor-PTwWoRk8.js} +1 -1
- package/dist/editor/assets/{tsMode-OfEpAJE7.js → tsMode-BqUQdgsR.js} +1 -1
- package/dist/editor/assets/{typescript-BQbhlbBK.js → typescript-BsJ7r2RJ.js} +1 -1
- package/dist/editor/assets/{xml-5qToUR1k.js → xml-DyyoPey4.js} +1 -1
- package/dist/editor/assets/{yaml-CFM1jPNu.js → yaml-Cjqtlp29.js} +1 -1
- package/dist/editor/index.html +2 -2
- package/dist/mcp/introspection.js +2 -0
- package/dist/mcp/node_modules/@waica/archetype-platformer/package.json +3 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.d.ts +4 -3
- package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +12 -15
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +1 -0
- package/dist/mcp/node_modules/@waica/behaviors/package.json +2 -2
- package/dist/mcp/node_modules/@waica/engine/README.md +85 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.d.ts +39 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component-update-schedule.js +157 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +5 -0
- package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +1 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.d.ts +7 -0
- package/dist/mcp/node_modules/@waica/engine/dist/game.js +79 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +7 -1
- package/dist/mcp/node_modules/@waica/engine/dist/index.js +3 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.d.ts +10 -0
- package/dist/mcp/node_modules/@waica/engine/dist/input.js +40 -2
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.d.ts +69 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-bridge.js +105 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.d.ts +71 -0
- package/dist/mcp/node_modules/@waica/engine/dist/runtime-inspection.js +279 -0
- package/dist/mcp/node_modules/@waica/engine/package.json +3 -2
- package/dist/mcp/project-component-loader.js +506 -240
- package/dist/mcp/project-component-runner.js +237 -0
- package/dist/mcp/runtime-browser.js +336 -0
- package/dist/mcp/runtime-dev-server.js +273 -0
- package/dist/mcp/runtime-preflight.js +249 -0
- package/dist/mcp/runtime-service.js +8 -0
- package/dist/mcp/runtime-session-manager.js +265 -0
- package/dist/mcp/scaffolds.js +126 -6
- package/dist/mcp/server.js +330 -9
- package/dist/mcp/stdio.js +0 -25
- package/dist/mcp/template/src/main.ts +6 -1
- package/dist/mcp/validation.js +77 -6
- package/package.json +2 -1
- package/dist/mcp/native-import.cjs +0 -3
|
@@ -1,287 +1,553 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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';
|
|
3
5
|
import * as nodeModule from 'node:module';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
4
7
|
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath
|
|
6
|
-
import {
|
|
7
|
-
import { causeText, PackageResolver, projectAnchoredRequire } from './package-resolver.js';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
8
10
|
import { directFiles } from './project-path.js';
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// that started it (an ephemeral, in-memory lookup, cleaned up per run), and
|
|
13
|
-
// LOAD_PARAM is a content hash that only changes when the file's own bytes
|
|
14
|
-
// change. Splitting them is what lets re-validating unchanged files reuse
|
|
15
|
-
// Node's module cache instead of pinning a fresh entry on every run (see
|
|
16
|
-
// loadProjectComponents below).
|
|
17
|
-
const PROJECT_PARAM = 'waica-component-project';
|
|
18
|
-
const LOAD_PARAM = 'waica-component-load';
|
|
19
|
-
const hookContexts = new Map();
|
|
20
|
-
// Loading this tiny CommonJS trampoline through Node keeps Vitest from
|
|
21
|
-
// rewriting fixture imports. The build copies the same file byte-for-byte.
|
|
22
|
-
const nativeImport = createRequire(import.meta.url)('./native-import.cjs');
|
|
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;
|
|
23
14
|
const MODULE_HOOKS_MIN_NODE = '22.15';
|
|
24
|
-
|
|
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. */
|
|
25
30
|
export function nodeSupportsModuleHooks(moduleApi = nodeModule) {
|
|
26
31
|
return typeof moduleApi.registerHooks === 'function';
|
|
27
32
|
}
|
|
28
|
-
/**
|
|
29
|
-
* `node:module`'s `registerHooks` landed in Node 22.15; this MCP server is
|
|
30
|
-
* still expected to start (and serve every other tool) on the older Node
|
|
31
|
-
* hosts an agent may be running. A static named import of `registerHooks`
|
|
32
|
-
* would fail ESM linking on those hosts before the transport ever connects
|
|
33
|
-
* (see workspace-runtime.ts for the same guard on the same API), so this is
|
|
34
|
-
* a namespace import feature-checked at runtime instead.
|
|
35
|
-
*/
|
|
36
|
-
const hooksSupported = nodeSupportsModuleHooks();
|
|
37
|
-
/** The single finding emitted in place of real component loading on a host too old for module hooks. */
|
|
33
|
+
/** Compatibility diagnostic for hosts below the package's declared Node floor. */
|
|
38
34
|
export function unsupportedNodeFailure(nodeVersion = process.version) {
|
|
39
35
|
return {
|
|
40
36
|
code: 'component-load-unsupported',
|
|
41
37
|
file: 'src',
|
|
42
38
|
message: `Deep component validation requires Node >= ${MODULE_HOOKS_MIN_NODE} (node:module registerHooks) ` +
|
|
43
|
-
`to run project
|
|
44
|
-
'Skipping
|
|
45
|
-
'will not appear in validate_project or list_components until the host upgrades.',
|
|
39
|
+
`to run isolated project-entry children; this host runs Node ${nodeVersion}. ` +
|
|
40
|
+
'Skipping validate_project component metadata loading until the host upgrades.',
|
|
46
41
|
};
|
|
47
42
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
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')) {
|
|
50
70
|
return undefined;
|
|
51
|
-
try {
|
|
52
|
-
const token = new URL(parentURL).searchParams.get(PROJECT_PARAM);
|
|
53
|
-
const context = token ? hookContexts.get(token) : undefined;
|
|
54
|
-
return token && context ? { token, context } : undefined;
|
|
55
71
|
}
|
|
56
|
-
|
|
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)) {
|
|
57
101
|
return undefined;
|
|
58
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
|
+
};
|
|
59
114
|
}
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/** Content identity for cache-busting: stable across runs, changes only when the file's bytes do. */
|
|
69
|
-
function contentToken(filePath) {
|
|
70
|
-
try {
|
|
71
|
-
return createHash('sha1').update(readFileSync(filePath)).digest('hex').slice(0, 16);
|
|
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;
|
|
72
123
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 };
|
|
77
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
|
+
};
|
|
78
146
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
...RELATIVE_EXTENSIONS.map((extension) => `${unresolvedPath}${extension}`),
|
|
84
|
-
...RELATIVE_EXTENSIONS.map((extension) => path.join(unresolvedPath, `index${extension}`)),
|
|
85
|
-
];
|
|
86
|
-
if (unresolvedPath.endsWith('.js')) {
|
|
87
|
-
const withoutJs = unresolvedPath.slice(0, -'.js'.length);
|
|
88
|
-
candidates.unshift(`${withoutJs}.ts`, `${withoutJs}.tsx`);
|
|
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);
|
|
89
151
|
}
|
|
90
|
-
|
|
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);
|
|
91
156
|
}
|
|
92
|
-
function
|
|
93
|
-
|
|
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;
|
|
94
164
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// resolver for a given run; nextResolve is always tried first so real Node
|
|
98
|
-
// resolution (package "exports" maps, symlinks, literal existing paths) stays
|
|
99
|
-
// authoritative, and our TypeScript-project conventions are only a fallback.
|
|
100
|
-
if (hooksSupported) {
|
|
101
|
-
nodeModule.registerHooks({
|
|
102
|
-
resolve(specifier, context, nextResolve) {
|
|
103
|
-
const run = runFromParent(context.parentURL);
|
|
104
|
-
if (!run)
|
|
105
|
-
return nextResolve(specifier, context);
|
|
106
|
-
if (specifier.startsWith('@waica/')) {
|
|
107
|
-
const bridge = run.context.packageBridges.get(specifier);
|
|
108
|
-
if (bridge)
|
|
109
|
-
return { url: bridge, shortCircuit: true };
|
|
110
|
-
const resolved = run.context.projectRequire.resolve(specifier);
|
|
111
|
-
const url = pathToFileURL(resolved);
|
|
112
|
-
return {
|
|
113
|
-
url: /\.tsx?$/.test(resolved)
|
|
114
|
-
? versioned(url, run.token, contentToken(resolved))
|
|
115
|
-
: url.href,
|
|
116
|
-
shortCircuit: true,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (specifier.startsWith('.')) {
|
|
120
|
-
const unresolved = new URL(specifier, context.parentURL);
|
|
121
|
-
let resolvedURL;
|
|
122
|
-
try {
|
|
123
|
-
resolvedURL = new URL(nextResolve(specifier, context).url);
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
// Directory imports (./lib -> ./lib/index.ts) and multi-dot
|
|
127
|
-
// specifiers (./foo.helper -> ./foo.helper.ts) are valid under the
|
|
128
|
-
// project's moduleResolution "bundler" and run fine in Vite, but
|
|
129
|
-
// Node's default resolver rejects both — fall back to the same
|
|
130
|
-
// candidates the editor tries before giving up.
|
|
131
|
-
const fallback = resolveRelativeCandidate(fileURLToPath(unresolved));
|
|
132
|
-
if (!fallback)
|
|
133
|
-
throw error;
|
|
134
|
-
resolvedURL = pathToFileURL(fallback);
|
|
135
|
-
}
|
|
136
|
-
const resolvedPath = fileURLToPath(resolvedURL);
|
|
137
|
-
return {
|
|
138
|
-
url: versioned(resolvedURL, run.token, contentToken(resolvedPath)),
|
|
139
|
-
shortCircuit: true,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
return nextResolve(specifier, context);
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function componentDescriptions(module, file) {
|
|
147
|
-
const descriptions = {};
|
|
148
|
-
for (const value of Object.values(module)) {
|
|
149
|
-
if (typeof value !== 'function' || !Object.hasOwn(value, 'componentName'))
|
|
150
|
-
continue;
|
|
151
|
-
const Class = value;
|
|
152
|
-
if (typeof Class.componentName !== 'string' || !Class.componentName)
|
|
153
|
-
continue;
|
|
154
|
-
descriptions[Class.componentName] = {
|
|
155
|
-
file,
|
|
156
|
-
params: objectRecord(Class.params),
|
|
157
|
-
defaults: classDefaults(Class),
|
|
158
|
-
};
|
|
165
|
+
function schedulingAdapter(row) {
|
|
166
|
+
class SchedulingAdapter {
|
|
159
167
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const seen = new Set();
|
|
166
|
-
while (current instanceof Error && !seen.has(current)) {
|
|
167
|
-
errors.push(current);
|
|
168
|
-
seen.add(current);
|
|
169
|
-
current = current.cause;
|
|
168
|
+
Object.defineProperty(SchedulingAdapter, 'componentName', { value: row.name });
|
|
169
|
+
if (row.hasUpdateAfter) {
|
|
170
|
+
Object.defineProperty(SchedulingAdapter, 'updateAfter', {
|
|
171
|
+
value: Object.freeze([...row.updateAfter]),
|
|
172
|
+
});
|
|
170
173
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const code = candidate.code;
|
|
176
|
-
if (code === 'ERR_UNKNOWN_FILE_EXTENSION' ||
|
|
177
|
-
code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
if (/not supported in strip-only mode|unsupported TypeScript syntax|unknown file extension/i.test(candidate.message)) {
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
174
|
+
if (row.hasOnUpdate) {
|
|
175
|
+
Object.defineProperty(SchedulingAdapter.prototype, 'onUpdate', {
|
|
176
|
+
value: () => undefined,
|
|
177
|
+
});
|
|
183
178
|
}
|
|
184
|
-
return
|
|
179
|
+
return SchedulingAdapter;
|
|
185
180
|
}
|
|
186
|
-
function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (name === 'default') {
|
|
197
|
-
lines.push('export default value.default');
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
const binding = `binding${index++}`;
|
|
201
|
-
lines.push(`const ${binding} = value[${JSON.stringify(name)}]`, `export { ${binding} as ${name} }`);
|
|
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;
|
|
202
191
|
}
|
|
203
192
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
193
|
+
file: row.file,
|
|
194
|
+
Class: schedulingAdapter(row),
|
|
195
|
+
params,
|
|
196
|
+
defaults,
|
|
206
197
|
};
|
|
207
198
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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.');
|
|
232
254
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
urls.set(entry.specifier, entry.bridge.url);
|
|
240
|
-
keys.push(entry.bridge.key);
|
|
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;
|
|
241
261
|
}
|
|
242
|
-
|
|
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;
|
|
243
280
|
}
|
|
244
281
|
async function projectModuleFiles(projectPath) {
|
|
245
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}`)));
|
|
246
283
|
return groups.flat();
|
|
247
284
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
255
297
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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);
|
|
271
335
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
file: relative,
|
|
276
|
-
message: causeText(error),
|
|
277
|
-
});
|
|
336
|
+
else {
|
|
337
|
+
for (const row of outcome.components)
|
|
338
|
+
components[row.name] = description(row);
|
|
278
339
|
}
|
|
279
340
|
}
|
|
341
|
+
return { components, failures };
|
|
280
342
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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)));
|
|
285
354
|
}
|
|
286
|
-
|
|
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);
|
|
287
553
|
}
|