@assethub/cli 0.1.1 → 0.1.3

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 (3) hide show
  1. package/README.md +37 -5
  2. package/dist/index.js +679 -44
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -8,10 +8,12 @@ export ASSETHUB_API_KEY=ah_live_xxx
8
8
  assethub models list
9
9
  assethub models list --domain meshGen --ids-only
10
10
  assethub files import https://example.com/input.png --media-type image
11
+ assethub source create --file ./input.png --media-type image
11
12
  assethub image generate --prompt "stylized prop concept" --wait
12
13
  assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
13
- assethub jobs watch job_123
14
- assethub production analyze --image-url https://example.com/input.png --part-extractor "V1.5"
14
+ assethub jobs watch job_123 --download --out-dir ./out/job
15
+ assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
16
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
15
17
  ```
16
18
 
17
19
  Configuration:
@@ -24,11 +26,41 @@ Agent-friendly login:
24
26
 
25
27
  ```sh
26
28
  printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
27
- assethub image generate --prompt "small stylized game asset chest" --wait --interval-ms 5000 --timeout-ms 900000
28
- assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --interval-ms 5000 --timeout-ms 900000
29
- assethub production analyze --image-url https://example.com/input.png --part-extractor "V1.5"
29
+ assethub image generate --prompt "small stylized game asset chest" --wait --download --out-dir ./out/image
30
+ assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --download --out-dir ./out/mesh
31
+ cat ./character.png | assethub parts split --stdin --file-name character.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
32
+ cat ./character.png | assethub parts compare --stdin --file-name character.png --preprocess-prompt "clean white background" --part-extractor "V1.5" --wait --all-ready
30
33
  ```
31
34
 
35
+ Agent-oriented input:
36
+
37
+ - `--file <path>` uploads a local image or mesh through the public file API.
38
+ - `--stdin --file-name <name>` accepts piped bytes. Give a file name with an
39
+ extension when possible.
40
+ - `--data-uri <uri>` accepts `data:image/png;base64,...` style inputs and uploads
41
+ them before running the target command.
42
+ - `--source-url`, `--source-id`/`--source-resource-id`, and `--file-ref-json`
43
+ can be reused across image, mesh, and part extraction commands where the API
44
+ supports them.
45
+
46
+ Agent-oriented output:
47
+
48
+ - stdout is one final JSON object.
49
+ - polling progress goes to stderr.
50
+ - `--download --out-dir <dir>` recursively downloads public artifact URLs from
51
+ job or production status payloads and writes a `manifest.json`.
52
+
53
+ `assethub parts compare` runs the normal part split first. When
54
+ `--preprocess-prompt <text>` is provided, it then uses the Nano Banana image
55
+ model to generate a preprocessed image, waits for that image job to finish, and
56
+ runs a second split on the generated image. The final JSON contains `direct`
57
+ and `preprocessed` sections so agents can compare both outputs. If
58
+ preprocessing fails, the direct result is still printed with
59
+ `preprocessed.ok: false`; pass `--fail-on-preprocess-error` when a non-zero exit
60
+ is preferred. Use `--all-ready` rather than explicit `--task-id` values when
61
+ preprocessing is enabled, because the generated-image split creates a separate
62
+ production order.
63
+
32
64
  Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
33
65
  only when the `v2` image endpoint is available in the target environment.
34
66
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { basename, dirname, join } from 'node:path';
4
+ import { basename, dirname, extname, join } from 'node:path';
5
5
  import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
6
6
  import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
7
7
  const defaultBaseUrl = 'https://app.assethub.io';
@@ -11,32 +11,26 @@ const defaultProfileName = 'default';
11
11
  const defaultPartExtractorName = 'V1.5';
12
12
  const partExtractorOptions = [
13
13
  {
14
- internalId: 'ah_part_extractor_v3',
15
14
  publicName: 'Part Extractor V3',
16
15
  aliases: ['part extractor v3', 'v3'],
17
16
  },
18
17
  {
19
- internalId: 'ah_part_extractor_v5',
20
18
  publicName: 'V1',
21
19
  aliases: ['v1', '1'],
22
20
  },
23
21
  {
24
- internalId: 'ah_part_extractor_v55',
25
22
  publicName: 'V1.5 alpha',
26
23
  aliases: ['v1.5 alpha', 'v1.5-alpha', '1.5 alpha', '1.5-alpha'],
27
24
  },
28
25
  {
29
- internalId: 'ah_part_extractor_v65',
30
26
  publicName: 'V1.5',
31
27
  aliases: ['v1.5', '1.5', 'latest', 'default'],
32
28
  },
33
29
  {
34
- internalId: 'ah_part_extractor_v20_alpha',
35
30
  publicName: 'V2.0 alpha',
36
31
  aliases: ['v2.0 alpha', 'v2 alpha', 'v2.0-alpha', 'v2-alpha', '2.0 alpha'],
37
32
  },
38
33
  {
39
- internalId: 'ah_part_extractor_v21_alpha',
40
34
  publicName: 'V2.1 alpha',
41
35
  aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
42
36
  },
@@ -45,15 +39,14 @@ const partExtractorPublicNames = partExtractorOptions
45
39
  .map(option => option.publicName)
46
40
  .join(', ');
47
41
  const normalizePartExtractorName = (value) => value.trim().toLowerCase().replace(/\s+/g, ' ');
48
- const publicNameByInternalPartExtractorId = new Map(partExtractorOptions.map(option => [option.internalId, option.publicName]));
49
- const internalPartExtractorIdByPublicName = new Map(partExtractorOptions.flatMap(option => [
50
- [normalizePartExtractorName(option.publicName), option.internalId],
42
+ const partExtractorPublicNameByInput = new Map(partExtractorOptions.flatMap(option => [
43
+ [normalizePartExtractorName(option.publicName), option.publicName],
51
44
  ...option.aliases.map(alias => [
52
45
  normalizePartExtractorName(alias),
53
- option.internalId,
46
+ option.publicName,
54
47
  ]),
55
48
  ]));
56
- const isInternalPartExtractorId = (value) => value === 'nano_bbox_v1' || value.startsWith('ah_part_extractor_');
49
+ const isInternalPartExtractorId = (value) => value.trim().includes('_');
57
50
  const usage = `AssetHub CLI
58
51
 
59
52
  Usage:
@@ -65,15 +58,18 @@ Usage:
65
58
  assethub models get <model-id>
66
59
  assethub files import <url> --media-type image|mesh
67
60
  assethub files upload <file> --media-type image|mesh
68
- assethub image generate --prompt <text> [--file <path> | --source-url <url>] [--model-id <id>] [--batch-size <n>] [--wait] [--api-version v1|v2]
61
+ assethub source create (--file <path> | --source-url <url> | --source-id <id> | --stdin | --data-uri <uri>) --media-type image|mesh
62
+ assethub image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --data-uri <uri>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--api-version v1|v2]
69
63
  assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
70
- assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json>) [--model-id <id>] [--name <name>] [--wait]
71
- assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json>) --model-id <id> [--wait]
72
- assethub jobs get <job-id>
73
- assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>]
74
- assethub production analyze --image-url <url> [--part-extractor <name>] [--name <name>]
75
- assethub production status <order-id>
76
- assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality]
64
+ assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--model-id <id>] [--name <name>] [--wait] [--download --out-dir <dir>]
65
+ assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) --model-id <id> [--wait] [--download --out-dir <dir>]
66
+ assethub jobs get <job-id> [--download --out-dir <dir>]
67
+ assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
68
+ assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--part-extractor <name>] [--name <name>] [--wait] [--download --out-dir <dir>]
69
+ assethub production status <order-id> [--wait] [--download --out-dir <dir>]
70
+ assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality] [--wait] [--download --out-dir <dir>]
71
+ assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri> | --order-id <id>) [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
72
+ assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --data-uri <uri>) [--preprocess-prompt <text>] [--preprocess-model-id <id>] [--fail-on-preprocess-error] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
77
73
 
78
74
  Global options:
79
75
  --api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
@@ -87,9 +83,13 @@ Examples:
87
83
  assethub auth login --api-key-stdin
88
84
  assethub models list --domain meshGen --ids-only
89
85
  assethub files upload ./input.png --media-type image
86
+ assethub source create --file ./input.png --media-type image
87
+ cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
90
88
  assethub image generate --prompt "stylized prop concept" --wait
91
89
  assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
92
- assethub production analyze --image-url https://example.com/input.png --part-extractor "V1.5"
90
+ assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
91
+ assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
92
+ assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
93
93
  `;
94
94
  const print = (value) => {
95
95
  stdout.write(`${JSON.stringify(value, null, 2)}\n`);
@@ -162,23 +162,22 @@ const requireFlag = (flags, name) => {
162
162
  const getPartExtractorFlag = (flags) => getFlag(flags, 'part-extractor') ??
163
163
  getFlag(flags, 'part-extractor-version') ??
164
164
  getFlag(flags, 'agent-version');
165
- const resolvePartExtractorInternalId = (value) => {
165
+ const resolvePartExtractorPublicName = (value) => {
166
166
  if (isInternalPartExtractorId(value)) {
167
167
  throw new Error(`Internal part extractor IDs are not accepted by the public CLI. Use one of: ${partExtractorPublicNames}.`);
168
168
  }
169
- const internalId = internalPartExtractorIdByPublicName.get(normalizePartExtractorName(value));
170
- if (internalId == null) {
169
+ const publicName = partExtractorPublicNameByInput.get(normalizePartExtractorName(value));
170
+ if (publicName == null) {
171
171
  throw new Error(`Unknown part extractor: ${value}. Use one of: ${partExtractorPublicNames}.`);
172
172
  }
173
- return internalId;
173
+ return publicName;
174
174
  };
175
- const resolvePartExtractorForAnalyze = (flags) => resolvePartExtractorInternalId(getPartExtractorFlag(flags) ?? defaultPartExtractorName);
175
+ const resolvePartExtractorForAnalyze = (flags) => resolvePartExtractorPublicName(getPartExtractorFlag(flags) ?? defaultPartExtractorName);
176
176
  const publicPartExtractorName = (value) => {
177
177
  if (value == null) {
178
178
  return null;
179
179
  }
180
- return (publicNameByInternalPartExtractorId.get(value) ??
181
- (isInternalPartExtractorId(value) ? 'Unknown' : value));
180
+ return (isInternalPartExtractorId(value) ? 'Unknown' : value);
182
181
  };
183
182
  const sanitizeProductionOutput = (value) => {
184
183
  if (Array.isArray(value)) {
@@ -224,6 +223,10 @@ const parseMediaType = (value) => {
224
223
  }
225
224
  throw new Error('--media-type must be "image" or "mesh"');
226
225
  };
226
+ const defaultFileNameForMediaType = (mediaType) => mediaType === 'image' ? 'stdin.png' : 'stdin.glb';
227
+ const getSourceResourceIdFlag = (flags) => getFlag(flags, 'source-resource-id') ??
228
+ getFlag(flags, 'source-id') ??
229
+ getFlag(flags, 'resource-id');
227
230
  const getProfileName = (flags) => getFlag(flags, 'profile') ?? defaultProfileName;
228
231
  const getConfigPath = (flags) => getFlag(flags, 'config') ??
229
232
  env.ASSETHUB_CLI_CONFIG ??
@@ -265,6 +268,18 @@ const readStdin = async () => await new Promise((resolve, reject) => {
265
268
  stdin.on('end', () => resolve(value));
266
269
  stdin.on('error', reject);
267
270
  });
271
+ const readStdinBlob = async (type) => await new Promise((resolve, reject) => {
272
+ const chunks = [];
273
+ stdin.on('data', chunk => {
274
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
275
+ });
276
+ stdin.on('end', () => {
277
+ const buffer = Buffer.concat(chunks);
278
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
279
+ resolve(new Blob([arrayBuffer], type == null ? undefined : { type }));
280
+ });
281
+ stdin.on('error', reject);
282
+ });
268
283
  const resolveAuth = async (flags) => {
269
284
  const profile = getProfileName(flags);
270
285
  const baseUrlFromFlags = getFlag(flags, 'base-url');
@@ -323,8 +338,46 @@ const readBlob = async (filePath) => {
323
338
  const buffer = await readFile(filePath);
324
339
  return new Blob([new Uint8Array(buffer)]);
325
340
  };
341
+ const blobFromDataUri = (value) => {
342
+ const match = value.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
343
+ if (match == null) {
344
+ throw new Error('--data-uri must be a valid data URI.');
345
+ }
346
+ const contentType = match[1] === '' ? undefined : match[1];
347
+ const isBase64 = match[2] != null;
348
+ const payload = match[3] ?? '';
349
+ const buffer = isBase64
350
+ ? Buffer.from(payload, 'base64')
351
+ : Buffer.from(decodeURIComponent(payload), 'utf8');
352
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
353
+ return {
354
+ blob: new Blob([arrayBuffer], contentType == null ? undefined : { type: contentType }),
355
+ contentType,
356
+ };
357
+ };
358
+ const extensionFromMimeType = (mimeType, mediaType) => {
359
+ if (mimeType === 'image/png')
360
+ return 'png';
361
+ if (mimeType === 'image/jpeg')
362
+ return 'jpg';
363
+ if (mimeType === 'image/webp')
364
+ return 'webp';
365
+ if (mimeType === 'image/gif')
366
+ return 'gif';
367
+ if (mimeType === 'model/gltf-binary')
368
+ return 'glb';
369
+ if (mimeType === 'model/gltf+json')
370
+ return 'gltf';
371
+ return mediaType === 'image' ? 'png' : 'glb';
372
+ };
373
+ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
374
+ if (extname(fileName) !== '') {
375
+ return fileName;
376
+ }
377
+ return `${fileName}.${extensionFromMimeType(mimeType, mediaType)}`;
378
+ };
326
379
  const resolveSource = async ({ flags, client, fileMediaType, }) => {
327
- const sourceResourceId = getFlag(flags, 'source-resource-id');
380
+ const sourceResourceId = getSourceResourceIdFlag(flags);
328
381
  if (sourceResourceId != null) {
329
382
  return { resourceId: sourceResourceId };
330
383
  }
@@ -345,12 +398,32 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
345
398
  });
346
399
  return { fileRef: fileRef.fileRef };
347
400
  }
401
+ const dataUri = getFlag(flags, 'data-uri');
402
+ if (dataUri != null) {
403
+ const { blob, contentType } = blobFromDataUri(dataUri);
404
+ const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'data-uri', fileMediaType, contentType);
405
+ const fileRef = await client.v2.uploadFile({
406
+ file: blob,
407
+ fileName,
408
+ mediaType: fileMediaType,
409
+ });
410
+ return { fileRef: fileRef.fileRef };
411
+ }
412
+ if (hasFlag(flags, 'stdin')) {
413
+ const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType);
414
+ const fileRef = await client.v2.uploadFile({
415
+ file: await readStdinBlob(),
416
+ fileName,
417
+ mediaType: fileMediaType,
418
+ });
419
+ return { fileRef: fileRef.fileRef };
420
+ }
348
421
  return undefined;
349
422
  };
350
423
  const requireSource = async (options) => {
351
424
  const source = await resolveSource(options);
352
425
  if (source == null) {
353
- throw new Error('Missing source. Pass --file, --source-url, --source-resource-id, or --file-ref-json.');
426
+ throw new Error('Missing source. Pass --file, --source-url, --source-resource-id, --source-id, --file-ref-json, --stdin, or --data-uri.');
354
427
  }
355
428
  return source;
356
429
  };
@@ -366,6 +439,62 @@ const sourceToV1ImageInput = (source) => {
366
439
  }
367
440
  throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
368
441
  };
442
+ const resolveProductionAnalyzeSource = async (ctx) => {
443
+ const imageUrl = getFlag(ctx.flags, 'image-url');
444
+ if (imageUrl != null) {
445
+ return { imageUrl };
446
+ }
447
+ const source = await requireSource({
448
+ flags: ctx.flags,
449
+ client: ctx.client,
450
+ fileMediaType: 'image',
451
+ });
452
+ if (source.resourceId != null) {
453
+ return { imageAssetId: source.resourceId };
454
+ }
455
+ if (source.url != null) {
456
+ return { imageUrl: source.url };
457
+ }
458
+ if (source.fileRef == null) {
459
+ throw new Error('Missing source. Pass --file, --source-url, --source-id, --file-ref-json, --stdin, or --data-uri.');
460
+ }
461
+ return { imageBlobLocation: source.fileRef };
462
+ };
463
+ const productionAnalyzeSourceFromSource = (source) => {
464
+ if (source.resourceId != null) {
465
+ return { imageAssetId: source.resourceId };
466
+ }
467
+ if (source.url != null) {
468
+ return { imageUrl: source.url };
469
+ }
470
+ return { imageBlobLocation: source.fileRef };
471
+ };
472
+ const sourceFromProductionAnalyzeSource = (source) => {
473
+ if (source.imageAssetId != null) {
474
+ return { resourceId: source.imageAssetId };
475
+ }
476
+ if (source.imageUrl != null) {
477
+ return { url: source.imageUrl };
478
+ }
479
+ return { fileRef: source.imageBlobLocation };
480
+ };
481
+ const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
482
+ const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
483
+ const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
484
+ const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
485
+ const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
486
+ const startedAt = Date.now();
487
+ while (Date.now() - startedAt <= timeoutMs) {
488
+ const status = await ctx.client.v1.getProductionStatus(orderId);
489
+ const orderStatus = status.order.status;
490
+ stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
491
+ if (terminalStatuses.has(orderStatus)) {
492
+ return status;
493
+ }
494
+ await new Promise(resolve => setTimeout(resolve, intervalMs));
495
+ }
496
+ throw new Error(`Polling timeout: order=${orderId} did not reach terminal state within ${timeoutMs}ms`);
497
+ };
369
498
  const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
370
499
  if (!hasFlag(ctx.flags, 'wait')) {
371
500
  return undefined;
@@ -388,6 +517,60 @@ const requirePositional = (positionals, index, label) => {
388
517
  }
389
518
  return value;
390
519
  };
520
+ const commandSource = async (subcommand, ctx) => {
521
+ if (subcommand !== 'create') {
522
+ throw new Error('Unknown source command. Use "source create".');
523
+ }
524
+ const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
525
+ const sourceUrl = getFlag(ctx.flags, 'source-url') ?? getFlag(ctx.flags, 'image-url');
526
+ const sourceResourceId = getSourceResourceIdFlag(ctx.flags);
527
+ const filePath = getFlag(ctx.flags, 'file');
528
+ const useStdin = hasFlag(ctx.flags, 'stdin');
529
+ const dataUri = getFlag(ctx.flags, 'data-uri');
530
+ const provided = [
531
+ sourceUrl != null,
532
+ sourceResourceId != null,
533
+ filePath != null,
534
+ useStdin,
535
+ dataUri != null,
536
+ ].filter(Boolean);
537
+ if (provided.length !== 1) {
538
+ throw new Error('Pass exactly one source input: --file, --source-url, --source-id, --source-resource-id, --stdin, or --data-uri.');
539
+ }
540
+ if (sourceUrl != null) {
541
+ print({ source: { url: sourceUrl }, url: sourceUrl, mediaType });
542
+ return;
543
+ }
544
+ if (sourceResourceId != null) {
545
+ print({
546
+ source: { resourceId: sourceResourceId },
547
+ resourceId: sourceResourceId,
548
+ mediaType,
549
+ });
550
+ return;
551
+ }
552
+ const upload = filePath != null
553
+ ? await ctx.client.v2.uploadFile({
554
+ file: await readBlob(filePath),
555
+ fileName: basename(filePath),
556
+ mediaType,
557
+ })
558
+ : dataUri != null
559
+ ? await (async () => {
560
+ const { blob, contentType } = blobFromDataUri(dataUri);
561
+ return await ctx.client.v2.uploadFile({
562
+ file: blob,
563
+ fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? 'data-uri', mediaType, contentType),
564
+ mediaType,
565
+ });
566
+ })()
567
+ : await ctx.client.v2.uploadFile({
568
+ file: await readStdinBlob(),
569
+ fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? defaultFileNameForMediaType(mediaType), mediaType),
570
+ mediaType,
571
+ });
572
+ print({ source: { fileRef: upload.fileRef }, fileRef: upload.fileRef, mediaType });
573
+ };
391
574
  const commandModels = async (subcommand, positionals, ctx) => {
392
575
  if (subcommand === 'list') {
393
576
  const domain = getFlag(ctx.flags, 'domain');
@@ -533,7 +716,7 @@ const commandImage = async (subcommand, ctx) => {
533
716
  source,
534
717
  });
535
718
  const job = await pollIfRequested({ ctx, jobId: queued.jobId, apiVersion });
536
- print({ queued, job, apiVersion });
719
+ print(await withOptionalDownloads(ctx.flags, { queued, job, apiVersion }));
537
720
  };
538
721
  const commandMesh = async (subcommand, ctx) => {
539
722
  if (subcommand === 'generate') {
@@ -551,7 +734,7 @@ const commandMesh = async (subcommand, ctx) => {
551
734
  isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
552
735
  });
553
736
  const job = await pollIfRequested({ ctx, jobId: queued.jobId });
554
- print({ queued, job });
737
+ print(await withOptionalDownloads(ctx.flags, { queued, job }));
555
738
  return;
556
739
  }
557
740
  if (subcommand === 'retopo') {
@@ -567,7 +750,7 @@ const commandMesh = async (subcommand, ctx) => {
567
750
  polygonType: getFlag(ctx.flags, 'polygon-type'),
568
751
  });
569
752
  const job = await pollIfRequested({ ctx, jobId: queued.jobId });
570
- print({ queued, job });
753
+ print(await withOptionalDownloads(ctx.flags, { queued, job }));
571
754
  return;
572
755
  }
573
756
  throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
@@ -575,7 +758,8 @@ const commandMesh = async (subcommand, ctx) => {
575
758
  const commandJobs = async (subcommand, positionals, ctx) => {
576
759
  const jobId = requirePositional(positionals, 2, 'job-id');
577
760
  if (subcommand === 'get') {
578
- print({ job: await ctx.client.v2.getJob(jobId) });
761
+ const job = await ctx.client.v2.getJob(jobId);
762
+ print(await withOptionalDownloads(ctx.flags, { job }));
579
763
  return;
580
764
  }
581
765
  if (subcommand === 'watch') {
@@ -586,7 +770,7 @@ const commandJobs = async (subcommand, positionals, ctx) => {
586
770
  stderr.write(`[poll] job=${current.id} status=${current.status}\n`);
587
771
  },
588
772
  });
589
- print({ job });
773
+ print(await withOptionalDownloads(ctx.flags, { job }));
590
774
  return;
591
775
  }
592
776
  throw new Error('Unknown jobs command. Use "jobs get" or "jobs watch".');
@@ -599,22 +783,300 @@ const resolvePartExtractorFromMission = async (ctx, orderId, missionId) => {
599
783
  partExtractorPublicNames);
600
784
  }
601
785
  if (isInternalPartExtractorId(mission.agentVersion)) {
602
- return mission.agentVersion;
786
+ throw new Error('Could not infer the public part extractor from the mission. Pass --part-extractor with one of: ' +
787
+ partExtractorPublicNames);
788
+ }
789
+ return resolvePartExtractorPublicName(mission.agentVersion);
790
+ };
791
+ const redactUrl = (value) => {
792
+ try {
793
+ const url = new URL(value);
794
+ url.search = '';
795
+ url.hash = '';
796
+ return url.toString();
797
+ }
798
+ catch {
799
+ return value;
800
+ }
801
+ };
802
+ const collectHttpUrls = (value, urls = new Set()) => {
803
+ if (typeof value === 'string') {
804
+ try {
805
+ const url = new URL(value);
806
+ if (url.protocol === 'http:' || url.protocol === 'https:') {
807
+ urls.add(value);
808
+ }
809
+ }
810
+ catch {
811
+ // Ignore non-URL strings.
812
+ }
813
+ return urls;
814
+ }
815
+ if (Array.isArray(value)) {
816
+ for (const item of value) {
817
+ collectHttpUrls(item, urls);
818
+ }
819
+ return urls;
820
+ }
821
+ if (value != null && typeof value === 'object') {
822
+ for (const item of Object.values(value)) {
823
+ collectHttpUrls(item, urls);
824
+ }
825
+ }
826
+ return urls;
827
+ };
828
+ const extensionFromContentType = (contentType) => {
829
+ if (contentType == null) {
830
+ return '';
831
+ }
832
+ const clean = contentType.split(';')[0]?.trim().toLowerCase();
833
+ switch (clean) {
834
+ case 'image/png':
835
+ return '.png';
836
+ case 'image/jpeg':
837
+ return '.jpg';
838
+ case 'image/webp':
839
+ return '.webp';
840
+ case 'image/gif':
841
+ return '.gif';
842
+ case 'model/gltf-binary':
843
+ return '.glb';
844
+ case 'model/gltf+json':
845
+ return '.gltf';
846
+ case 'application/octet-stream':
847
+ default:
848
+ return '';
603
849
  }
604
- return resolvePartExtractorInternalId(mission.agentVersion);
850
+ };
851
+ const safeDownloadFileName = (value, index, contentType) => {
852
+ let name = `asset-${index + 1}`;
853
+ try {
854
+ const url = new URL(value);
855
+ const fromPath = basename(url.pathname);
856
+ if (fromPath !== '' && fromPath !== '/' && fromPath !== '.') {
857
+ name = fromPath;
858
+ }
859
+ }
860
+ catch {
861
+ // Keep fallback name.
862
+ }
863
+ name = name.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^_+/, '');
864
+ if (name === '' || name === '.' || name === '..') {
865
+ name = `asset-${index + 1}`;
866
+ }
867
+ if (extname(name) === '') {
868
+ name = `${name}${extensionFromContentType(contentType)}`;
869
+ }
870
+ return `${String(index + 1).padStart(3, '0')}-${name}`;
871
+ };
872
+ const maybeDownloadUrls = async (flags, value) => {
873
+ if (!hasFlag(flags, 'download')) {
874
+ return undefined;
875
+ }
876
+ const outDir = getFlag(flags, 'out-dir') ?? getFlag(flags, 'out');
877
+ if (outDir == null || outDir === 'true') {
878
+ throw new Error('Missing download output directory. Pass --out-dir <dir> or --out <dir>.');
879
+ }
880
+ await mkdir(outDir, { recursive: true });
881
+ const urls = [...collectHttpUrls(value)];
882
+ const files = [];
883
+ for (const [index, url] of urls.entries()) {
884
+ const response = await fetch(url);
885
+ if (!response.ok) {
886
+ throw new Error(`Download failed: ${redactUrl(url)} returned HTTP ${response.status}`);
887
+ }
888
+ const contentType = response.headers.get('content-type');
889
+ const bytes = new Uint8Array(await response.arrayBuffer());
890
+ const fileName = safeDownloadFileName(url, index, contentType);
891
+ const filePath = join(outDir, fileName);
892
+ await writeFile(filePath, bytes);
893
+ files.push({
894
+ url: redactUrl(url),
895
+ path: filePath,
896
+ contentType,
897
+ bytes: bytes.byteLength,
898
+ });
899
+ }
900
+ const manifest = { generatedAt: new Date().toISOString(), files };
901
+ await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
902
+ return { outDir, files };
903
+ };
904
+ const withOptionalDownloads = async (flags, value) => {
905
+ const downloads = await maybeDownloadUrls(flags, value);
906
+ return downloads == null ? value : { ...value, downloads };
907
+ };
908
+ const firstStringValueForKeys = (value, keys) => {
909
+ if (value == null || typeof value !== 'object') {
910
+ return undefined;
911
+ }
912
+ if (Array.isArray(value)) {
913
+ for (const item of value) {
914
+ const found = firstStringValueForKeys(item, keys);
915
+ if (found != null) {
916
+ return found;
917
+ }
918
+ }
919
+ return undefined;
920
+ }
921
+ for (const [key, entry] of Object.entries(value)) {
922
+ if (keys.has(key) && typeof entry === 'string' && entry.trim() !== '') {
923
+ return entry;
924
+ }
925
+ const found = firstStringValueForKeys(entry, keys);
926
+ if (found != null) {
927
+ return found;
928
+ }
929
+ }
930
+ return undefined;
931
+ };
932
+ const firstBlobLocation = (value) => {
933
+ if (value == null || typeof value !== 'object') {
934
+ return undefined;
935
+ }
936
+ if (Array.isArray(value)) {
937
+ for (const item of value) {
938
+ const found = firstBlobLocation(item);
939
+ if (found != null) {
940
+ return found;
941
+ }
942
+ }
943
+ return undefined;
944
+ }
945
+ const record = value;
946
+ if (record.type === 'supabase' && typeof record.bucket === 'string' && typeof record.path === 'string') {
947
+ return {
948
+ type: 'supabase',
949
+ bucket: record.bucket,
950
+ path: record.path,
951
+ };
952
+ }
953
+ if (record.type === 'signedSupabase' && typeof record.signedUrl === 'string') {
954
+ return {
955
+ type: 'signedSupabase',
956
+ signedUrl: record.signedUrl,
957
+ };
958
+ }
959
+ if (record.type === 'asset' && typeof record.assetId === 'string') {
960
+ return {
961
+ type: 'asset',
962
+ assetId: record.assetId,
963
+ };
964
+ }
965
+ for (const entry of Object.values(record)) {
966
+ const found = firstBlobLocation(entry);
967
+ if (found != null) {
968
+ return found;
969
+ }
970
+ }
971
+ return undefined;
972
+ };
973
+ const imageSourceFromCompletedJob = (job) => {
974
+ if (job.resourceId != null && job.resourceId.trim() !== '') {
975
+ return { imageAssetId: job.resourceId };
976
+ }
977
+ const resourceId = firstStringValueForKeys(job, new Set(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
978
+ if (resourceId != null) {
979
+ return { imageAssetId: resourceId };
980
+ }
981
+ const imageUrl = firstStringValueForKeys(job, new Set(['imageUrl', 'image_url', 'url', 'publicUrl', 'public_url']));
982
+ if (imageUrl != null) {
983
+ return { imageUrl };
984
+ }
985
+ const blobLocation = firstBlobLocation(job);
986
+ if (blobLocation != null) {
987
+ return { imageBlobLocation: blobLocation };
988
+ }
989
+ throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
990
+ };
991
+ const errorToOutput = (error) => {
992
+ if (error instanceof AssetHubApiError) {
993
+ return {
994
+ name: error.name,
995
+ message: error.message,
996
+ status: error.status,
997
+ code: error.code,
998
+ };
999
+ }
1000
+ if (error instanceof Error) {
1001
+ return {
1002
+ name: error.name,
1003
+ message: error.message,
1004
+ };
1005
+ }
1006
+ return { message: String(error) };
1007
+ };
1008
+ const readyTaskIdsFromStatus = (status) => {
1009
+ const missions = status != null && typeof status === 'object' && 'missions' in status
1010
+ ? status.missions
1011
+ : undefined;
1012
+ if (!Array.isArray(missions)) {
1013
+ return [];
1014
+ }
1015
+ return missions.flatMap(mission => {
1016
+ if (mission == null || typeof mission !== 'object' || !('tasks' in mission)) {
1017
+ return [];
1018
+ }
1019
+ const tasks = mission.tasks;
1020
+ if (!Array.isArray(tasks)) {
1021
+ return [];
1022
+ }
1023
+ return tasks.flatMap(task => {
1024
+ if (task == null || typeof task !== 'object') {
1025
+ return [];
1026
+ }
1027
+ const id = task.id;
1028
+ const taskStatus = task.status;
1029
+ return typeof id === 'string' &&
1030
+ typeof taskStatus === 'string' &&
1031
+ ['ready', 'pending', 'pending_review'].includes(taskStatus)
1032
+ ? [id]
1033
+ : [];
1034
+ });
1035
+ });
1036
+ };
1037
+ const singleMissionIdFromStatus = (status) => {
1038
+ const missions = status != null && typeof status === 'object' && 'missions' in status
1039
+ ? status.missions
1040
+ : undefined;
1041
+ if (!Array.isArray(missions) || missions.length !== 1) {
1042
+ throw new Error('Pass --mission-id when the production order does not have exactly one mission.');
1043
+ }
1044
+ const missionId = missions[0].id;
1045
+ if (typeof missionId !== 'string' || missionId.trim() === '') {
1046
+ throw new Error('Could not infer mission id. Pass --mission-id.');
1047
+ }
1048
+ return missionId;
605
1049
  };
606
1050
  const commandProduction = async (subcommand, positionals, ctx) => {
607
1051
  if (subcommand === 'analyze') {
608
- print(sanitizeProductionOutput(await ctx.client.v1.analyzeProduction({
609
- imageUrl: requireFlag(ctx.flags, 'image-url'),
1052
+ const analyzed = await ctx.client.v1.analyzeProduction({
1053
+ ...(await resolveProductionAnalyzeSource(ctx)),
610
1054
  agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
611
1055
  name: getFlag(ctx.flags, 'name'),
612
- })));
1056
+ });
1057
+ const status = hasFlag(ctx.flags, 'wait')
1058
+ ? await pollProductionStatus({
1059
+ ctx,
1060
+ orderId: analyzed.orderId,
1061
+ terminalStatuses: productionAnalyzeTerminalStatuses,
1062
+ })
1063
+ : undefined;
1064
+ print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ analyzed, status })));
613
1065
  return;
614
1066
  }
615
1067
  if (subcommand === 'status') {
616
1068
  const orderId = requirePositional(positionals, 2, 'order-id');
617
- print(sanitizeProductionOutput(await ctx.client.v1.getProductionStatus(orderId)));
1069
+ const status = hasFlag(ctx.flags, 'wait')
1070
+ ? await pollProductionStatus({
1071
+ ctx,
1072
+ orderId,
1073
+ terminalStatuses: new Set([
1074
+ ...productionAnalyzeTerminalStatuses,
1075
+ ...productionExecuteTerminalStatuses,
1076
+ ]),
1077
+ })
1078
+ : await ctx.client.v1.getProductionStatus(orderId);
1079
+ print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ status })));
618
1080
  return;
619
1081
  }
620
1082
  if (subcommand === 'execute') {
@@ -633,18 +1095,185 @@ const commandProduction = async (subcommand, positionals, ctx) => {
633
1095
  const partExtractorFlag = getPartExtractorFlag(ctx.flags);
634
1096
  const agentVersion = partExtractorFlag == null
635
1097
  ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
636
- : resolvePartExtractorInternalId(partExtractorFlag);
637
- print(sanitizeProductionOutput(await ctx.client.v1.executeProduction({
1098
+ : resolvePartExtractorPublicName(partExtractorFlag);
1099
+ const executed = await ctx.client.v1.executeProduction({
638
1100
  orderId,
639
1101
  missionId,
640
1102
  confirmedTaskIds,
641
1103
  agentVersion,
642
1104
  partExtractionMode,
643
- })));
1105
+ });
1106
+ const status = hasFlag(ctx.flags, 'wait')
1107
+ ? await pollProductionStatus({
1108
+ ctx,
1109
+ orderId,
1110
+ terminalStatuses: productionExecuteTerminalStatuses,
1111
+ })
1112
+ : undefined;
1113
+ print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ executed, status })));
644
1114
  return;
645
1115
  }
646
1116
  throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
647
1117
  };
1118
+ const commandParts = async (subcommand, ctx) => {
1119
+ const runPartSplit = async (source) => {
1120
+ let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
1121
+ let analyzed;
1122
+ let status;
1123
+ if (orderId == null) {
1124
+ const result = await ctx.client.v1.analyzeProduction({
1125
+ ...(source ?? (await resolveProductionAnalyzeSource(ctx))),
1126
+ agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
1127
+ name: getFlag(ctx.flags, 'name'),
1128
+ });
1129
+ analyzed = result;
1130
+ orderId = result.orderId;
1131
+ if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1132
+ status = await pollProductionStatus({
1133
+ ctx,
1134
+ orderId,
1135
+ terminalStatuses: productionAnalyzeTerminalStatuses,
1136
+ });
1137
+ }
1138
+ }
1139
+ else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
1140
+ status = await pollProductionStatus({
1141
+ ctx,
1142
+ orderId,
1143
+ terminalStatuses: productionAnalyzeTerminalStatuses,
1144
+ });
1145
+ }
1146
+ let taskIds = getFlagValues(ctx.flags, 'task-id');
1147
+ if (hasFlag(ctx.flags, 'all-ready')) {
1148
+ if (status == null) {
1149
+ status = await ctx.client.v1.getProductionStatus(orderId);
1150
+ }
1151
+ taskIds = readyTaskIdsFromStatus(status);
1152
+ if (taskIds.length === 0) {
1153
+ throw new Error('No ready tasks were found for --all-ready.');
1154
+ }
1155
+ }
1156
+ let executed;
1157
+ let executionStatus;
1158
+ if (taskIds.length > 0) {
1159
+ if (status == null) {
1160
+ status = await ctx.client.v1.getProductionStatus(orderId);
1161
+ }
1162
+ const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
1163
+ const partExtractorFlag = getPartExtractorFlag(ctx.flags);
1164
+ const agentVersion = partExtractorFlag == null
1165
+ ? await resolvePartExtractorFromMission(ctx, orderId, missionId)
1166
+ : resolvePartExtractorPublicName(partExtractorFlag);
1167
+ const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
1168
+ if (partExtractionMode != null &&
1169
+ partExtractionMode !== 'fast' &&
1170
+ partExtractionMode !== 'high_quality') {
1171
+ throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
1172
+ }
1173
+ executed = await ctx.client.v1.executeProduction({
1174
+ orderId,
1175
+ missionId,
1176
+ confirmedTaskIds: taskIds,
1177
+ agentVersion,
1178
+ partExtractionMode,
1179
+ });
1180
+ if (hasFlag(ctx.flags, 'wait')) {
1181
+ executionStatus = await pollProductionStatus({
1182
+ ctx,
1183
+ orderId,
1184
+ terminalStatuses: productionExecuteTerminalStatuses,
1185
+ });
1186
+ }
1187
+ }
1188
+ return sanitizeProductionOutput({
1189
+ analyzed,
1190
+ status,
1191
+ executed,
1192
+ executionStatus,
1193
+ });
1194
+ };
1195
+ if (subcommand === 'split') {
1196
+ print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
1197
+ return;
1198
+ }
1199
+ if (subcommand === 'compare') {
1200
+ if (getFlag(ctx.flags, 'order-id') != null) {
1201
+ throw new Error('parts compare requires an image source. Use --file, --source-url, --source-id, --image-url, --file-ref-json, --stdin, or --data-uri.');
1202
+ }
1203
+ const preprocessPrompt = getFlag(ctx.flags, 'preprocess-prompt');
1204
+ if (preprocessPrompt === 'true') {
1205
+ throw new Error('Missing required flag value: --preprocess-prompt <text>');
1206
+ }
1207
+ if (preprocessPrompt != null && getFlagValues(ctx.flags, 'task-id').length > 0) {
1208
+ throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
1209
+ }
1210
+ if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
1211
+ throw new Error('parts compare preprocessing creates a second order. Omit --mission-id and let the CLI infer it.');
1212
+ }
1213
+ const imageUrlFlag = getFlag(ctx.flags, 'image-url');
1214
+ if (imageUrlFlag === 'true') {
1215
+ throw new Error('Missing required flag value: --image-url <url>');
1216
+ }
1217
+ const inputSource = imageUrlFlag == null
1218
+ ? productionAnalyzeSourceFromSource(await requireSource({
1219
+ flags: ctx.flags,
1220
+ client: ctx.client,
1221
+ fileMediaType: 'image',
1222
+ }))
1223
+ : { imageUrl: imageUrlFlag };
1224
+ const direct = await runPartSplit(inputSource);
1225
+ let preprocessed = null;
1226
+ if (preprocessPrompt != null && preprocessPrompt !== 'true') {
1227
+ try {
1228
+ const queued = await ctx.client.v2.generateImage({
1229
+ source: sourceFromProductionAnalyzeSource(inputSource),
1230
+ prompt: preprocessPrompt,
1231
+ modelId: getFlag(ctx.flags, 'preprocess-model-id') ?? defaultImageModelId,
1232
+ batchSize: 1,
1233
+ negativePrompt: getFlag(ctx.flags, 'preprocess-negative-prompt'),
1234
+ seed: getFlag(ctx.flags, 'preprocess-seed') == null
1235
+ ? undefined
1236
+ : parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
1237
+ });
1238
+ const job = await ctx.client.v2.pollJob(queued.jobId, {
1239
+ intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
1240
+ timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
1241
+ onPoll: current => {
1242
+ stderr.write(`[poll] preprocess job=${current.id} status=${current.status}\n`);
1243
+ },
1244
+ });
1245
+ if (job.status !== 'completed') {
1246
+ throw new Error(`Preprocessing failed: job=${job.id} status=${job.status}`);
1247
+ }
1248
+ const generatedSource = imageSourceFromCompletedJob(job);
1249
+ preprocessed = {
1250
+ ok: true,
1251
+ image: {
1252
+ queued,
1253
+ job,
1254
+ source: generatedSource,
1255
+ },
1256
+ split: await runPartSplit(generatedSource),
1257
+ };
1258
+ }
1259
+ catch (error) {
1260
+ if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
1261
+ throw error;
1262
+ }
1263
+ preprocessed = {
1264
+ ok: false,
1265
+ error: errorToOutput(error),
1266
+ };
1267
+ }
1268
+ }
1269
+ print(await withOptionalDownloads(ctx.flags, {
1270
+ direct,
1271
+ preprocessed,
1272
+ }));
1273
+ return;
1274
+ }
1275
+ throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
1276
+ };
648
1277
  const run = async () => {
649
1278
  const parsed = parseArgs(argv.slice(2));
650
1279
  if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
@@ -664,6 +1293,9 @@ const run = async () => {
664
1293
  case 'files':
665
1294
  await commandFiles(subcommand, parsed.positionals, ctx);
666
1295
  return;
1296
+ case 'source':
1297
+ await commandSource(subcommand, ctx);
1298
+ return;
667
1299
  case 'image':
668
1300
  await commandImage(subcommand, ctx);
669
1301
  return;
@@ -676,6 +1308,9 @@ const run = async () => {
676
1308
  case 'production':
677
1309
  await commandProduction(subcommand, parsed.positionals, ctx);
678
1310
  return;
1311
+ case 'parts':
1312
+ await commandParts(subcommand, ctx);
1313
+ return;
679
1314
  default:
680
1315
  throw new Error(`Unknown command: ${command}`);
681
1316
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "assethub": "./dist/index.js"
7
+ "assethub": "dist/index.js"
8
8
  },
9
9
  "main": "./dist/index.js",
10
10
  "types": "./dist/index.d.ts",
@@ -20,19 +20,19 @@
20
20
  "README.md",
21
21
  "LICENSE"
22
22
  ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
25
+ "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
26
+ },
23
27
  "license": "MIT",
24
28
  "publishConfig": {
25
29
  "access": "public"
26
30
  },
27
31
  "dependencies": {
28
- "@assethub/api-client": "^0.1.0"
32
+ "@assethub/api-client": "^0.1.1"
29
33
  },
30
34
  "devDependencies": {
31
35
  "@types/node": "24.10.1",
32
36
  "typescript": "5.9.3"
33
- },
34
- "scripts": {
35
- "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
36
- "typecheck": "tsc -p tsconfig.typecheck.json --noEmit"
37
37
  }
38
- }
38
+ }