@hypersoniclabs/helix-cli 0.1.25-helix3.518 → 0.1.25-helix3.520

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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/device.d.ts +16 -4
  3. package/dist/device.js +63 -2
  4. package/dist/device.js.map +1 -1
  5. package/dist/deviceActionValidation.js +33 -0
  6. package/dist/deviceActionValidation.js.map +1 -1
  7. package/dist/deviceGraphChecks.d.ts +63 -0
  8. package/dist/deviceGraphChecks.js +298 -0
  9. package/dist/deviceGraphChecks.js.map +1 -0
  10. package/dist/index.js +2 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/itemLod/artifact.d.ts +8 -2
  13. package/dist/itemLod/artifact.js +3 -2
  14. package/dist/itemLod/artifact.js.map +1 -1
  15. package/dist/itemLod/index.d.ts +3 -0
  16. package/dist/itemLod/index.js +3 -1
  17. package/dist/itemLod/index.js.map +1 -1
  18. package/dist/tsconfig.build.tsbuildinfo +1 -1
  19. package/package.json +1 -1
  20. package/plugins/beamng-vehicle/bin/__pycache__/beamng_abs.cpython-312.pyc +0 -0
  21. package/plugins/beamng-vehicle/bin/__pycache__/beamng_audio.cpython-312.pyc +0 -0
  22. package/plugins/beamng-vehicle/bin/__pycache__/beamng_engine.cpython-312.pyc +0 -0
  23. package/plugins/beamng-vehicle/bin/__pycache__/beamng_expression.cpython-312.pyc +0 -0
  24. package/plugins/beamng-vehicle/bin/__pycache__/beamng_jbeam.cpython-312.pyc +0 -0
  25. package/plugins/beamng-vehicle/bin/__pycache__/beamng_materials.cpython-312.pyc +0 -0
  26. package/plugins/beamng-vehicle/bin/__pycache__/beamng_stock_events.cpython-312.pyc +0 -0
  27. package/plugins/beamng-vehicle/bin/__pycache__/fmod_studio.cpython-312.pyc +0 -0
  28. package/plugins/beamng-vehicle/bin/__pycache__/fsb5.cpython-312.pyc +0 -0
  29. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio.cpython-312.pyc +0 -0
  30. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_ac.cpython-312.pyc +0 -0
  31. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_beamng.cpython-312.pyc +0 -0
  32. package/plugins/beamng-vehicle/bin/__pycache__/sim_audio_fixtures.cpython-312.pyc +0 -0
  33. package/plugins/beamng-vehicle/bin/__pycache__/test_beamng_engine.cpython-312.pyc +0 -0
  34. package/plugins/beamng-vehicle/bin/__pycache__/test_sim_audio.cpython-312.pyc +0 -0
package/README.md CHANGED
@@ -78,7 +78,7 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
78
78
  | `helix item thumbnail set <item-id> <image> [--json]` | Upload your own thumbnail (png/jpg/webp, up to 10 MiB, checked locally) for a published item (`PUT /api/v1/universal-items/<id>/thumbnail`, moderated). A creator upload stays in place across version moves until you run `thumbnail refresh --force`. 403 = you are not the item's owner. |
79
79
  | `helix item lods <packageId>[@version] \| --item <id> \| --glb <file> [--out <dir>] [--variant <id>] [--profile prop\|vehicle] [--api-url <url>] [--json]` | Compile the `helix.item-lod/1` Runtime Artifact for a published prop, Device or vehicle Package Version: up to two levels, LOD1 (props at most 25% of the triangles, vehicles max(5000, 10%), textures at most 512 px) and LOD2 (at most 300 triangles, textures at most 256 px), each its own GLB drawn in one primitive per render state (usually 1 or 2, via a baked material atlas) with its own textures (never larger than what they replace) and a measured `maxErrorM`, plus an 8-view 512 px octahedral impostor atlas. A level that does not cut half the triangles, or cannot be made strictly smaller in bytes (a vehicle LOD2 also: one over 25% of the extent from the source), is omitted. Content-addressed and deterministic; skinned input (avatars, add-ons) is refused. Local preview; the backend worker is the catalogue's source of truth. See [docs/item-lod.md](docs/item-lod.md). |
80
80
  | `helix device create <dir> --id <slug> --title <t> --model <mesh.glb> --template standing-desk\|clock\|television` | Scaffold a GLB-backed `helix.device/1` definition, including the interactive TV/media-player template. There is intentionally no website Device editor. |
81
- | `helix device validate [dir] [--json]` | Validate the definition, GLB bytes and named-node bindings, typed state/controls, capabilities, and Home+World compatibility, including [declarative character hand reach](docs/device-hand-reach.md). |
81
+ | `helix device validate [dir] [--json] [--room]` | Validate the definition, GLB bytes and named-node bindings, typed state/controls, capabilities, the runtime 512-step graph budget, `deadline` triggers, `fill` action args, and Home+World compatibility, including [declarative character hand reach](docs/device-hand-reach.md). `--room` also refuses state writes the room cannot run: any write reachable from an `interact` trigger through `emitSignal` handlers, `delay` or `branch` (the rule the backend applies when sealing a new publish). |
82
82
  | `helix device test [dir] [--mode single\|multiplayer\|both] [--json]` | Exercise every interaction, persistence remount, and deterministic one-/two-client authority convergence. |
83
83
  | `helix world scene-elements compile <input> --output <scene>` | Compile typed Scene v2 authoring, including exact Continuum Device pins, instances, and locally validated typed links. See [`examples/scene-elements-devices.json`](examples/scene-elements-devices.json). |
84
84
  | `helix preview-video <slug> <youtube-url>` | Set or clear a published world's YouTube preview. |
package/dist/device.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const DEVICE_INTERACTION_VERB_LIST: string;
23
23
  * fail `helix device validate` while the client loaded it happily. Add
24
24
  * capabilities to both lists, never to one.
25
25
  */
26
- export declare const DEVICE_CAPABILITIES: readonly ["model.glb", "presentation.nodeMotion", "clock.serverTime", "state.typed", "state.durable", "room.authority", "audio.playback", "presentation.light", "presentation.vfx", "character.posture", "character.animate", "character.hand", "signals.local", "screen.render", "screen.input", "app.web.local", "app.web.remote", "app.web.panel", "app.image.remote", "app.image.owner", "app.canvas", "device.ports", "physics.solid"];
26
+ export declare const DEVICE_CAPABILITIES: readonly ["model.glb", "presentation.nodeMotion", "clock.serverTime", "clock.deadline", "state.typed", "state.durable", "room.authority", "audio.playback", "presentation.light", "presentation.vfx", "character.posture", "character.animate", "character.hand", "signals.local", "screen.render", "screen.input", "app.web.local", "app.web.remote", "app.web.panel", "app.image.remote", "app.image.owner", "app.canvas", "device.ports", "physics.solid"];
27
27
  /**
28
28
  * The closed component vocabulary.
29
29
  *
@@ -56,8 +56,12 @@ export declare const DEVICE_GRAPH_STEP_KINDS: readonly ["setState", "toggleState
56
56
  export declare const DEVICE_HAND_SOCKETS: readonly ["hand_r.grip", "hand_l.grip"];
57
57
  /** The same vocabulary rendered as one readable list for error messages. */
58
58
  export declare const DEVICE_HAND_SOCKET_LIST: string;
59
- /** The closed trigger vocabulary (`validate.ts:224-243`). */
60
- export declare const DEVICE_TRIGGER_KINDS: readonly ["init", "interact", "stateChanged", "signal", "playerNear"];
59
+ /**
60
+ * The closed trigger vocabulary (`validate.ts:224-243`), plus `deadline`: an
61
+ * epoch-ms instant held in numeric state that fires once when the clock reaches
62
+ * it. Its `do` is the server-evaluable subset (see `validateDeadlineTrigger`).
63
+ */
64
+ export declare const DEVICE_TRIGGER_KINDS: readonly ["init", "interact", "stateChanged", "signal", "playerNear", "deadline"];
61
65
  /** The closed host vocabulary for `compatibility.hosts` (`validate.ts:555-560`). */
62
66
  export declare const DEVICE_HOSTS: readonly ["home", "world"];
63
67
  /** The same vocabulary rendered as one readable list for error messages. */
@@ -88,7 +92,15 @@ export interface DeviceTestReport {
88
92
  errors: string[];
89
93
  }
90
94
  export declare function readDeviceGlbNodeNames(file: string): string[];
91
- export declare function validateDeviceProject(input: string): DeviceValidationReport;
95
+ export interface DeviceValidateOptions {
96
+ /**
97
+ * Also apply the room (server) authority rule: refuse state writes reachable
98
+ * from an `interact` trigger through `emitSignal` handlers, `delay`, or
99
+ * `branch`. The backend applies the same rule when sealing a new publish.
100
+ */
101
+ room?: boolean;
102
+ }
103
+ export declare function validateDeviceProject(input: string, options?: DeviceValidateOptions): DeviceValidationReport;
92
104
  export declare function testDeviceProject(input: string, mode: 'single' | 'multiplayer'): DeviceTestReport;
93
105
  export declare function createDeviceProject(input: {
94
106
  directory: string;
package/dist/device.js CHANGED
@@ -8,6 +8,7 @@ exports.createDeviceProject = createDeviceProject;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = require("node:path");
10
10
  const deviceActionValidation_1 = require("./deviceActionValidation");
11
+ const deviceGraphChecks_1 = require("./deviceGraphChecks");
11
12
  var deviceActionValidation_2 = require("./deviceActionValidation");
12
13
  Object.defineProperty(exports, "deviceActionAstBudgetError", { enumerable: true, get: function () { return deviceActionValidation_2.deviceActionAstBudgetError; } });
13
14
  exports.DEVICE_FILENAME = 'device.json';
@@ -61,6 +62,7 @@ exports.DEVICE_CAPABILITIES = [
61
62
  'model.glb',
62
63
  'presentation.nodeMotion',
63
64
  'clock.serverTime',
65
+ 'clock.deadline',
64
66
  'state.typed',
65
67
  'state.durable',
66
68
  'room.authority',
@@ -142,13 +144,18 @@ exports.DEVICE_GRAPH_STEP_KINDS = [
142
144
  exports.DEVICE_HAND_SOCKETS = ['hand_r.grip', 'hand_l.grip'];
143
145
  /** The same vocabulary rendered as one readable list for error messages. */
144
146
  exports.DEVICE_HAND_SOCKET_LIST = exports.DEVICE_HAND_SOCKETS.join('|');
145
- /** The closed trigger vocabulary (`validate.ts:224-243`). */
147
+ /**
148
+ * The closed trigger vocabulary (`validate.ts:224-243`), plus `deadline`: an
149
+ * epoch-ms instant held in numeric state that fires once when the clock reaches
150
+ * it. Its `do` is the server-evaluable subset (see `validateDeadlineTrigger`).
151
+ */
146
152
  exports.DEVICE_TRIGGER_KINDS = [
147
153
  'init',
148
154
  'interact',
149
155
  'stateChanged',
150
156
  'signal',
151
157
  'playerNear',
158
+ 'deadline',
152
159
  ];
153
160
  /** The closed host vocabulary for `compatibility.hosts` (`validate.ts:555-560`). */
154
161
  exports.DEVICE_HOSTS = ['home', 'world'];
@@ -353,6 +360,39 @@ function validateGraphCondition(raw, context, path) {
353
360
  validateGraphExpr(node.right, context, `${path}.right`);
354
361
  }
355
362
  const DEVICE_GRAPH_MAX_BRANCH_DEPTH = 8;
363
+ /**
364
+ * The interaction `display` hint: `{ kind: 'time', stateKey }` renders an
365
+ * epoch-millisecond state value in the VIEWER's local time ("rings 7:00 AM"),
366
+ * and "Off" when it is 0 or absent. The hint is presentation only.
367
+ */
368
+ function validateInteractionDisplay(raw, id, state, errors) {
369
+ const path = `interaction ${id} display`;
370
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
371
+ errors.push(`${path} must be an object`);
372
+ return;
373
+ }
374
+ const display = object(raw);
375
+ if (display.kind !== 'time') {
376
+ errors.push(`${path}.kind must be "time"`);
377
+ }
378
+ for (const key of Object.keys(display)) {
379
+ if (key !== 'kind' && key !== 'stateKey') {
380
+ errors.push(`${path}: unknown field "${key}"`);
381
+ }
382
+ }
383
+ if (!isNonEmptyName(display.stateKey)) {
384
+ errors.push(`${path}.stateKey must be a non-empty state key`);
385
+ }
386
+ else {
387
+ const spec = object(state[String(display.stateKey)]);
388
+ if (state[String(display.stateKey)] === undefined) {
389
+ errors.push(`${path}.stateKey "${String(display.stateKey)}" is not declared in state`);
390
+ }
391
+ else if (spec.type !== 'float' && spec.type !== 'integer') {
392
+ errors.push(`${path}.stateKey "${String(display.stateKey)}" must be a float or integer state holding epoch milliseconds`);
393
+ }
394
+ }
395
+ }
356
396
  /**
357
397
  * Validate one behavior-graph step list, recursing into every nested list.
358
398
  *
@@ -1307,7 +1347,7 @@ function deviceWebPanelInteractionError(definition, interaction) {
1307
1347
  }
1308
1348
  return null;
1309
1349
  }
1310
- function validateDeviceProject(input) {
1350
+ function validateDeviceProject(input, options = {}) {
1311
1351
  const target = readDefinitionPath(input);
1312
1352
  const errors = [];
1313
1353
  const warnings = [];
@@ -1537,6 +1577,9 @@ function validateDeviceProject(input) {
1537
1577
  errors.push(`interaction ${id} requires a label`);
1538
1578
  else if (interaction.label.length > 40)
1539
1579
  errors.push(`interaction ${id} label must be 40 characters or fewer`);
1580
+ if (interaction.display !== undefined) {
1581
+ validateInteractionDisplay(interaction.display, id, state, errors);
1582
+ }
1540
1583
  if (interaction.maxDistanceM !== undefined) {
1541
1584
  if (!isFiniteNumber(interaction.maxDistanceM) ||
1542
1585
  interaction.maxDistanceM <= 0 ||
@@ -2242,6 +2285,8 @@ function validateDeviceProject(input) {
2242
2285
  errors,
2243
2286
  usage: graphUsage,
2244
2287
  };
2288
+ let deadlineTriggerUsed = false;
2289
+ const deadlineKeys = new Set();
2245
2290
  triggers.forEach((raw, index) => {
2246
2291
  const triggerPath = `graph.triggers.${index}`;
2247
2292
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
@@ -2265,6 +2310,15 @@ function validateDeviceProject(input) {
2265
2310
  if (!isNonEmptyName(trigger.key))
2266
2311
  errors.push(`${triggerPath}: stateChanged requires a non-empty key`);
2267
2312
  }
2313
+ else if (on === 'deadline') {
2314
+ if (typeof trigger.key === 'string' && deadlineKeys.has(trigger.key)) {
2315
+ errors.push(`${triggerPath}: deadline key "${trigger.key}" is declared by more than one deadline trigger`);
2316
+ }
2317
+ if (typeof trigger.key === 'string')
2318
+ deadlineKeys.add(trigger.key);
2319
+ deadlineTriggerUsed = true;
2320
+ errors.push(...(0, deviceGraphChecks_1.deviceDeadlineTriggerErrors)(trigger, triggerPath, state));
2321
+ }
2268
2322
  else if (on === 'signal') {
2269
2323
  if (!isNonEmptyName(trigger.name))
2270
2324
  errors.push(`${triggerPath}: signal requires a non-empty name`);
@@ -2309,6 +2363,12 @@ function validateDeviceProject(input) {
2309
2363
  }
2310
2364
  }
2311
2365
  });
2366
+ // The engine charges one counter for every step of every trigger, nested
2367
+ // steps included (`PortableDeviceSupport.unsupportedStep`); the per-action AST
2368
+ // budget above does not see the graph at all.
2369
+ errors.push(...(0, deviceGraphChecks_1.deviceGraphBudgetErrors)(triggers));
2370
+ if (options.room)
2371
+ errors.push(...(0, deviceGraphChecks_1.deviceRoomAuthorityErrors)(definition));
2312
2372
  // `compatibility` itself is optional on the host, and its `hosts` array is a
2313
2373
  // `min(1)` enum — NOT "both home and world". Demanding both here rejected a
2314
2374
  // world-only Device the host loads happily.
@@ -2366,6 +2426,7 @@ function validateDeviceProject(input) {
2366
2426
  Object.values(state).some((spec) => object(spec).persist === true),
2367
2427
  ],
2368
2428
  ['presentation.nodeMotion', graphUsage.nodeMotion],
2429
+ ['clock.deadline', deadlineTriggerUsed],
2369
2430
  [
2370
2431
  'clock.serverTime',
2371
2432
  components.some((component) => component.type === 'clockHands'),