@helix3/helix-cli 0.1.13-helix3.53 → 0.1.13-helix3.55

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 CHANGED
@@ -4,6 +4,12 @@
4
4
  backend. It shares bundle validation with the server via `@hypersoniclabs/helix-manifest`, so "validate passed but
5
5
  publish failed" can't happen for contract reasons.
6
6
 
7
+ ## Architecture boundary
8
+
9
+ All creator feature and operational logic, validation, file mutation, network behavior, and tests
10
+ live here. MCP packages may expose transport schemas, resources, instructions, and thin delegation
11
+ to these commands, but must not reimplement this logic.
12
+
7
13
  ## Install
8
14
 
9
15
  ```bash
@@ -35,8 +41,16 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
35
41
  | `helix init <dir>` | Scaffold a minimal Three.js world (`helix.json` + `index.html` + `main.js`). |
36
42
  | `helix install [--update]` | Resolve a world's `systems`/`abilities` pins (a v0.2 **or v0.3** manifest) → materialize the modules, the `three` import map, and `helix.runtime.ts`. |
37
43
  | `helix validate [dir]` | Validate a bundle locally — the exact rules the server enforces. |
38
- | `helix publish [dir]` | Validate → resolve/create the world → upload files → finalize → print the play URL. |
44
+ | `helix publish [dir] [--thumbnail <file>]` | Validate → resolve/create the world → upload files → finalize → set the cover image → print the play URL. |
39
45
  | `helix list` | List your worlds. |
46
+ | `helix item list-slots [--json]` | Print the Character-Creator vocabulary a wearable declares: the cosmetic slot tags, and the genders. |
47
+ | `helix item publish <mesh.glb> --title <t> [--kind <k>] [--slot <tag>] [--gender <g>] [--price-lix <n> \| --personal] [--thumbnail <f>] [--dry-run]` | Publish a universal item (wearable / avatar / home item) from one `.glb`; the server verifies the mesh inline. |
48
+ | `helix preview-video <slug> <youtube-url>` | Set or clear a published world's YouTube preview. |
49
+ | `helix thumbnail set <slug> <file>` | Upload a thumbnail or preview image through the CLI-owned media path. |
50
+ | `helix assets materials\|material\|search\|get\|install` | Discover platform materials and Vault assets, then install immutable assets with provenance receipts. |
51
+ | `helix assets generate\|resume\|generate-image\|generate-audio\|generate-environment` | Run Dreamer asset generation and resume existing jobs. |
52
+ | `helix assets credits\|check-loaders` | Render asset credits from provenance and reject models requiring unsupported loaders. |
53
+ | `helix world audit\|source-audit\|perf-gate\|prove-live` | Run the blocking world QA, performance, and deployed-build identity gates. |
40
54
  | `helix doctor [--project <dir>]` | Print the environment + whether the @helix toolchain (CLI/MCP/SDK/manifest) is current. |
41
55
 
42
56
  ## Publish flow
@@ -51,8 +65,35 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
51
65
  5. `POST /api/v1/instant-worlds/:id/builds/:buildId/finalize` → byte-exact verify, activate, publish.
52
66
  6. `GET /api/v1/instant-worlds/:slug` → the play URL.
53
67
 
54
- The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported from the package
55
- main, so an MCP server can drive the exact same code paths an agent would.
68
+ The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported for other CLI
69
+ modules. Agent integrations should instruct or delegate to the `helix` commands above rather than
70
+ reimplementing operational behavior in a transport package.
71
+
72
+ ## Item publish flow
73
+
74
+ `helix item publish` is a different shape from a world publish: one mesh, one round trip, an
75
+ inline verdict — which is what an editor (Helix Studio) needs when an artist clicks Publish.
76
+
77
+ 1. Local validation, before anything leaves the machine: the `--kind`, the Character-Creator
78
+ `--slot` and `--gender` (**both required** for a wearable, exact-match), the title/slug/tag
79
+ limits, and the GLB container magic. `--dry-run` stops here and prints exactly what would be
80
+ sent.
81
+ 2. `POST /api/v1/universal-items/upload` — `multipart/form-data` with a `mesh` file part, an
82
+ optional `thumbnail` file part, and the metadata as ONE JSON string in `payload`.
83
+ 3. The server verifies the mesh **inline** (triangles, texture edges, materials, and — for a
84
+ wearable — whether it is a skinned garment or a rigid socketed accessory) and returns the
85
+ created item together with the verification verdict and warnings.
86
+
87
+ The slot table in `src/item.ts` MIRRORS `helix-backend-api → src/universal-items/cc-wearable-slot.ts`,
88
+ and the gender list mirrors `src/universal-items/cc-gender.ts` the same way. They are a local fast
89
+ path so a typo costs no upload; the server remains the authority and its 400 lists every valid
90
+ value. Adding a slot or a gender means editing both, in the same PR wave.
91
+
92
+ `--gender` takes `male`, `female`, or `male,female`, and is **required** for `--kind wearable`.
93
+ There is no `both`/`unisex`/`all`: Unreal's cosmetics enum has exactly Male and Female, so a garment
94
+ that fits every body names both — one HELIX row with `{male, female}` is the web equivalent of
95
+ Unreal's two entries sharing one mesh. The field goes on the wire as `genders`; `ccGenders` is the
96
+ database column and the backend rejects it by name rather than dropping it in silence.
56
97
 
57
98
  ## Develop
58
99
 
package/dist/api.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare class HelixApi {
18
18
  setToken(token: string): void;
19
19
  request<T>(method: string, path: string, body?: unknown): Promise<T>;
20
20
  requestItem<T>(method: string, path: string, body?: unknown): Promise<T>;
21
+ postMultipart<T>(path: string, form: FormData): Promise<T>;
21
22
  static forCredentials(creds: CliCredentials): HelixApi;
22
23
  }
23
24
  export declare function uploadToTicket(url: string, body: Buffer, contentType: string, cacheControl?: string): Promise<void>;
package/dist/api.js CHANGED
@@ -59,6 +59,26 @@ class HelixApi {
59
59
  const envelope = await this.request(method, path, body);
60
60
  return envelope.item;
61
61
  }
62
+ // multipart/form-data POST — for the endpoints that take file parts alongside
63
+ // JSON (universal-item uploads). Deliberately does NOT set content-type: the
64
+ // boundary is generated by the runtime and a hand-written header loses it,
65
+ // which the server sees as a malformed body. Uses Node's built-in FormData /
66
+ // Blob (globals since Node 18, same undici that already backs `fetch` above),
67
+ // so multipart needs no dependency.
68
+ async postMultipart(path, form) {
69
+ const res = await fetch(`${this.baseUrl}${path}`, {
70
+ method: 'POST',
71
+ headers: this.token ? { authorization: `Bearer ${this.token}` } : {},
72
+ body: form,
73
+ });
74
+ if (!res.ok) {
75
+ const problem = (await res.json().catch(() => ({})));
76
+ throw new ApiError(res.status, problem);
77
+ }
78
+ if (res.status === 204)
79
+ return undefined;
80
+ return (await res.json());
81
+ }
62
82
  static forCredentials(creds) {
63
83
  return new HelixApi(creds.apiUrl, creds.token);
64
84
  }
package/dist/api.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const commander_1 = require("commander");
38
+ const node_child_process_1 = require("node:child_process");
38
39
  const node_fs_1 = require("node:fs");
39
40
  const node_path_1 = require("node:path");
40
41
  const lib_1 = require("./lib");
@@ -230,19 +231,53 @@ program
230
231
  .command('publish')
231
232
  .description('Publish a world to HELIX Instant')
232
233
  .argument('[directory]', 'bundle directory', '.')
233
- .action(async (directory) => {
234
+ .option('--thumbnail <file>', 'cover image (png, jpg/jpeg, webp, avif, or gif); otherwise uses thumbnail.<ext> at the bundle root')
235
+ .action(async (directory, opts) => {
234
236
  const creds = requireAuth();
235
237
  const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
236
238
  if (mismatch)
237
239
  fail(`✖ Refusing to publish:\n${mismatch}`);
238
240
  try {
239
- const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg));
241
+ const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg), { thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined });
240
242
  console.log(`✔ Published "${result.title}" build ${result.buildNumber}`);
241
243
  if (result.playUrl)
242
244
  console.log(` play: ${result.playUrl}`);
243
245
  console.log(` api: ${result.worldUrl}`);
244
- if (!result.hasCustomThumbnail) {
245
- console.log(` no custom thumbnail yet — run "helix screenshot" in this world's dir, then "helix thumbnail set ${result.slug} <file>"`);
246
+ if (result.thumbnailSource !== 'generated') {
247
+ console.log(` cover: ${result.thumbnailSource}`);
248
+ }
249
+ if (result.thumbnailWarning)
250
+ console.warn(` • ${result.thumbnailWarning}`);
251
+ }
252
+ catch (err) {
253
+ fail(err instanceof lib_1.ApiError
254
+ ? err.render()
255
+ : `✖ ${err instanceof Error ? err.message : String(err)}`);
256
+ }
257
+ });
258
+ program
259
+ .command('preview-video')
260
+ .description("Set (or clear) a world's preview video from a YouTube link")
261
+ .argument('<slug>', 'world slug (as in `helix publish` / list-my-worlds)')
262
+ .argument('[url]', 'YouTube link — watch?v=, youtu.be, /embed/, /shorts/ or a bare video id. Omit with --clear.')
263
+ .option('--clear', "remove the world's preview video")
264
+ .action(async (slug, url, opts) => {
265
+ const creds = requireAuth();
266
+ // Video is referenced, never uploaded: the preview slot takes images only,
267
+ // and YouTube does the transcoding/streaming/moderation.
268
+ if (!opts.clear && !url)
269
+ fail('✖ Pass a YouTube link, or --clear to remove the video.');
270
+ try {
271
+ const result = opts.clear
272
+ ? await (0, lib_1.clearWorldPreviewVideo)(slug, creds)
273
+ : await (0, lib_1.setWorldPreviewVideo)(slug, url, creds);
274
+ if (result.previewYoutubeId) {
275
+ console.log(`✔ Preview video set for "${result.title}"`);
276
+ console.log(` video: ${result.previewYoutubeId}`);
277
+ console.log(` embed: ${result.previewYoutubeUrl}`);
278
+ }
279
+ else {
280
+ console.log(`✔ Preview video cleared for "${result.title}"`);
246
281
  }
247
282
  }
248
283
  catch (err) {
@@ -270,6 +305,401 @@ program
270
305
  : `✖ ${err instanceof Error ? err.message : String(err)}`);
271
306
  }
272
307
  });
308
+ // --- helix item ------------------------------------------------------------
309
+ // Universal items (wearables, avatars, home items) uploaded straight from a DCC
310
+ // export — the surface Helix Studio drives. Publishing an item is not
311
+ // publishing a world: one mesh, one multipart round trip, an inline verdict.
312
+ const item = program
313
+ .command('item')
314
+ .description('Universal items — publish a wearable / avatar / home item from a .glb');
315
+ // One command prints the whole Character-Creator vocabulary a wearable must
316
+ // declare — the 34 slot tags AND the two genders. There is no `list-genders`:
317
+ // a dedicated command for a closed two-value list is a command to discover, to
318
+ // document and to keep in sync, for information that fits on one line under the
319
+ // list it is always read with. `--gender`'s own help carries the same values.
320
+ item
321
+ .command('list-slots')
322
+ .description('Print the Character-Creator vocabulary a wearable declares: the cosmetic slot tags, and the genders')
323
+ .option('--json', 'machine-readable output')
324
+ .action((opts) => {
325
+ if (opts.json) {
326
+ console.log(JSON.stringify({ slots: lib_1.CC_WEARABLE_SLOT_GROUPS, genders: lib_1.CC_GENDERS }, null, 2));
327
+ return;
328
+ }
329
+ console.log('Character-Creator cosmetic slots — pass one to `helix item publish --slot`.');
330
+ console.log('Tags match EXACTLY, including case.\n');
331
+ console.log((0, lib_1.renderCcSlotTable)());
332
+ console.log('\nGenders — pass to `helix item publish --gender`, REQUIRED for a wearable.');
333
+ console.log(` ${(0, lib_1.renderCcGenderList)()}`);
334
+ });
335
+ function parseItemKind(value) {
336
+ if (!(0, lib_1.isUploadableItemKind)(value)) {
337
+ fail(`✖ --kind must be one of ${lib_1.UPLOADABLE_ITEM_KINDS.join(', ')}, got '${value}'`);
338
+ }
339
+ return value;
340
+ }
341
+ function parsePriceLix(value) {
342
+ const price = Number(value);
343
+ if (!Number.isFinite(price))
344
+ fail(`✖ --price-lix must be a number, got '${value}'`);
345
+ return price;
346
+ }
347
+ item
348
+ .command('publish')
349
+ .description('Publish a universal item from a local .glb — the server verifies the mesh inline and returns the verdict in the same round trip')
350
+ .argument('<mesh>', 'the item mesh (binary glTF, .glb)')
351
+ .requiredOption('--title <title>', 'display title')
352
+ .option('--kind <kind>', `item kind: ${lib_1.UPLOADABLE_ITEM_KINDS.join(' | ')}`, 'wearable')
353
+ .option('--slot <tag>', 'Character-Creator cosmetic slot, REQUIRED for a wearable (see: helix item list-slots)')
354
+ // Comma form, not a repeated flag and not a `both` alias. The repo already
355
+ // spells "a list" as one comma-separated value (--tags a,b,c / --views a,b /
356
+ // --lod-tris a,b,c) and has no repeatable option anywhere, and the backend's
357
+ // own multipart field takes the identical "male,female" string — so this adds
358
+ // no vocabulary and no mechanism. A `both` alias was rejected: it would be a
359
+ // token the API does not have, and the server's errors name `male`/`female`.
360
+ .option('--gender <male|female|male,female>', 'Character-Creator gender(s) the item is authored for, REQUIRED for a wearable; a garment that fits every body passes male,female')
361
+ .option('--slug <slug>', 'url slug (default: derived from the title)')
362
+ .option('--description <text>', 'item description')
363
+ .option('--thumbnail <file>', 'thumbnail image (png/jpg/webp); rendered from the mesh when omitted, required for an avatar')
364
+ .option('--price-lix <n>', 'list on the marketplace at this LIX price (charges a non-refundable publish fee)')
365
+ .option('--personal', 'keep it private and free instead of listing it (no publish fee)')
366
+ .option('--tags <a,b,c>', 'comma-separated search keywords')
367
+ .option('--allow-oversize', 'relax the mesh size/polycount ceilings for a deliberately heavy asset')
368
+ .option('--dry-run', 'validate everything locally and print what WOULD be sent; no upload, no fee')
369
+ .option('--json', 'machine-readable output')
370
+ .action(async (mesh, opts) => {
371
+ const publishOptions = {
372
+ mesh: (0, node_path_1.resolve)(mesh),
373
+ thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined,
374
+ kind: parseItemKind(opts.kind),
375
+ title: opts.title,
376
+ slot: opts.slot,
377
+ genders: opts.gender !== undefined ? (0, lib_1.parseGenders)(opts.gender) : undefined,
378
+ slug: opts.slug,
379
+ description: opts.description,
380
+ priceLix: opts.priceLix !== undefined ? parsePriceLix(opts.priceLix) : null,
381
+ keepPersonal: opts.personal === true,
382
+ tags: opts.tags !== undefined ? (0, lib_1.parseTags)(opts.tags) : undefined,
383
+ allowOversize: opts.allowOversize === true,
384
+ };
385
+ // --dry-run stops before ANY network call, so it works logged out — the
386
+ // point is to check the mesh and the metadata, not the account.
387
+ if (opts.dryRun) {
388
+ try {
389
+ const plan = (0, lib_1.planItemPublish)(publishOptions);
390
+ if (opts.json) {
391
+ console.log(JSON.stringify({
392
+ dryRun: true,
393
+ endpoint: plan.endpoint,
394
+ payload: plan.payload,
395
+ mesh: plan.mesh,
396
+ thumbnail: plan.thumbnail,
397
+ warnings: plan.warnings,
398
+ }, null, 2));
399
+ return;
400
+ }
401
+ console.log(`✔ Dry run — nothing uploaded, no fee charged.`);
402
+ console.log(` POST ${plan.endpoint}`);
403
+ console.log(` mesh: ${plan.mesh.file} (${(plan.mesh.sizeBytes / 1024).toFixed(1)} KiB, ${plan.mesh.contentType})`);
404
+ console.log(` thumbnail: ${plan.thumbnail ? `${plan.thumbnail.file} (${(plan.thumbnail.sizeBytes / 1024).toFixed(1)} KiB, ${plan.thumbnail.contentType})` : '(none — server-rendered)'}`);
405
+ console.log(` payload: ${JSON.stringify(plan.payload, null, 2).replace(/\n/g, '\n ')}`);
406
+ for (const warning of plan.warnings)
407
+ console.log(` • ${warning}`);
408
+ const status = (0, lib_1.authStatus)();
409
+ if (!status.loggedIn)
410
+ console.log(' • not logged in — run `helix login` before the real publish');
411
+ }
412
+ catch (err) {
413
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
414
+ }
415
+ return;
416
+ }
417
+ const creds = requireAuth();
418
+ const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
419
+ if (mismatch)
420
+ fail(`✖ Refusing to publish:\n${mismatch}`);
421
+ try {
422
+ const result = await (0, lib_1.publishItem)(publishOptions, creds, {
423
+ onProgress: (msg) => !opts.json && console.log(msg),
424
+ });
425
+ if (opts.json) {
426
+ console.log(JSON.stringify(result, null, 2));
427
+ return;
428
+ }
429
+ console.log(`✔ Published ${result.kind} "${result.title}" (${result.slug})`);
430
+ if (result.slot)
431
+ console.log(` slot: ${result.slot}`);
432
+ // Echoed from the SERVER's row, not from what was sent — so a value the
433
+ // server normalised, dropped or never stored is visible here.
434
+ if (result.genders.length)
435
+ console.log(` gender: ${result.genders.join(', ')}`);
436
+ console.log(` status: ${result.status}`);
437
+ console.log(result.priceLix === null
438
+ ? ' price: (personal — not listed)'
439
+ : ` price: ${result.priceLix} LIX (publish fee ${result.publishFeeLix} LIX)`);
440
+ console.log(` id: ${result.id}`);
441
+ for (const warning of result.verification.warnings)
442
+ console.log(` ⚠ ${warning}`);
443
+ }
444
+ catch (err) {
445
+ fail(err instanceof lib_1.ApiError
446
+ ? err.render()
447
+ : `✖ ${err instanceof Error ? err.message : String(err)}`);
448
+ }
449
+ });
450
+ // --- helix assets ----------------------------------------------------------
451
+ // Operational asset work lives here. MCP surfaces may point agents at these
452
+ // commands, but they must not reimplement Vault, Dreamer, download, or receipt
453
+ // behavior in the transport package.
454
+ const assets = program
455
+ .command('assets')
456
+ .description('Discover, install, and generate HELIX platform assets');
457
+ function optionalNumber(value, name) {
458
+ if (value === undefined)
459
+ return undefined;
460
+ const parsed = Number(value);
461
+ if (!Number.isFinite(parsed))
462
+ fail(`✖ ${name} must be a number, got '${value}'`);
463
+ return parsed;
464
+ }
465
+ function commaList(value) {
466
+ const values = value
467
+ ?.split(',')
468
+ .map((entry) => entry.trim())
469
+ .filter(Boolean);
470
+ return values?.length ? values : undefined;
471
+ }
472
+ function printJson(value) {
473
+ console.log(JSON.stringify(value, null, 2));
474
+ }
475
+ function runBundledAudit(script, args) {
476
+ const result = (0, node_child_process_1.spawnSync)(process.execPath, [(0, node_path_1.resolve)(__dirname, '..', 'vendor', 'audits', script), ...args], { stdio: 'inherit' });
477
+ if (result.error)
478
+ fail(`✖ ${result.error.message}`);
479
+ if (result.status !== 0)
480
+ process.exit(result.status ?? 1);
481
+ }
482
+ function addBundledAudit(parent, audit) {
483
+ parent
484
+ .command(audit.name)
485
+ .description(audit.description)
486
+ .usage(audit.usage)
487
+ .argument('[args...]', 'audit options and paths')
488
+ .allowUnknownOption(true)
489
+ .action((args) => runBundledAudit(audit.script, args));
490
+ }
491
+ addBundledAudit(assets, {
492
+ name: 'credits',
493
+ description: 'Render ASSET-CREDITS.md from the world provenance table',
494
+ script: 'asset-credits.mjs',
495
+ usage: '[--assets <file>] [--out <file>] [--title <text>]',
496
+ });
497
+ addBundledAudit(assets, {
498
+ name: 'check-loaders',
499
+ description: 'Reject GLTF assets that require unsupported Draco or meshopt loaders',
500
+ script: 'check-loaders.mjs',
501
+ usage: '[path ...]',
502
+ });
503
+ assets
504
+ .command('materials')
505
+ .description('Search the versioned HELIX material catalog')
506
+ .option('-q, --query <text>', 'search id, name, category, and tags')
507
+ .option('--category <category>', 'exact material category')
508
+ .option('--limit <n>', 'maximum results')
509
+ .action(async (opts) => {
510
+ try {
511
+ printJson(await (0, lib_1.listMaterials)(config_1.DEFAULT_API_URL, {
512
+ q: opts.query,
513
+ category: opts.category,
514
+ limit: optionalNumber(opts.limit, '--limit'),
515
+ }));
516
+ }
517
+ catch (err) {
518
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
519
+ }
520
+ });
521
+ assets
522
+ .command('material')
523
+ .description('Resolve one material id to its immutable map URLs')
524
+ .argument('<id>', 'material id from `helix assets materials`')
525
+ .action(async (id) => {
526
+ try {
527
+ printJson(await (0, lib_1.resolveMaterial)(config_1.DEFAULT_API_URL, id));
528
+ }
529
+ catch (err) {
530
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
531
+ }
532
+ });
533
+ assets
534
+ .command('search')
535
+ .description('Search reusable HELIX Vault assets')
536
+ .argument('[query]', 'free-text search')
537
+ .option('--kind <a,b>', 'comma-separated kinds')
538
+ .option('--skeleton <id>', 'required skeleton')
539
+ .option('--engine <engine>', 'target engine')
540
+ .option('--license <license>', 'license filter')
541
+ .option('--source <source>', 'source filter')
542
+ .option('--origin <origin>', 'origin/provenance filter')
543
+ .option('--creator-id <id>', 'creator id')
544
+ .option('--page <n>', 'page number')
545
+ .option('--limit <n>', 'page size')
546
+ .action(async (query, opts) => {
547
+ const creds = (0, config_1.loadCredentials)();
548
+ const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
549
+ try {
550
+ printJson(await (0, lib_1.searchVaultAssets)(apiUrl, {
551
+ q: query,
552
+ kind: commaList(opts.kind),
553
+ skeleton: opts.skeleton,
554
+ engine: opts.engine,
555
+ license: opts.license,
556
+ source: opts.source,
557
+ origin: opts.origin,
558
+ creatorId: opts.creatorId,
559
+ page: optionalNumber(opts.page, '--page'),
560
+ limit: optionalNumber(opts.limit, '--limit'),
561
+ }, creds ?? undefined));
562
+ }
563
+ catch (err) {
564
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
565
+ }
566
+ });
567
+ assets
568
+ .command('get')
569
+ .description('Read one HELIX Vault asset')
570
+ .argument('<asset-id>', 'Vault asset UUID')
571
+ .action(async (assetId) => {
572
+ const creds = (0, config_1.loadCredentials)();
573
+ const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
574
+ try {
575
+ printJson(await (0, lib_1.getVaultAsset)(apiUrl, assetId, creds ?? undefined));
576
+ }
577
+ catch (err) {
578
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
579
+ }
580
+ });
581
+ assets
582
+ .command('install')
583
+ .description('Install one Vault artifact into a world and write its provenance receipt')
584
+ .argument('<asset-id>', 'Vault asset UUID')
585
+ .option('-C, --world <dir>', 'world project directory', '.')
586
+ .action(async (assetId, opts) => {
587
+ const creds = (0, config_1.loadCredentials)();
588
+ const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
589
+ try {
590
+ printJson(await (0, lib_1.installVaultAsset)((0, node_path_1.resolve)(opts.world), apiUrl, assetId, creds ?? undefined));
591
+ }
592
+ catch (err) {
593
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
594
+ }
595
+ });
596
+ assets
597
+ .command('generate')
598
+ .description('Generate and publish a new prop or character through Dreamer')
599
+ .argument('<kind>', 'prop or character')
600
+ .argument('<prompt>', 'asset prompt')
601
+ .option('--title <title>', 'asset title')
602
+ .option('--additional-prompt <text>', 'additional generation guidance')
603
+ .option('--target-polycount <n>', 'target triangle count')
604
+ .option('--visibility <visibility>', 'public, unlisted, or private', 'public')
605
+ .action(async (kind, prompt, opts) => {
606
+ if (kind !== 'prop' && kind !== 'character') {
607
+ fail(`✖ kind must be prop or character, got '${kind}'`);
608
+ }
609
+ if (!['public', 'unlisted', 'private'].includes(opts.visibility)) {
610
+ fail(`✖ --visibility must be public, unlisted, or private`);
611
+ }
612
+ const creds = requireAuth();
613
+ try {
614
+ printJson(await (0, lib_1.generateAndPublishAsset)(creds, {
615
+ kind,
616
+ prompt,
617
+ title: opts.title,
618
+ additionalPrompt: opts.additionalPrompt,
619
+ targetPolycount: optionalNumber(opts.targetPolycount, '--target-polycount'),
620
+ }, {
621
+ visibility: opts.visibility,
622
+ }));
623
+ }
624
+ catch (err) {
625
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
626
+ }
627
+ });
628
+ assets
629
+ .command('resume')
630
+ .description('Resume and publish an existing Dreamer asset job')
631
+ .argument('<job-id>', 'Dreamer job id')
632
+ .option('--visibility <visibility>', 'public, unlisted, or private', 'public')
633
+ .action(async (jobId, opts) => {
634
+ if (!['public', 'unlisted', 'private'].includes(opts.visibility)) {
635
+ fail(`✖ --visibility must be public, unlisted, or private`);
636
+ }
637
+ const creds = requireAuth();
638
+ try {
639
+ printJson(await (0, lib_1.resumeAndPublishAsset)(creds, jobId, {
640
+ visibility: opts.visibility,
641
+ }));
642
+ }
643
+ catch (err) {
644
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
645
+ }
646
+ });
647
+ assets
648
+ .command('generate-image')
649
+ .description('Generate a standalone image through Dreamer')
650
+ .argument('<prompt>', 'image prompt')
651
+ .option('--additional-prompt <text>', 'additional generation guidance')
652
+ .option('--project-id <id>', 'Dreamer project id')
653
+ .action(async (prompt, opts) => {
654
+ const creds = requireAuth();
655
+ try {
656
+ printJson(await (0, lib_1.generateImage)(creds, {
657
+ prompt,
658
+ additionalPrompt: opts.additionalPrompt,
659
+ projectId: opts.projectId,
660
+ }));
661
+ }
662
+ catch (err) {
663
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
664
+ }
665
+ });
666
+ for (const generation of [
667
+ { command: 'generate-audio', kind: 'audio' },
668
+ { command: 'generate-environment', kind: 'gaussian_splat' },
669
+ ]) {
670
+ const command = assets
671
+ .command(generation.command)
672
+ .description(generation.kind === 'audio'
673
+ ? 'Generate standalone audio through Dreamer'
674
+ : 'Generate an experimental Gaussian-splat environment through Dreamer')
675
+ .argument('<prompt>', 'generation prompt')
676
+ .option('--additional-prompt <text>', 'additional generation guidance')
677
+ .option('--project-id <id>', 'Dreamer project id')
678
+ .option('--title <title>', 'asset title');
679
+ if (generation.kind === 'audio') {
680
+ command
681
+ .option('--loop', 'request loopable output')
682
+ .option('--duration-seconds <n>', 'requested duration');
683
+ }
684
+ command
685
+ .action(async (prompt, opts) => {
686
+ const creds = requireAuth();
687
+ try {
688
+ printJson(await (0, lib_1.generateStandaloneAsset)(creds, {
689
+ kind: generation.kind,
690
+ prompt,
691
+ additionalPrompt: opts.additionalPrompt,
692
+ projectId: opts.projectId,
693
+ title: opts.title,
694
+ loop: opts.loop,
695
+ durationSeconds: optionalNumber(opts.durationSeconds, '--duration-seconds'),
696
+ }));
697
+ }
698
+ catch (err) {
699
+ fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
700
+ }
701
+ });
702
+ }
273
703
  async function printCatalog(kind, system, apiUrlOpt) {
274
704
  const apiUrl = apiUrlOpt ?? (0, config_1.loadCredentials)()?.apiUrl ?? config_1.DEFAULT_API_URL;
275
705
  try {
@@ -532,6 +962,30 @@ gesture
532
962
  const world = program
533
963
  .command('world')
534
964
  .description("World layout tools — measure a built world's geometry as data instead of eyeballing screenshots");
965
+ addBundledAudit(world, {
966
+ name: 'audit',
967
+ description: 'Run the blocking composition, bundle-budget, loader, and provenance audit',
968
+ script: 'world-audit.mjs',
969
+ usage: '[--snapshot <file>] [--dist <dir>] [--root <dir>] [--assets <file>] [--json]',
970
+ });
971
+ addBundledAudit(world, {
972
+ name: 'source-audit',
973
+ description: 'Run the blocking lighting, materials, audio, mobile, and authority audit',
974
+ script: 'source-audit.mjs',
975
+ usage: '[--src <dir>] [--dist <dir>] [--manifest <file>] [--genre <name>] [--multiplayer] [--console <file>] [--perf <file>] [--json]',
976
+ });
977
+ addBundledAudit(world, {
978
+ name: 'perf-gate',
979
+ description: 'Measure real-GPU frame time and enforce the blocking performance budget',
980
+ script: 'perf-gate.mjs',
981
+ usage: '--url <url> [--seconds <n>] [--width <n>] [--height <n>] [--dpr <n>] [--angle <name>] [--label <text>] [--dist <dir>] [--json <file>] [--no-resize-probe] [--playwright <path>] [--quiet]',
982
+ });
983
+ addBundledAudit(world, {
984
+ name: 'prove-live',
985
+ description: 'Prove the deployed world matches the local published bundle',
986
+ script: 'prove-live.mjs',
987
+ usage: '--url <url> --world-api <url> --build <n> [--dist <dir>] [--entry <file>] [--spot <n>] [--timeout <ms>] [--perf <file>] [--json]',
988
+ });
535
989
  world
536
990
  .command('inspect')
537
991
  .description('Snapshot a built world headlessly and report layout anomalies: floating/sunk/intersecting geometry, collider-vs-visual divergence, stray transforms. ' +