@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.
- package/README.md +37 -5
- package/dist/index.js +679 -44
- 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
|
|
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 --
|
|
28
|
-
assethub mesh generate --source-url https://example.com/input.png --model-id meshGen.hunyuan31 --wait --
|
|
29
|
-
assethub
|
|
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
|
|
49
|
-
|
|
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.
|
|
46
|
+
option.publicName,
|
|
54
47
|
]),
|
|
55
48
|
]));
|
|
56
|
-
const isInternalPartExtractorId = (value) => value
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
170
|
-
if (
|
|
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
|
|
173
|
+
return publicName;
|
|
174
174
|
};
|
|
175
|
-
const resolvePartExtractorForAnalyze = (flags) =>
|
|
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 (
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
637
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Command line interface for AssetHub.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"assethub": "
|
|
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.
|
|
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
|
+
}
|