@waica/cli 0.4.2 → 0.5.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 (66) hide show
  1. package/dist/editor/assets/{cssMode-BO8FrG9g.js → cssMode-2_8FrcPt.js} +1 -1
  2. package/dist/editor/assets/{freemarker2-C6rCz24J.js → freemarker2-BgSLfjNS.js} +1 -1
  3. package/dist/editor/assets/{handlebars-MZdu9Qw_.js → handlebars-CZIkUhwZ.js} +1 -1
  4. package/dist/editor/assets/{html-BrjX1TJ1.js → html-B66ooLhD.js} +1 -1
  5. package/dist/editor/assets/{htmlMode-cEvpMwiW.js → htmlMode-BiIT--Mv.js} +1 -1
  6. package/dist/editor/assets/{index-eTEBznqW.js → index-Br-M9Tdo.js} +1846 -259
  7. package/dist/editor/assets/{index-Btzc6zxr.css → index-q7IkLe-P.css} +1 -1
  8. package/dist/editor/assets/{javascript-DHUgp9_L.js → javascript-BZrbbzWQ.js} +1 -1
  9. package/dist/editor/assets/{jsonMode-D0PmIuaM.js → jsonMode-CABkbZgt.js} +1 -1
  10. package/dist/editor/assets/{liquid-UM67NCct.js → liquid-CT7pyNYe.js} +1 -1
  11. package/dist/editor/assets/{mdx-D3xXLqrP.js → mdx-DW7MPqdm.js} +1 -1
  12. package/dist/editor/assets/{python-BhLrcAva.js → python-CgBwEaNf.js} +1 -1
  13. package/dist/editor/assets/{razor-DcG373mQ.js → razor-Dvggr_Q1.js} +1 -1
  14. package/dist/editor/assets/{tsMode-ByTYaf45.js → tsMode-OfEpAJE7.js} +1 -1
  15. package/dist/editor/assets/{typescript-DfJlvcTN.js → typescript-BQbhlbBK.js} +1 -1
  16. package/dist/editor/assets/{xml-22dL0oCq.js → xml-5qToUR1k.js} +1 -1
  17. package/dist/editor/assets/{yaml-mgCu6yV6.js → yaml-CFM1jPNu.js} +1 -1
  18. package/dist/editor/index.html +2 -2
  19. package/dist/mcp/component-metadata.js +23 -0
  20. package/dist/mcp/introspection.js +2 -14
  21. package/dist/mcp/native-import.cjs +3 -0
  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 +81 -0
  32. package/dist/mcp/node_modules/@waica/behaviors/dist/health.js +150 -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 +29 -0
  38. package/dist/mcp/node_modules/@waica/behaviors/dist/out-of-bounds.js +37 -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/dist/authoring-defaults.d.ts +11 -0
  48. package/dist/mcp/node_modules/@waica/engine/dist/authoring-defaults.js +51 -0
  49. package/dist/mcp/node_modules/@waica/engine/dist/component.d.ts +10 -1
  50. package/dist/mcp/node_modules/@waica/engine/dist/component.js +1 -0
  51. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.d.ts +1 -0
  52. package/dist/mcp/node_modules/@waica/engine/dist/components/animated-sprite.js +10 -0
  53. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.d.ts +1 -0
  54. package/dist/mcp/node_modules/@waica/engine/dist/components/sprite.js +1 -0
  55. package/dist/mcp/node_modules/@waica/engine/dist/index.d.ts +1 -0
  56. package/dist/mcp/node_modules/@waica/engine/dist/index.js +1 -0
  57. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.d.ts +2 -1
  58. package/dist/mcp/node_modules/@waica/engine/dist/state/state-machine.js +7 -1
  59. package/dist/mcp/node_modules/@waica/engine/package.json +1 -1
  60. package/dist/mcp/package-resolver.js +10 -2
  61. package/dist/mcp/project-component-loader.js +287 -0
  62. package/dist/mcp/stdio.js +25 -0
  63. package/dist/mcp/template/src/characters/player.character.json +29 -2
  64. package/dist/mcp/template/src/characters/slime.character.json +6 -0
  65. package/dist/mcp/validation.js +168 -23
  66. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { discoverArchetypes, pickArchetype } from './archetypes.js';
4
+ import { classDefaults, objectRecord } from './component-metadata.js';
4
5
  import { PackageResolver, mixedSourceWarnings, provenanceRows, } from './package-resolver.js';
6
+ import { loadProjectComponents } from './project-component-loader.js';
5
7
  import { directFiles, requireWaicaProject } from './project-path.js';
6
8
  function add(context, severity, code, message, file, ref) {
7
9
  context.findings.push({ severity, code, message, file, ...(ref ? { ref } : {}) });
@@ -45,10 +47,63 @@ function componentList(value) {
45
47
  return [];
46
48
  return value.filter((entry) => !!entry && typeof entry === 'object' && typeof entry.type === 'string');
47
49
  }
48
- function objectRecord(value) {
49
- return value && typeof value === 'object' && !Array.isArray(value)
50
- ? value
51
- : {};
50
+ function classMetadata(Class) {
51
+ // A throwing constructor has no observable defaults, matching list_components.
52
+ return { params: Class.params ?? {}, defaults: classDefaults(Class) };
53
+ }
54
+ /** Clip names declared by the sibling AnimatedSprite, or undefined when there is none (no animation contract to check). */
55
+ function siblingClips(siblings) {
56
+ const animated = siblings.find((component) => component.type === 'AnimatedSprite');
57
+ return animated ? new Set(Object.keys(objectRecord(animated.props?.clips))) : undefined;
58
+ }
59
+ /** Whether `action` has at least one real binding — an own key, since bindings is keyed by an untrusted string. */
60
+ function isBoundAction(bindings, action) {
61
+ return Object.hasOwn(bindings, action) && (bindings[action]?.length ?? 0) > 0;
62
+ }
63
+ function validateParamReferences(entries, siblings, file, context) {
64
+ const clips = siblingClips(siblings);
65
+ for (const { component, only } of entries) {
66
+ const metadata = context.componentMetadata.get(component.type);
67
+ if (!metadata)
68
+ continue;
69
+ const props = objectRecord(component.props);
70
+ for (const [param, spec] of Object.entries(metadata.params)) {
71
+ if (only && !only.has(param))
72
+ continue;
73
+ if (!spec.ref || spec.options !== undefined)
74
+ continue;
75
+ const value = Object.hasOwn(props, param) ? props[param] : metadata.defaults[param];
76
+ if (typeof value !== 'string' || value === '')
77
+ continue;
78
+ const field = `${component.type}.${param}`;
79
+ switch (spec.ref) {
80
+ case 'prefab':
81
+ if (!context.prefabRefs.has(value)) {
82
+ add(context, 'error', 'broken-prefab-ref', `Component "${component.type}" param "${param}" references missing prefab "${value}".`, file, field);
83
+ }
84
+ break;
85
+ case 'clip':
86
+ if (clips && !clips.has(value)) {
87
+ add(context, 'error', 'missing-clip', `Component "${component.type}" param "${param}" references missing animation clip "${value}".`, file, field);
88
+ }
89
+ break;
90
+ case 'action':
91
+ if (!isBoundAction(context.bindings, value)) {
92
+ add(context,
93
+ // Consistent with the pre-existing state-transition check for the
94
+ // same condition (below): an unbound action is a real gap the
95
+ // agent should look at, but not one that flips ok:false.
96
+ 'warning', 'input-action-unbound', `Component "${component.type}" param "${param}" references unbound input action "${value}".`, file, field);
97
+ }
98
+ break;
99
+ case 'stat':
100
+ if (!context.declaredStats.has(value)) {
101
+ add(context, 'warning', 'undeclared-stat', `Component "${component.type}" param "${param}" references undeclared stat "${value}"; runtime writes may still create it.`, file, field);
102
+ }
103
+ break;
104
+ }
105
+ }
106
+ }
52
107
  }
53
108
  function machineStates(component) {
54
109
  return objectRecord(component.props?.states);
@@ -85,10 +140,7 @@ function stateCodeExists(role, state, context) {
85
140
  (context.roleStateSources.get(role) ?? []).some((source) => roleSourceHasState(source, state)));
86
141
  }
87
142
  function validateStateMachines(components, file, ref, context) {
88
- const animated = components.find((component) => component.type === 'AnimatedSprite');
89
- const clips = animated
90
- ? new Set(Object.keys(objectRecord(animated.props?.clips)))
91
- : undefined;
143
+ const clips = siblingClips(components);
92
144
  for (const machine of components.filter((component) => component.type === 'StateMachine')) {
93
145
  const states = machineStates(machine);
94
146
  const realNames = Object.keys(states).filter((name) => name !== '*');
@@ -98,9 +150,15 @@ function validateStateMachines(components, file, ref, context) {
98
150
  : (realNames[0] ?? '');
99
151
  for (const name of realNames) {
100
152
  const definition = objectRecord(states[name]);
101
- const clip = typeof definition.clip === 'string' ? definition.clip : name;
153
+ const explicitClip = typeof definition.clip === 'string' ? definition.clip : undefined;
154
+ const clip = explicitClip ?? name;
155
+ // No "empty means none" special case here: the runtime looks up
156
+ // `this.states[state]?.clip ?? state`, and '' survives that nullish
157
+ // coalesce, so an explicit empty clip is looked up literally and must
158
+ // be validated like any other explicit value (unlike Collectible.stat,
159
+ // which the runtime genuinely treats as unset).
102
160
  if (clips && !clips.has(clip)) {
103
- add(context, 'warning', 'missing-clip', `State "${name}" uses missing animation clip "${clip}".`, file, ref ?? name);
161
+ add(context, explicitClip === undefined ? 'warning' : 'error', 'missing-clip', `State "${name}" uses missing animation clip "${clip}".`, file, explicitClip === undefined ? (ref ?? name) : `StateMachine.states.${name}.clip`);
104
162
  }
105
163
  const transitions = Array.isArray(definition.transitions) ? definition.transitions : [];
106
164
  for (const transition of transitions) {
@@ -145,6 +203,7 @@ function validatePrefab(prefab, file, ref, context) {
145
203
  const components = componentList(prefab.components);
146
204
  for (const component of components)
147
205
  checkComponent(component, file, ref, context);
206
+ validateParamReferences(components.map((component) => ({ component })), components, file, context);
148
207
  validateStateMachines(components, file, ref, context);
149
208
  }
150
209
  function validateScene(scene, file, prefabs, uiNames, context) {
@@ -186,6 +245,57 @@ function validateScene(scene, file, prefabs, uiNames, context) {
186
245
  }
187
246
  }
188
247
  }
248
+ // Lazily resolved: only computed when a consumer below actually needs
249
+ // the merged prefab+override component list (an override that changes a
250
+ // ref param, a clip-context change, an inline ref:clip lookup that needs
251
+ // the effective sibling AnimatedSprite, or state-machine revalidation).
252
+ // A plain "prefab: ref" entity with no overrides and no inline
253
+ // components never pays for building it (restores the pre-typed-refs
254
+ // laziness that only ran this for state-behavior changes).
255
+ let cachedEffectiveComponents;
256
+ const effectiveComponents = () => {
257
+ cachedEffectiveComponents ??= resolvedEntityComponents(entity, prefab);
258
+ return cachedEffectiveComponents;
259
+ };
260
+ const hasRefKind = (type, kind) => Object.values(context.componentMetadata.get(type)?.params ?? {}).some((spec) => spec.ref === kind && spec.options === undefined);
261
+ // Undefined scope means "check every declared ref param" — correct for
262
+ // inline components (never validated elsewhere) and for a component a
263
+ // clip-context change forces a full recheck of. A Set scopes the check
264
+ // to only the override's changed params, so a prefab-level finding for
265
+ // a param the override never touched is not reported a second time
266
+ // under the scene file.
267
+ const referenceScopes = new Map();
268
+ for (const component of inline)
269
+ referenceScopes.set(component, undefined);
270
+ const inlineTypes = new Set(inline.map((component) => component.type));
271
+ for (const [type, rawPatch] of Object.entries(overrides)) {
272
+ if (inlineTypes.has(type))
273
+ continue;
274
+ const metadata = context.componentMetadata.get(type);
275
+ const patch = objectRecord(rawPatch);
276
+ const changedRefParams = new Set(Object.entries(metadata?.params ?? {})
277
+ .filter(([param, spec]) => spec.ref !== undefined && spec.options === undefined && Object.hasOwn(patch, param))
278
+ .map(([param]) => param));
279
+ if (changedRefParams.size === 0)
280
+ continue;
281
+ const effective = effectiveComponents().find((component) => component.type === type);
282
+ if (effective && !referenceScopes.has(effective))
283
+ referenceScopes.set(effective, changedRefParams);
284
+ }
285
+ const changesClipContext = inline.some((component) => component.type === 'AnimatedSprite') ||
286
+ Object.hasOwn(overrides, 'AnimatedSprite');
287
+ if (changesClipContext) {
288
+ for (const component of effectiveComponents()) {
289
+ // A clip-context change can affect params this component didn't
290
+ // itself change, so this always widens to a full check rather than
291
+ // narrowing an already-scoped entry from the override loop above.
292
+ if (hasRefKind(component.type, 'clip'))
293
+ referenceScopes.set(component, undefined);
294
+ }
295
+ }
296
+ const needsSiblings = cachedEffectiveComponents !== undefined ||
297
+ [...referenceScopes.keys()].some((component) => hasRefKind(component.type, 'clip'));
298
+ validateParamReferences([...referenceScopes.entries()].map(([component, only]) => ({ component, only })), needsSiblings ? effectiveComponents() : [...referenceScopes.keys()], file, context);
189
299
  // Re-evaluate inherited state behavior only when this entity actually
190
300
  // changes a StateMachine or AnimatedSprite. Unrelated overrides keep the
191
301
  // prefab-level finding as the single source of truth.
@@ -193,7 +303,7 @@ function validateScene(scene, file, prefabs, uiNames, context) {
193
303
  const changesStateBehavior = inline.some((component) => stateTypes.has(component.type)) ||
194
304
  Object.keys(overrides).some((type) => stateTypes.has(type));
195
305
  if (changesStateBehavior) {
196
- validateStateMachines(resolvedEntityComponents(entity, prefab), file, entityRef, context);
306
+ validateStateMachines(effectiveComponents(), file, entityRef, context);
197
307
  }
198
308
  }
199
309
  }
@@ -234,15 +344,30 @@ export async function validateProject(projectPath) {
234
344
  : null;
235
345
  const resolver = new PackageResolver(projectPath);
236
346
  const discoveryWarnings = [];
237
- const [engine, behaviors, archetypes, projectComponents, roleStateSources] = await Promise.all([
347
+ const [engine, behaviors, archetypes, projectComponents, roleStateSources, loadedProjectComponents,] = await Promise.all([
238
348
  resolver.load('@waica/engine'),
239
349
  resolver.load('@waica/behaviors'),
240
350
  discoverArchetypes(projectPath, resolver, discoveryWarnings, activeId ? [activeId] : []),
241
351
  projectComponentCandidates(projectPath),
242
352
  projectRoleStateSources(projectPath),
353
+ loadProjectComponents(projectPath, resolver),
243
354
  ]);
355
+ for (const failure of loadedProjectComponents.failures) {
356
+ add({ findings },
357
+ // component-load-unsupported means Node's strip-only loader cannot run
358
+ // code that can still be perfectly valid in the project's Vite/browser
359
+ // toolchain (asset imports, TS enums, an old Node host) — that is not a
360
+ // project defect, so it must not flip a healthy project to ok:false.
361
+ failure.code === 'component-load-unsupported' ? 'info' : 'error', failure.code, `Cannot execute project module: ${failure.message}`, failure.file);
362
+ }
244
363
  const manifest = pickArchetype(archetypes, activeId, projectPath).manifest;
245
364
  const controls = objectRecord(objectRecord(fixed.get('src/controls.json')).bindings);
365
+ // Bound/unbound is decided from controls.json alone: the shipped runtime
366
+ // installs exactly controls.json's bindings (template main.ts passes
367
+ // controls.bindings raw; engine DEFAULT_BINDINGS is {}), so an action an
368
+ // archetype defines by default but controls.json drops is genuinely
369
+ // unbound at runtime even though discoverArchetypes never guarantees
370
+ // manifest.bindings exists (only manifest.id is validated).
246
371
  const bindings = {};
247
372
  for (const [name, value] of Object.entries(controls)) {
248
373
  if (Array.isArray(value) && value.every((entry) => typeof entry === 'string')) {
@@ -250,16 +375,22 @@ export async function validateProject(projectPath) {
250
375
  }
251
376
  }
252
377
  const stateFiles = new Set((await directFiles(path.join(projectPath, 'src/states'), '.ts')).map((file) => file.slice(0, -'.ts'.length)));
253
- const context = {
254
- findings,
255
- manifest,
256
- knownComponents: new Set(Object.keys(manifest.registry.components)),
257
- projectComponents,
258
- stateFiles,
259
- roleStateSources,
260
- bindings,
261
- };
378
+ const declaredStats = new Set(Object.keys(objectRecord(objectRecord(fixed.get('src/stats.json')).stats)));
379
+ const componentMetadata = new Map(Object.entries(manifest.registry.components).map(([name, Class]) => [
380
+ name,
381
+ classMetadata(Class),
382
+ ]));
383
+ for (const [name, description] of Object.entries(loadedProjectComponents.components)) {
384
+ componentMetadata.set(name, {
385
+ params: description.params,
386
+ defaults: description.defaults,
387
+ });
388
+ projectComponents.add(name);
389
+ }
390
+ // Reconstruct the complete ref set before validating any prefab, so a
391
+ // lexically earlier file can refer to one discovered later in the tree.
262
392
  const prefabs = new Map();
393
+ const prefabFiles = [];
263
394
  for (const [directory, type] of [
264
395
  ['characters', 'character'],
265
396
  ['objects', 'object'],
@@ -274,12 +405,26 @@ export async function validateProject(projectPath) {
274
405
  const ref = `${directory}/${file.slice(0, -suffix.length)}`;
275
406
  const prefab = parsed;
276
407
  prefabs.set(ref, prefab);
277
- validatePrefab(prefab, relative, ref, context);
408
+ prefabFiles.push({ prefab, relative, ref });
278
409
  }
279
410
  }
411
+ const context = {
412
+ findings,
413
+ manifest,
414
+ knownComponents: new Set(Object.keys(manifest.registry.components)),
415
+ projectComponents,
416
+ componentMetadata,
417
+ prefabRefs: new Set(prefabs.keys()),
418
+ declaredStats,
419
+ stateFiles,
420
+ roleStateSources,
421
+ bindings,
422
+ };
423
+ for (const { prefab, relative, ref } of prefabFiles) {
424
+ validatePrefab(prefab, relative, ref, context);
425
+ }
280
426
  const uiFiles = await directFiles(path.join(projectPath, 'src/ui'), '.html');
281
427
  const uiNames = new Set(uiFiles.map((file) => file.slice(0, -'.html'.length)));
282
- const declaredStats = new Set(Object.keys(objectRecord(objectRecord(fixed.get('src/stats.json')).stats)));
283
428
  for (const file of uiFiles) {
284
429
  const relative = `src/ui/${file}`;
285
430
  const html = await readFile(path.join(projectPath, relative), 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/cli",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Waica in one command — `npx @waica/cli` opens the editor, `waica mcp` serves the MCP server",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=20.19"
22
+ "node": ">=22.18"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.build.json && node bundle-editor.mjs && node bundle-mcp.mjs",