@helix3/helix-cli 0.1.13-helix3.53 → 0.1.13-helix3.54
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 -3
- package/dist/api.d.ts +1 -0
- package/dist/api.js +20 -0
- package/dist/api.js.map +1 -1
- package/dist/index.js +401 -4
- package/dist/index.js.map +1 -1
- package/dist/init.js +65 -7
- package/dist/init.js.map +1 -1
- package/dist/item.d.ts +135 -0
- package/dist/item.js +481 -0
- package/dist/item.js.map +1 -0
- package/dist/lib.d.ts +100 -1
- package/dist/lib.js +210 -7
- package/dist/lib.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
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,14 @@ 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. |
|
|
40
52
|
| `helix doctor [--project <dir>]` | Print the environment + whether the @helix toolchain (CLI/MCP/SDK/manifest) is current. |
|
|
41
53
|
|
|
42
54
|
## Publish flow
|
|
@@ -51,8 +63,35 @@ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
|
|
|
51
63
|
5. `POST /api/v1/instant-worlds/:id/builds/:buildId/finalize` → byte-exact verify, activate, publish.
|
|
52
64
|
6. `GET /api/v1/instant-worlds/:slug` → the play URL.
|
|
53
65
|
|
|
54
|
-
The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported
|
|
55
|
-
|
|
66
|
+
The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported for other CLI
|
|
67
|
+
modules. Agent integrations should instruct or delegate to the `helix` commands above rather than
|
|
68
|
+
reimplementing operational behavior in a transport package.
|
|
69
|
+
|
|
70
|
+
## Item publish flow
|
|
71
|
+
|
|
72
|
+
`helix item publish` is a different shape from a world publish: one mesh, one round trip, an
|
|
73
|
+
inline verdict — which is what an editor (Helix Studio) needs when an artist clicks Publish.
|
|
74
|
+
|
|
75
|
+
1. Local validation, before anything leaves the machine: the `--kind`, the Character-Creator
|
|
76
|
+
`--slot` and `--gender` (**both required** for a wearable, exact-match), the title/slug/tag
|
|
77
|
+
limits, and the GLB container magic. `--dry-run` stops here and prints exactly what would be
|
|
78
|
+
sent.
|
|
79
|
+
2. `POST /api/v1/universal-items/upload` — `multipart/form-data` with a `mesh` file part, an
|
|
80
|
+
optional `thumbnail` file part, and the metadata as ONE JSON string in `payload`.
|
|
81
|
+
3. The server verifies the mesh **inline** (triangles, texture edges, materials, and — for a
|
|
82
|
+
wearable — whether it is a skinned garment or a rigid socketed accessory) and returns the
|
|
83
|
+
created item together with the verification verdict and warnings.
|
|
84
|
+
|
|
85
|
+
The slot table in `src/item.ts` MIRRORS `helix-backend-api → src/universal-items/cc-wearable-slot.ts`,
|
|
86
|
+
and the gender list mirrors `src/universal-items/cc-gender.ts` the same way. They are a local fast
|
|
87
|
+
path so a typo costs no upload; the server remains the authority and its 400 lists every valid
|
|
88
|
+
value. Adding a slot or a gender means editing both, in the same PR wave.
|
|
89
|
+
|
|
90
|
+
`--gender` takes `male`, `female`, or `male,female`, and is **required** for `--kind wearable`.
|
|
91
|
+
There is no `both`/`unisex`/`all`: Unreal's cosmetics enum has exactly Male and Female, so a garment
|
|
92
|
+
that fits every body names both — one HELIX row with `{male, female}` is the web equivalent of
|
|
93
|
+
Unreal's two entries sharing one mesh. The field goes on the wire as `genders`; `ccGenders` is the
|
|
94
|
+
database column and the backend rejects it by name rather than dropping it in silence.
|
|
56
95
|
|
|
57
96
|
## Develop
|
|
58
97
|
|
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":";;;
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":";;;AAgGA,wCAmBC;AAvGD,MAAa,QAAS,SAAQ,KAAK;IAEtB;IACA;IAFX,YACW,MAAc,EACd,IAAmB;QAE5B,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,MAAM,EAAE,CAAC,CAAC;QAHrD,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAe;IAG9B,CAAC;IAEO,MAAM,CAAC,QAAQ,CAAC,IAAmB;QACzC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QACrD,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;YAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5D,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,gCAAgC;IAChC,MAAM;QACJ,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC;QAC3C,MAAM,KAAK,GAAG,CAAC,KAAK,OAAO,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAChD,KAAK,MAAM,GAAG,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;QACzE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;CACF;AArBD,4BAqBC;AAED,MAAa,QAAQ;IAEA;IACT;IAFV,YACmB,OAAe,EACxB,QAAuB,IAAI;QADlB,YAAO,GAAP,OAAO,CAAQ;QACxB,UAAK,GAAL,KAAK,CAAsB;IAClC,CAAC;IAEJ,QAAQ,CAAC,KAAa;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;YAChD,MAAM;YACN,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACjE;YACD,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC5D,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAkB,CAAC;YACtE,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,SAAc,CAAC;QAC9C,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;IACjC,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,WAAW,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAkB,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACzE,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,8EAA8E;IAC9E,oCAAoC;IACpC,KAAK,CAAC,aAAa,CAAI,IAAY,EAAE,IAAc;QACjD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE;YACpE,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAkB,CAAC;YACtE,MAAM,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,SAAc,CAAC;QAC9C,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;IACjC,CAAC;IAED,MAAM,CAAC,cAAc,CAAC,KAAqB;QACzC,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;CACF;AAxDD,4BAwDC;AAED,yEAAyE;AACzE,6EAA6E;AAC7E,gEAAgE;AACzD,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,IAAY,EACZ,WAAmB,EACnB,YAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC;QAC1B,OAAO,EAAE;YACP,cAAc,EAAE,WAAW;YAC3B,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D;KACF,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,kBAAkB,GAAG,CAAC,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CACrE,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -230,19 +230,53 @@ program
|
|
|
230
230
|
.command('publish')
|
|
231
231
|
.description('Publish a world to HELIX Instant')
|
|
232
232
|
.argument('[directory]', 'bundle directory', '.')
|
|
233
|
-
.
|
|
233
|
+
.option('--thumbnail <file>', 'cover image (png, jpg/jpeg, webp, avif, or gif); otherwise uses thumbnail.<ext> at the bundle root')
|
|
234
|
+
.action(async (directory, opts) => {
|
|
234
235
|
const creds = requireAuth();
|
|
235
236
|
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
236
237
|
if (mismatch)
|
|
237
238
|
fail(`✖ Refusing to publish:\n${mismatch}`);
|
|
238
239
|
try {
|
|
239
|
-
const result = await (0, lib_1.publishWorld)((0, node_path_1.resolve)(directory), creds, (msg) => console.log(msg));
|
|
240
|
+
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
241
|
console.log(`✔ Published "${result.title}" build ${result.buildNumber}`);
|
|
241
242
|
if (result.playUrl)
|
|
242
243
|
console.log(` play: ${result.playUrl}`);
|
|
243
244
|
console.log(` api: ${result.worldUrl}`);
|
|
244
|
-
if (
|
|
245
|
-
console.log(`
|
|
245
|
+
if (result.thumbnailSource !== 'generated') {
|
|
246
|
+
console.log(` cover: ${result.thumbnailSource}`);
|
|
247
|
+
}
|
|
248
|
+
if (result.thumbnailWarning)
|
|
249
|
+
console.warn(` • ${result.thumbnailWarning}`);
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
fail(err instanceof lib_1.ApiError
|
|
253
|
+
? err.render()
|
|
254
|
+
: `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
program
|
|
258
|
+
.command('preview-video')
|
|
259
|
+
.description("Set (or clear) a world's preview video from a YouTube link")
|
|
260
|
+
.argument('<slug>', 'world slug (as in `helix publish` / list-my-worlds)')
|
|
261
|
+
.argument('[url]', 'YouTube link — watch?v=, youtu.be, /embed/, /shorts/ or a bare video id. Omit with --clear.')
|
|
262
|
+
.option('--clear', "remove the world's preview video")
|
|
263
|
+
.action(async (slug, url, opts) => {
|
|
264
|
+
const creds = requireAuth();
|
|
265
|
+
// Video is referenced, never uploaded: the preview slot takes images only,
|
|
266
|
+
// and YouTube does the transcoding/streaming/moderation.
|
|
267
|
+
if (!opts.clear && !url)
|
|
268
|
+
fail('✖ Pass a YouTube link, or --clear to remove the video.');
|
|
269
|
+
try {
|
|
270
|
+
const result = opts.clear
|
|
271
|
+
? await (0, lib_1.clearWorldPreviewVideo)(slug, creds)
|
|
272
|
+
: await (0, lib_1.setWorldPreviewVideo)(slug, url, creds);
|
|
273
|
+
if (result.previewYoutubeId) {
|
|
274
|
+
console.log(`✔ Preview video set for "${result.title}"`);
|
|
275
|
+
console.log(` video: ${result.previewYoutubeId}`);
|
|
276
|
+
console.log(` embed: ${result.previewYoutubeUrl}`);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.log(`✔ Preview video cleared for "${result.title}"`);
|
|
246
280
|
}
|
|
247
281
|
}
|
|
248
282
|
catch (err) {
|
|
@@ -270,6 +304,369 @@ program
|
|
|
270
304
|
: `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
271
305
|
}
|
|
272
306
|
});
|
|
307
|
+
// --- helix item ------------------------------------------------------------
|
|
308
|
+
// Universal items (wearables, avatars, home items) uploaded straight from a DCC
|
|
309
|
+
// export — the surface Helix Studio drives. Publishing an item is not
|
|
310
|
+
// publishing a world: one mesh, one multipart round trip, an inline verdict.
|
|
311
|
+
const item = program
|
|
312
|
+
.command('item')
|
|
313
|
+
.description('Universal items — publish a wearable / avatar / home item from a .glb');
|
|
314
|
+
// One command prints the whole Character-Creator vocabulary a wearable must
|
|
315
|
+
// declare — the 34 slot tags AND the two genders. There is no `list-genders`:
|
|
316
|
+
// a dedicated command for a closed two-value list is a command to discover, to
|
|
317
|
+
// document and to keep in sync, for information that fits on one line under the
|
|
318
|
+
// list it is always read with. `--gender`'s own help carries the same values.
|
|
319
|
+
item
|
|
320
|
+
.command('list-slots')
|
|
321
|
+
.description('Print the Character-Creator vocabulary a wearable declares: the cosmetic slot tags, and the genders')
|
|
322
|
+
.option('--json', 'machine-readable output')
|
|
323
|
+
.action((opts) => {
|
|
324
|
+
if (opts.json) {
|
|
325
|
+
console.log(JSON.stringify({ slots: lib_1.CC_WEARABLE_SLOT_GROUPS, genders: lib_1.CC_GENDERS }, null, 2));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.log('Character-Creator cosmetic slots — pass one to `helix item publish --slot`.');
|
|
329
|
+
console.log('Tags match EXACTLY, including case.\n');
|
|
330
|
+
console.log((0, lib_1.renderCcSlotTable)());
|
|
331
|
+
console.log('\nGenders — pass to `helix item publish --gender`, REQUIRED for a wearable.');
|
|
332
|
+
console.log(` ${(0, lib_1.renderCcGenderList)()}`);
|
|
333
|
+
});
|
|
334
|
+
function parseItemKind(value) {
|
|
335
|
+
if (!(0, lib_1.isUploadableItemKind)(value)) {
|
|
336
|
+
fail(`✖ --kind must be one of ${lib_1.UPLOADABLE_ITEM_KINDS.join(', ')}, got '${value}'`);
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
function parsePriceLix(value) {
|
|
341
|
+
const price = Number(value);
|
|
342
|
+
if (!Number.isFinite(price))
|
|
343
|
+
fail(`✖ --price-lix must be a number, got '${value}'`);
|
|
344
|
+
return price;
|
|
345
|
+
}
|
|
346
|
+
item
|
|
347
|
+
.command('publish')
|
|
348
|
+
.description('Publish a universal item from a local .glb — the server verifies the mesh inline and returns the verdict in the same round trip')
|
|
349
|
+
.argument('<mesh>', 'the item mesh (binary glTF, .glb)')
|
|
350
|
+
.requiredOption('--title <title>', 'display title')
|
|
351
|
+
.option('--kind <kind>', `item kind: ${lib_1.UPLOADABLE_ITEM_KINDS.join(' | ')}`, 'wearable')
|
|
352
|
+
.option('--slot <tag>', 'Character-Creator cosmetic slot, REQUIRED for a wearable (see: helix item list-slots)')
|
|
353
|
+
// Comma form, not a repeated flag and not a `both` alias. The repo already
|
|
354
|
+
// spells "a list" as one comma-separated value (--tags a,b,c / --views a,b /
|
|
355
|
+
// --lod-tris a,b,c) and has no repeatable option anywhere, and the backend's
|
|
356
|
+
// own multipart field takes the identical "male,female" string — so this adds
|
|
357
|
+
// no vocabulary and no mechanism. A `both` alias was rejected: it would be a
|
|
358
|
+
// token the API does not have, and the server's errors name `male`/`female`.
|
|
359
|
+
.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')
|
|
360
|
+
.option('--slug <slug>', 'url slug (default: derived from the title)')
|
|
361
|
+
.option('--description <text>', 'item description')
|
|
362
|
+
.option('--thumbnail <file>', 'thumbnail image (png/jpg/webp); rendered from the mesh when omitted, required for an avatar')
|
|
363
|
+
.option('--price-lix <n>', 'list on the marketplace at this LIX price (charges a non-refundable publish fee)')
|
|
364
|
+
.option('--personal', 'keep it private and free instead of listing it (no publish fee)')
|
|
365
|
+
.option('--tags <a,b,c>', 'comma-separated search keywords')
|
|
366
|
+
.option('--allow-oversize', 'relax the mesh size/polycount ceilings for a deliberately heavy asset')
|
|
367
|
+
.option('--dry-run', 'validate everything locally and print what WOULD be sent; no upload, no fee')
|
|
368
|
+
.option('--json', 'machine-readable output')
|
|
369
|
+
.action(async (mesh, opts) => {
|
|
370
|
+
const publishOptions = {
|
|
371
|
+
mesh: (0, node_path_1.resolve)(mesh),
|
|
372
|
+
thumbnail: opts.thumbnail ? (0, node_path_1.resolve)(opts.thumbnail) : undefined,
|
|
373
|
+
kind: parseItemKind(opts.kind),
|
|
374
|
+
title: opts.title,
|
|
375
|
+
slot: opts.slot,
|
|
376
|
+
genders: opts.gender !== undefined ? (0, lib_1.parseGenders)(opts.gender) : undefined,
|
|
377
|
+
slug: opts.slug,
|
|
378
|
+
description: opts.description,
|
|
379
|
+
priceLix: opts.priceLix !== undefined ? parsePriceLix(opts.priceLix) : null,
|
|
380
|
+
keepPersonal: opts.personal === true,
|
|
381
|
+
tags: opts.tags !== undefined ? (0, lib_1.parseTags)(opts.tags) : undefined,
|
|
382
|
+
allowOversize: opts.allowOversize === true,
|
|
383
|
+
};
|
|
384
|
+
// --dry-run stops before ANY network call, so it works logged out — the
|
|
385
|
+
// point is to check the mesh and the metadata, not the account.
|
|
386
|
+
if (opts.dryRun) {
|
|
387
|
+
try {
|
|
388
|
+
const plan = (0, lib_1.planItemPublish)(publishOptions);
|
|
389
|
+
if (opts.json) {
|
|
390
|
+
console.log(JSON.stringify({
|
|
391
|
+
dryRun: true,
|
|
392
|
+
endpoint: plan.endpoint,
|
|
393
|
+
payload: plan.payload,
|
|
394
|
+
mesh: plan.mesh,
|
|
395
|
+
thumbnail: plan.thumbnail,
|
|
396
|
+
warnings: plan.warnings,
|
|
397
|
+
}, null, 2));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
console.log(`✔ Dry run — nothing uploaded, no fee charged.`);
|
|
401
|
+
console.log(` POST ${plan.endpoint}`);
|
|
402
|
+
console.log(` mesh: ${plan.mesh.file} (${(plan.mesh.sizeBytes / 1024).toFixed(1)} KiB, ${plan.mesh.contentType})`);
|
|
403
|
+
console.log(` thumbnail: ${plan.thumbnail ? `${plan.thumbnail.file} (${(plan.thumbnail.sizeBytes / 1024).toFixed(1)} KiB, ${plan.thumbnail.contentType})` : '(none — server-rendered)'}`);
|
|
404
|
+
console.log(` payload: ${JSON.stringify(plan.payload, null, 2).replace(/\n/g, '\n ')}`);
|
|
405
|
+
for (const warning of plan.warnings)
|
|
406
|
+
console.log(` • ${warning}`);
|
|
407
|
+
const status = (0, lib_1.authStatus)();
|
|
408
|
+
if (!status.loggedIn)
|
|
409
|
+
console.log(' • not logged in — run `helix login` before the real publish');
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const creds = requireAuth();
|
|
417
|
+
const mismatch = (0, config_1.credsEnvMismatchWarning)(creds.apiUrl);
|
|
418
|
+
if (mismatch)
|
|
419
|
+
fail(`✖ Refusing to publish:\n${mismatch}`);
|
|
420
|
+
try {
|
|
421
|
+
const result = await (0, lib_1.publishItem)(publishOptions, creds, {
|
|
422
|
+
onProgress: (msg) => !opts.json && console.log(msg),
|
|
423
|
+
});
|
|
424
|
+
if (opts.json) {
|
|
425
|
+
console.log(JSON.stringify(result, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log(`✔ Published ${result.kind} "${result.title}" (${result.slug})`);
|
|
429
|
+
if (result.slot)
|
|
430
|
+
console.log(` slot: ${result.slot}`);
|
|
431
|
+
// Echoed from the SERVER's row, not from what was sent — so a value the
|
|
432
|
+
// server normalised, dropped or never stored is visible here.
|
|
433
|
+
if (result.genders.length)
|
|
434
|
+
console.log(` gender: ${result.genders.join(', ')}`);
|
|
435
|
+
console.log(` status: ${result.status}`);
|
|
436
|
+
console.log(result.priceLix === null
|
|
437
|
+
? ' price: (personal — not listed)'
|
|
438
|
+
: ` price: ${result.priceLix} LIX (publish fee ${result.publishFeeLix} LIX)`);
|
|
439
|
+
console.log(` id: ${result.id}`);
|
|
440
|
+
for (const warning of result.verification.warnings)
|
|
441
|
+
console.log(` ⚠ ${warning}`);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
fail(err instanceof lib_1.ApiError
|
|
445
|
+
? err.render()
|
|
446
|
+
: `✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
// --- helix assets ----------------------------------------------------------
|
|
450
|
+
// Operational asset work lives here. MCP surfaces may point agents at these
|
|
451
|
+
// commands, but they must not reimplement Vault, Dreamer, download, or receipt
|
|
452
|
+
// behavior in the transport package.
|
|
453
|
+
const assets = program
|
|
454
|
+
.command('assets')
|
|
455
|
+
.description('Discover, install, and generate HELIX platform assets');
|
|
456
|
+
function optionalNumber(value, name) {
|
|
457
|
+
if (value === undefined)
|
|
458
|
+
return undefined;
|
|
459
|
+
const parsed = Number(value);
|
|
460
|
+
if (!Number.isFinite(parsed))
|
|
461
|
+
fail(`✖ ${name} must be a number, got '${value}'`);
|
|
462
|
+
return parsed;
|
|
463
|
+
}
|
|
464
|
+
function commaList(value) {
|
|
465
|
+
const values = value
|
|
466
|
+
?.split(',')
|
|
467
|
+
.map((entry) => entry.trim())
|
|
468
|
+
.filter(Boolean);
|
|
469
|
+
return values?.length ? values : undefined;
|
|
470
|
+
}
|
|
471
|
+
function printJson(value) {
|
|
472
|
+
console.log(JSON.stringify(value, null, 2));
|
|
473
|
+
}
|
|
474
|
+
assets
|
|
475
|
+
.command('materials')
|
|
476
|
+
.description('Search the versioned HELIX material catalog')
|
|
477
|
+
.option('-q, --query <text>', 'search id, name, category, and tags')
|
|
478
|
+
.option('--category <category>', 'exact material category')
|
|
479
|
+
.option('--limit <n>', 'maximum results')
|
|
480
|
+
.action(async (opts) => {
|
|
481
|
+
try {
|
|
482
|
+
printJson(await (0, lib_1.listMaterials)(config_1.DEFAULT_API_URL, {
|
|
483
|
+
q: opts.query,
|
|
484
|
+
category: opts.category,
|
|
485
|
+
limit: optionalNumber(opts.limit, '--limit'),
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
assets
|
|
493
|
+
.command('material')
|
|
494
|
+
.description('Resolve one material id to its immutable map URLs')
|
|
495
|
+
.argument('<id>', 'material id from `helix assets materials`')
|
|
496
|
+
.action(async (id) => {
|
|
497
|
+
try {
|
|
498
|
+
printJson(await (0, lib_1.resolveMaterial)(config_1.DEFAULT_API_URL, id));
|
|
499
|
+
}
|
|
500
|
+
catch (err) {
|
|
501
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
assets
|
|
505
|
+
.command('search')
|
|
506
|
+
.description('Search reusable HELIX Vault assets')
|
|
507
|
+
.argument('[query]', 'free-text search')
|
|
508
|
+
.option('--kind <a,b>', 'comma-separated kinds')
|
|
509
|
+
.option('--skeleton <id>', 'required skeleton')
|
|
510
|
+
.option('--engine <engine>', 'target engine')
|
|
511
|
+
.option('--license <license>', 'license filter')
|
|
512
|
+
.option('--source <source>', 'source filter')
|
|
513
|
+
.option('--origin <origin>', 'origin/provenance filter')
|
|
514
|
+
.option('--creator-id <id>', 'creator id')
|
|
515
|
+
.option('--page <n>', 'page number')
|
|
516
|
+
.option('--limit <n>', 'page size')
|
|
517
|
+
.action(async (query, opts) => {
|
|
518
|
+
const creds = (0, config_1.loadCredentials)();
|
|
519
|
+
const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
|
|
520
|
+
try {
|
|
521
|
+
printJson(await (0, lib_1.searchVaultAssets)(apiUrl, {
|
|
522
|
+
q: query,
|
|
523
|
+
kind: commaList(opts.kind),
|
|
524
|
+
skeleton: opts.skeleton,
|
|
525
|
+
engine: opts.engine,
|
|
526
|
+
license: opts.license,
|
|
527
|
+
source: opts.source,
|
|
528
|
+
origin: opts.origin,
|
|
529
|
+
creatorId: opts.creatorId,
|
|
530
|
+
page: optionalNumber(opts.page, '--page'),
|
|
531
|
+
limit: optionalNumber(opts.limit, '--limit'),
|
|
532
|
+
}, creds ?? undefined));
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
assets
|
|
539
|
+
.command('get')
|
|
540
|
+
.description('Read one HELIX Vault asset')
|
|
541
|
+
.argument('<asset-id>', 'Vault asset UUID')
|
|
542
|
+
.action(async (assetId) => {
|
|
543
|
+
const creds = (0, config_1.loadCredentials)();
|
|
544
|
+
const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
|
|
545
|
+
try {
|
|
546
|
+
printJson(await (0, lib_1.getVaultAsset)(apiUrl, assetId, creds ?? undefined));
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
assets
|
|
553
|
+
.command('install')
|
|
554
|
+
.description('Install one Vault artifact into a world and write its provenance receipt')
|
|
555
|
+
.argument('<asset-id>', 'Vault asset UUID')
|
|
556
|
+
.option('-C, --world <dir>', 'world project directory', '.')
|
|
557
|
+
.action(async (assetId, opts) => {
|
|
558
|
+
const creds = (0, config_1.loadCredentials)();
|
|
559
|
+
const apiUrl = creds?.apiUrl ?? config_1.DEFAULT_API_URL;
|
|
560
|
+
try {
|
|
561
|
+
printJson(await (0, lib_1.installVaultAsset)((0, node_path_1.resolve)(opts.world), apiUrl, assetId, creds ?? undefined));
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
assets
|
|
568
|
+
.command('generate')
|
|
569
|
+
.description('Generate and publish a new prop or character through Dreamer')
|
|
570
|
+
.argument('<kind>', 'prop or character')
|
|
571
|
+
.argument('<prompt>', 'asset prompt')
|
|
572
|
+
.option('--title <title>', 'asset title')
|
|
573
|
+
.option('--additional-prompt <text>', 'additional generation guidance')
|
|
574
|
+
.option('--target-polycount <n>', 'target triangle count')
|
|
575
|
+
.option('--visibility <visibility>', 'public, unlisted, or private', 'public')
|
|
576
|
+
.action(async (kind, prompt, opts) => {
|
|
577
|
+
if (kind !== 'prop' && kind !== 'character') {
|
|
578
|
+
fail(`✖ kind must be prop or character, got '${kind}'`);
|
|
579
|
+
}
|
|
580
|
+
if (!['public', 'unlisted', 'private'].includes(opts.visibility)) {
|
|
581
|
+
fail(`✖ --visibility must be public, unlisted, or private`);
|
|
582
|
+
}
|
|
583
|
+
const creds = requireAuth();
|
|
584
|
+
try {
|
|
585
|
+
printJson(await (0, lib_1.generateAndPublishAsset)(creds, {
|
|
586
|
+
kind,
|
|
587
|
+
prompt,
|
|
588
|
+
title: opts.title,
|
|
589
|
+
additionalPrompt: opts.additionalPrompt,
|
|
590
|
+
targetPolycount: optionalNumber(opts.targetPolycount, '--target-polycount'),
|
|
591
|
+
}, {
|
|
592
|
+
visibility: opts.visibility,
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
assets
|
|
600
|
+
.command('resume')
|
|
601
|
+
.description('Resume and publish an existing Dreamer asset job')
|
|
602
|
+
.argument('<job-id>', 'Dreamer job id')
|
|
603
|
+
.option('--visibility <visibility>', 'public, unlisted, or private', 'public')
|
|
604
|
+
.action(async (jobId, opts) => {
|
|
605
|
+
if (!['public', 'unlisted', 'private'].includes(opts.visibility)) {
|
|
606
|
+
fail(`✖ --visibility must be public, unlisted, or private`);
|
|
607
|
+
}
|
|
608
|
+
const creds = requireAuth();
|
|
609
|
+
try {
|
|
610
|
+
printJson(await (0, lib_1.resumeAndPublishAsset)(creds, jobId, {
|
|
611
|
+
visibility: opts.visibility,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
assets
|
|
619
|
+
.command('generate-image')
|
|
620
|
+
.description('Generate a standalone image through Dreamer')
|
|
621
|
+
.argument('<prompt>', 'image prompt')
|
|
622
|
+
.option('--additional-prompt <text>', 'additional generation guidance')
|
|
623
|
+
.option('--project-id <id>', 'Dreamer project id')
|
|
624
|
+
.action(async (prompt, opts) => {
|
|
625
|
+
const creds = requireAuth();
|
|
626
|
+
try {
|
|
627
|
+
printJson(await (0, lib_1.generateImage)(creds, {
|
|
628
|
+
prompt,
|
|
629
|
+
additionalPrompt: opts.additionalPrompt,
|
|
630
|
+
projectId: opts.projectId,
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
catch (err) {
|
|
634
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
for (const generation of [
|
|
638
|
+
{ command: 'generate-audio', kind: 'audio' },
|
|
639
|
+
{ command: 'generate-environment', kind: 'gaussian_splat' },
|
|
640
|
+
]) {
|
|
641
|
+
assets
|
|
642
|
+
.command(generation.command)
|
|
643
|
+
.description(generation.kind === 'audio'
|
|
644
|
+
? 'Generate standalone audio through Dreamer'
|
|
645
|
+
: 'Generate an experimental Gaussian-splat environment through Dreamer')
|
|
646
|
+
.argument('<prompt>', 'generation prompt')
|
|
647
|
+
.option('--additional-prompt <text>', 'additional generation guidance')
|
|
648
|
+
.option('--project-id <id>', 'Dreamer project id')
|
|
649
|
+
.option('--title <title>', 'asset title')
|
|
650
|
+
.option('--loop', 'request loopable output (audio)')
|
|
651
|
+
.option('--duration-seconds <n>', 'requested duration (audio)')
|
|
652
|
+
.action(async (prompt, opts) => {
|
|
653
|
+
const creds = requireAuth();
|
|
654
|
+
try {
|
|
655
|
+
printJson(await (0, lib_1.generateStandaloneAsset)(creds, {
|
|
656
|
+
kind: generation.kind,
|
|
657
|
+
prompt,
|
|
658
|
+
additionalPrompt: opts.additionalPrompt,
|
|
659
|
+
projectId: opts.projectId,
|
|
660
|
+
title: opts.title,
|
|
661
|
+
loop: opts.loop,
|
|
662
|
+
durationSeconds: optionalNumber(opts.durationSeconds, '--duration-seconds'),
|
|
663
|
+
}));
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
fail(`✖ ${err instanceof Error ? err.message : String(err)}`);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
273
670
|
async function printCatalog(kind, system, apiUrlOpt) {
|
|
274
671
|
const apiUrl = apiUrlOpt ?? (0, config_1.loadCredentials)()?.apiUrl ?? config_1.DEFAULT_API_URL;
|
|
275
672
|
try {
|