@assethub/cli 0.1.2 → 0.1.4
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 +36 -5
- package/dist/index.js +705 -152
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ Command line interface for AssetHub.
|
|
|
6
6
|
export ASSETHUB_API_KEY=ah_live_xxx
|
|
7
7
|
|
|
8
8
|
assethub models list
|
|
9
|
-
assethub models list --domain meshGen --ids-only
|
|
10
9
|
assethub files import https://example.com/input.png --media-type image
|
|
11
10
|
assethub source create --file ./input.png --media-type image
|
|
12
11
|
assethub image generate --prompt "stylized prop concept" --wait
|
|
13
|
-
assethub mesh generate --file ./input.png --
|
|
12
|
+
assethub mesh generate --file ./input.png --wait
|
|
14
13
|
assethub jobs watch job_123 --download --out-dir ./out/job
|
|
15
14
|
assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
|
|
15
|
+
assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Configuration:
|
|
@@ -26,21 +26,41 @@ Agent-friendly login:
|
|
|
26
26
|
```sh
|
|
27
27
|
printf '%s' "$ASSETHUB_API_KEY" | assethub auth login --api-key-stdin
|
|
28
28
|
assethub image generate --prompt "small stylized game asset chest" --wait --download --out-dir ./out/image
|
|
29
|
-
assethub mesh generate --source-url https://example.com/input.png --
|
|
29
|
+
assethub mesh generate --source-url https://example.com/input.png --wait --download --out-dir ./out/mesh
|
|
30
30
|
cat ./character.png | assethub parts split --stdin --file-name character.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
|
|
31
|
+
base64 -i ./character.png | assethub parts split --stdin-base64 --file-name character.png --content-type image/png --part-extractor "V1.5" --wait --all-ready
|
|
32
|
+
printf '{"image_url":{"url":"data:image/png;base64,..."}}' | assethub parts split --stdin-json --part-extractor "V1.5" --wait --all-ready
|
|
33
|
+
assethub parts split --clipboard --file-name character.png --part-extractor "V1.5" --wait --all-ready
|
|
34
|
+
cat ./character.png | assethub parts compare --stdin --file-name character.png --preprocess-prompt "clean white background" --part-extractor "V1.5" --wait --all-ready
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
Agent-oriented input:
|
|
34
38
|
|
|
35
39
|
- `--file <path>` uploads a local image or mesh through the public file API.
|
|
36
|
-
- `--stdin --file-name <name>` accepts piped bytes. Give a file name with
|
|
37
|
-
extension when possible.
|
|
40
|
+
- `--stdin --file-name <name>` accepts raw piped bytes. Give a file name with
|
|
41
|
+
an extension when possible.
|
|
42
|
+
- `--stdin-base64 --file-name <name> --content-type <type>` accepts raw base64
|
|
43
|
+
from stdin. It also accepts a full `data:*;base64,...` value.
|
|
44
|
+
- `--stdin-data-uri` accepts a data URI from stdin.
|
|
45
|
+
- `--stdin-json` accepts common agent payload shapes from stdin, including
|
|
46
|
+
OpenAI-style `{"image_url":{"url":"..."}}`, Anthropic-style
|
|
47
|
+
`{"source":{"type":"base64","media_type":"image/png","data":"..."}}`,
|
|
48
|
+
`resourceId`, `fileRef`, `dataUri`, `base64`, `url`, and local `path`.
|
|
49
|
+
- `--source-json <json|@file|@->` parses the same JSON shapes from an argument,
|
|
50
|
+
a file, or stdin.
|
|
38
51
|
- `--data-uri <uri>` accepts `data:image/png;base64,...` style inputs and uploads
|
|
39
52
|
them before running the target command.
|
|
53
|
+
- `--clipboard` reads an image from the macOS clipboard for image inputs. It
|
|
54
|
+
uses `pngpaste` when available and falls back to the system clipboard bridge.
|
|
40
55
|
- `--source-url`, `--source-id`/`--source-resource-id`, and `--file-ref-json`
|
|
41
56
|
can be reused across image, mesh, and part extraction commands where the API
|
|
42
57
|
supports them.
|
|
43
58
|
|
|
59
|
+
For Codex or other AI agents, the important contract is: if the runtime can
|
|
60
|
+
expose the attachment as bytes, base64, a data URI, a local path, an
|
|
61
|
+
OpenAI/Anthropic-style JSON object, or a URL, the CLI can consume it directly.
|
|
62
|
+
It does not require writing a temporary file first.
|
|
63
|
+
|
|
44
64
|
Agent-oriented output:
|
|
45
65
|
|
|
46
66
|
- stdout is one final JSON object.
|
|
@@ -48,6 +68,17 @@ Agent-oriented output:
|
|
|
48
68
|
- `--download --out-dir <dir>` recursively downloads public artifact URLs from
|
|
49
69
|
job or production status payloads and writes a `manifest.json`.
|
|
50
70
|
|
|
71
|
+
`assethub parts compare` runs the normal part split first. When
|
|
72
|
+
`--preprocess-prompt <text>` is provided, it then uses the Nano Banana image
|
|
73
|
+
model to generate a preprocessed image, waits for that image job to finish, and
|
|
74
|
+
runs a second split on the generated image. The final JSON contains `direct`
|
|
75
|
+
and `preprocessed` sections so agents can compare both outputs. If
|
|
76
|
+
preprocessing fails, the direct result is still printed with
|
|
77
|
+
`preprocessed.ok: false`; pass `--fail-on-preprocess-error` when a non-zero exit
|
|
78
|
+
is preferred. Use `--all-ready` rather than explicit `--task-id` values when
|
|
79
|
+
preprocessing is enabled, because the generated-image split creates a separate
|
|
80
|
+
production order.
|
|
81
|
+
|
|
51
82
|
Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
|
|
52
83
|
only when the `v2` image endpoint is available in the target environment.
|
|
53
84
|
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
5
|
import { basename, dirname, extname, join } from 'node:path';
|
|
5
6
|
import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
|
|
7
9
|
const defaultBaseUrl = 'https://app.assethub.io';
|
|
8
10
|
const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
|
|
@@ -47,6 +49,7 @@ const partExtractorPublicNameByInput = new Map(partExtractorOptions.flatMap(opti
|
|
|
47
49
|
]),
|
|
48
50
|
]));
|
|
49
51
|
const isInternalPartExtractorId = (value) => value.trim().includes('_');
|
|
52
|
+
const execFileAsync = promisify(execFile);
|
|
50
53
|
const usage = `AssetHub CLI
|
|
51
54
|
|
|
52
55
|
Usage:
|
|
@@ -58,17 +61,18 @@ Usage:
|
|
|
58
61
|
assethub models get <model-id>
|
|
59
62
|
assethub files import <url> --media-type image|mesh
|
|
60
63
|
assethub files upload <file> --media-type image|mesh
|
|
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]
|
|
64
|
+
assethub source create (--file <path> | --source-url <url> | --source-id <id> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) --media-type image|mesh [--file-name <name>] [--content-type <type>]
|
|
65
|
+
assethub image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard] [--file-name <name>] [--content-type <type>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--api-version v1|v2]
|
|
63
66
|
assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
|
|
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>]
|
|
67
|
+
assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) [--file-name <name>] [--content-type <type>] [--model-id <id>] [--name <name>] [--wait] [--download --out-dir <dir>]
|
|
68
|
+
assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri>) --model-id <id> [--file-name <name>] [--content-type <type>] [--wait] [--download --out-dir <dir>]
|
|
66
69
|
assethub jobs get <job-id> [--download --out-dir <dir>]
|
|
67
70
|
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>]
|
|
71
|
+
assethub production analyze (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--name <name>] [--wait] [--download --out-dir <dir>]
|
|
69
72
|
assethub production status <order-id> [--wait] [--download --out-dir <dir>]
|
|
70
73
|
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>]
|
|
74
|
+
assethub parts split (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard | --order-id <id>) [--file-name <name>] [--content-type <type>] [--part-extractor <name>] [--wait] [--task-id <id>...] [--mission-id <id>] [--all-ready] [--download --out-dir <dir>]
|
|
75
|
+
assethub parts compare (--file <path> | --source-url <url> | --source-id <id> | --image-url <url> | --file-ref-json <json> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard) [--file-name <name>] [--content-type <type>] [--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>]
|
|
72
76
|
|
|
73
77
|
Global options:
|
|
74
78
|
--api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
|
|
@@ -80,14 +84,19 @@ Global options:
|
|
|
80
84
|
Examples:
|
|
81
85
|
ASSETHUB_API_KEY=ah_live_xxx assethub models list
|
|
82
86
|
assethub auth login --api-key-stdin
|
|
83
|
-
assethub models list --
|
|
87
|
+
assethub models list --ids-only
|
|
84
88
|
assethub files upload ./input.png --media-type image
|
|
85
89
|
assethub source create --file ./input.png --media-type image
|
|
90
|
+
pbpaste | assethub source create --stdin-data-uri --media-type image
|
|
91
|
+
assethub source create --clipboard --media-type image --file-name clipboard.png
|
|
92
|
+
printf '{"image_url":{"url":"data:image/png;base64,..."}}' | assethub parts split --stdin-json --part-extractor "V1.5" --wait
|
|
93
|
+
base64 -i ./input.png | assethub parts split --stdin-base64 --file-name input.png --content-type image/png --part-extractor "V1.5" --wait
|
|
86
94
|
cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
|
|
87
95
|
assethub image generate --prompt "stylized prop concept" --wait
|
|
88
|
-
assethub mesh generate --file ./input.png --
|
|
96
|
+
assethub mesh generate --file ./input.png --wait
|
|
89
97
|
assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
|
|
90
98
|
assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
|
|
99
|
+
assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
|
|
91
100
|
`;
|
|
92
101
|
const print = (value) => {
|
|
93
102
|
stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -325,31 +334,73 @@ const createContext = async (flags) => {
|
|
|
325
334
|
}),
|
|
326
335
|
};
|
|
327
336
|
};
|
|
337
|
+
const isRecord = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
338
|
+
const isBlobLocation = (value) => {
|
|
339
|
+
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (value.type === 'supabase' &&
|
|
343
|
+
typeof value.bucket === 'string' &&
|
|
344
|
+
typeof value.path === 'string') {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
if (value.type === 'signedSupabase' &&
|
|
348
|
+
typeof value.signedUrl === 'string') {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (value.type === 'asset' && typeof value.assetId === 'string') {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
};
|
|
328
356
|
const parseFileRef = (value) => {
|
|
329
357
|
const parsed = JSON.parse(value);
|
|
330
|
-
if (
|
|
358
|
+
if (!isBlobLocation(parsed)) {
|
|
331
359
|
throw new Error('--file-ref-json must be a BlobLocation JSON object');
|
|
332
360
|
}
|
|
333
361
|
return parsed;
|
|
334
362
|
};
|
|
335
|
-
const
|
|
336
|
-
const
|
|
337
|
-
return new Blob([
|
|
363
|
+
const blobFromBuffer = (buffer, type) => {
|
|
364
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
365
|
+
return new Blob([arrayBuffer], type == null ? undefined : { type });
|
|
338
366
|
};
|
|
339
|
-
const
|
|
340
|
-
|
|
367
|
+
const readBlob = async (filePath, type) => blobFromBuffer(await readFile(filePath), type);
|
|
368
|
+
const getContentTypeFlag = (flags) => getFlag(flags, 'content-type') ?? getFlag(flags, 'mime-type');
|
|
369
|
+
const blobFromDataUri = (value, flagName = '--data-uri') => {
|
|
370
|
+
const trimmed = value.trim();
|
|
371
|
+
const match = trimmed.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
|
|
341
372
|
if (match == null) {
|
|
342
|
-
throw new Error(
|
|
373
|
+
throw new Error(`${flagName} must be a valid data URI.`);
|
|
343
374
|
}
|
|
344
375
|
const contentType = match[1] === '' ? undefined : match[1];
|
|
345
376
|
const isBase64 = match[2] != null;
|
|
346
|
-
const payload = match[3] ?? '';
|
|
377
|
+
const payload = (match[3] ?? '').replace(/\s+/g, '');
|
|
347
378
|
const buffer = isBase64
|
|
348
379
|
? Buffer.from(payload, 'base64')
|
|
349
380
|
: Buffer.from(decodeURIComponent(payload), 'utf8');
|
|
350
|
-
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
351
381
|
return {
|
|
352
|
-
blob:
|
|
382
|
+
blob: blobFromBuffer(buffer, contentType),
|
|
383
|
+
contentType,
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
const blobFromBase64 = (value, contentType, flagName) => {
|
|
387
|
+
const trimmed = value.trim();
|
|
388
|
+
if (trimmed.startsWith('data:')) {
|
|
389
|
+
return blobFromDataUri(trimmed, flagName);
|
|
390
|
+
}
|
|
391
|
+
const normalized = trimmed.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
|
|
392
|
+
if (normalized === '') {
|
|
393
|
+
throw new Error(`${flagName} input is empty.`);
|
|
394
|
+
}
|
|
395
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(normalized)) {
|
|
396
|
+
throw new Error(`${flagName} must contain base64 data or a data URI.`);
|
|
397
|
+
}
|
|
398
|
+
const buffer = Buffer.from(normalized, 'base64');
|
|
399
|
+
if (buffer.byteLength === 0) {
|
|
400
|
+
throw new Error(`${flagName} did not decode to any bytes.`);
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
blob: blobFromBuffer(buffer, contentType),
|
|
353
404
|
contentType,
|
|
354
405
|
};
|
|
355
406
|
};
|
|
@@ -374,54 +425,392 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
|
374
425
|
}
|
|
375
426
|
return `${fileName}.${extensionFromMimeType(mimeType, mediaType)}`;
|
|
376
427
|
};
|
|
428
|
+
const missingSourceMessage = 'Missing source. Pass --file, --source-url, --source-resource-id, --source-id, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.';
|
|
429
|
+
const exactlyOneSourceMessage = 'Pass exactly one source input: --file, --source-url/--image-url, --source-id/--source-resource-id, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.';
|
|
430
|
+
const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ?? getFlag(flags, 'image-url') ?? getFlag(flags, 'url');
|
|
431
|
+
const assertFlagHasValue = (value, flagDescription) => {
|
|
432
|
+
if (value === 'true' || value.trim() === '') {
|
|
433
|
+
throw new Error(`Missing required flag value: ${flagDescription}`);
|
|
434
|
+
}
|
|
435
|
+
return value;
|
|
436
|
+
};
|
|
437
|
+
const sourceInputCount = (flags) => [
|
|
438
|
+
getSourceResourceIdFlag(flags) != null,
|
|
439
|
+
getSourceUrlFlag(flags) != null,
|
|
440
|
+
getFlag(flags, 'file-ref-json') != null,
|
|
441
|
+
getFlag(flags, 'file') != null,
|
|
442
|
+
getFlag(flags, 'data-uri') != null,
|
|
443
|
+
hasFlag(flags, 'stdin'),
|
|
444
|
+
hasFlag(flags, 'stdin-base64'),
|
|
445
|
+
hasFlag(flags, 'stdin-data-uri'),
|
|
446
|
+
hasFlag(flags, 'stdin-json'),
|
|
447
|
+
getFlag(flags, 'source-json') != null,
|
|
448
|
+
hasFlag(flags, 'clipboard'),
|
|
449
|
+
].filter(Boolean).length;
|
|
450
|
+
const sourceFromString = (value) => {
|
|
451
|
+
const trimmed = value.trim();
|
|
452
|
+
if (trimmed === '' || trimmed.startsWith('data:')) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const url = new URL(trimmed);
|
|
457
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
458
|
+
return { url: trimmed };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// Ignore non-URL strings.
|
|
463
|
+
}
|
|
464
|
+
return undefined;
|
|
465
|
+
};
|
|
466
|
+
const getFirstString = (record, keys) => {
|
|
467
|
+
for (const key of keys) {
|
|
468
|
+
const value = record[key];
|
|
469
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return undefined;
|
|
474
|
+
};
|
|
475
|
+
const stringFromJsonImageUrl = (value) => {
|
|
476
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
if (isRecord(value)) {
|
|
480
|
+
return getFirstString(value, ['url']);
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
};
|
|
484
|
+
const descriptorFromDataUri = (value, mediaType, fileName, flagName) => {
|
|
485
|
+
const { blob, contentType } = blobFromDataUri(value, flagName);
|
|
486
|
+
return {
|
|
487
|
+
kind: 'blob',
|
|
488
|
+
blob,
|
|
489
|
+
fileName: ensureFileNameExtension(fileName, mediaType, contentType),
|
|
490
|
+
contentType,
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
const descriptorFromBase64 = ({ value, contentType, fileName, mediaType, flagName, }) => {
|
|
494
|
+
const decoded = blobFromBase64(value, contentType, flagName);
|
|
495
|
+
return {
|
|
496
|
+
kind: 'blob',
|
|
497
|
+
blob: decoded.blob,
|
|
498
|
+
fileName: ensureFileNameExtension(fileName, mediaType, decoded.contentType),
|
|
499
|
+
contentType: decoded.contentType,
|
|
500
|
+
};
|
|
501
|
+
};
|
|
502
|
+
const sourceDescriptorFromJson = (value, options) => {
|
|
503
|
+
if (typeof value === 'string') {
|
|
504
|
+
const trimmed = value.trim();
|
|
505
|
+
if (trimmed === '') {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
if (trimmed.startsWith('data:')) {
|
|
509
|
+
return descriptorFromDataUri(trimmed, options.mediaType, options.fallbackFileName, '--source-json');
|
|
510
|
+
}
|
|
511
|
+
const source = sourceFromString(trimmed);
|
|
512
|
+
return source == null ? undefined : { kind: 'source', source };
|
|
513
|
+
}
|
|
514
|
+
if (Array.isArray(value)) {
|
|
515
|
+
for (const item of value) {
|
|
516
|
+
const found = sourceDescriptorFromJson(item, options);
|
|
517
|
+
if (found != null) {
|
|
518
|
+
return found;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
if (!isRecord(value)) {
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
const nestedSource = value.source;
|
|
527
|
+
if (nestedSource != null) {
|
|
528
|
+
const found = sourceDescriptorFromJson(nestedSource, options);
|
|
529
|
+
if (found != null) {
|
|
530
|
+
return found;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const imageUrl = stringFromJsonImageUrl(value.image_url ?? value.imageUrl);
|
|
534
|
+
if (imageUrl != null) {
|
|
535
|
+
if (imageUrl.trim().startsWith('data:')) {
|
|
536
|
+
return descriptorFromDataUri(imageUrl, options.mediaType, options.fallbackFileName, '--source-json');
|
|
537
|
+
}
|
|
538
|
+
return { kind: 'source', source: { url: imageUrl } };
|
|
539
|
+
}
|
|
540
|
+
const resourceId = getFirstString(value, [
|
|
541
|
+
'resourceId',
|
|
542
|
+
'resource_id',
|
|
543
|
+
'sourceResourceId',
|
|
544
|
+
'source_resource_id',
|
|
545
|
+
'sourceId',
|
|
546
|
+
'source_id',
|
|
547
|
+
'imageAssetId',
|
|
548
|
+
'image_asset_id',
|
|
549
|
+
'assetId',
|
|
550
|
+
'asset_id',
|
|
551
|
+
]);
|
|
552
|
+
if (resourceId != null) {
|
|
553
|
+
return { kind: 'source', source: { resourceId } };
|
|
554
|
+
}
|
|
555
|
+
const fileRefValue = value.fileRef ?? value.file_ref ?? value.imageBlobLocation ?? value.image_blob_location ?? value.blobLocation ?? value.blob_location;
|
|
556
|
+
if (isBlobLocation(fileRefValue)) {
|
|
557
|
+
return { kind: 'source', source: { fileRef: fileRefValue } };
|
|
558
|
+
}
|
|
559
|
+
const dataUri = getFirstString(value, ['dataUri', 'data_uri']);
|
|
560
|
+
if (dataUri != null) {
|
|
561
|
+
return descriptorFromDataUri(dataUri, options.mediaType, getFirstString(value, ['fileName', 'file_name', 'name']) ??
|
|
562
|
+
options.fallbackFileName, '--source-json');
|
|
563
|
+
}
|
|
564
|
+
const contentType = getFirstString(value, [
|
|
565
|
+
'contentType',
|
|
566
|
+
'content_type',
|
|
567
|
+
'mimeType',
|
|
568
|
+
'mime_type',
|
|
569
|
+
'media_type',
|
|
570
|
+
]);
|
|
571
|
+
const base64Value = getFirstString(value, ['base64', 'data', 'content']);
|
|
572
|
+
if (base64Value != null && (value.type === 'base64' || contentType != null)) {
|
|
573
|
+
return descriptorFromBase64({
|
|
574
|
+
value: base64Value,
|
|
575
|
+
contentType,
|
|
576
|
+
fileName: getFirstString(value, ['fileName', 'file_name', 'name']) ??
|
|
577
|
+
options.fallbackFileName,
|
|
578
|
+
mediaType: options.mediaType,
|
|
579
|
+
flagName: '--source-json',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const url = getFirstString(value, ['sourceUrl', 'source_url', 'url', 'publicUrl', 'public_url']);
|
|
583
|
+
if (url != null) {
|
|
584
|
+
const source = sourceFromString(url);
|
|
585
|
+
if (source != null) {
|
|
586
|
+
return { kind: 'source', source };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const path = getFirstString(value, ['path', 'file', 'filePath', 'file_path']);
|
|
590
|
+
if (path != null) {
|
|
591
|
+
return { kind: 'file', path };
|
|
592
|
+
}
|
|
593
|
+
const preferredKeys = [
|
|
594
|
+
'input',
|
|
595
|
+
'image',
|
|
596
|
+
'asset',
|
|
597
|
+
'file',
|
|
598
|
+
'message',
|
|
599
|
+
'messages',
|
|
600
|
+
'content',
|
|
601
|
+
'attachments',
|
|
602
|
+
];
|
|
603
|
+
for (const key of preferredKeys) {
|
|
604
|
+
const found = sourceDescriptorFromJson(value[key], options);
|
|
605
|
+
if (found != null) {
|
|
606
|
+
return found;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
for (const entry of Object.values(value)) {
|
|
610
|
+
const found = sourceDescriptorFromJson(entry, options);
|
|
611
|
+
if (found != null) {
|
|
612
|
+
return found;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return undefined;
|
|
616
|
+
};
|
|
617
|
+
const parseJsonValue = (raw, sourceName) => {
|
|
618
|
+
try {
|
|
619
|
+
return JSON.parse(raw);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
throw new Error(`${sourceName} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const readSourceJsonFlag = async (flags) => {
|
|
626
|
+
const value = getFlag(flags, 'source-json');
|
|
627
|
+
if (value == null || value === 'true' || value.trim() === '') {
|
|
628
|
+
throw new Error('Missing required flag value: --source-json <json|@file|@->');
|
|
629
|
+
}
|
|
630
|
+
if (value === '@-' || value === '-') {
|
|
631
|
+
return await readStdin();
|
|
632
|
+
}
|
|
633
|
+
if (value.startsWith('@')) {
|
|
634
|
+
return await readFile(value.slice(1), 'utf8');
|
|
635
|
+
}
|
|
636
|
+
return value;
|
|
637
|
+
};
|
|
638
|
+
const readClipboardImage = async () => {
|
|
639
|
+
const filePath = join(tmpdir(), `assethub-clipboard-${process.pid}-${Date.now()}.png`);
|
|
640
|
+
const readTempFile = async () => await readBlob(filePath, 'image/png');
|
|
641
|
+
try {
|
|
642
|
+
await execFileAsync('pngpaste', [filePath], { timeout: 10000 });
|
|
643
|
+
return await readTempFile();
|
|
644
|
+
}
|
|
645
|
+
catch (pngpasteError) {
|
|
646
|
+
const quoteAppleScriptString = (value) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
647
|
+
const pngClass = `${String.fromCharCode(0x00ab)}class PNGf${String.fromCharCode(0x00bb)}`;
|
|
648
|
+
const script = [
|
|
649
|
+
`set outPath to ${quoteAppleScriptString(filePath)}`,
|
|
650
|
+
`set pngData to the clipboard as ${pngClass}`,
|
|
651
|
+
'set fileRef to open for access (POSIX file outPath) with write permission',
|
|
652
|
+
'try',
|
|
653
|
+
'set eof fileRef to 0',
|
|
654
|
+
'write pngData to fileRef',
|
|
655
|
+
'close access fileRef',
|
|
656
|
+
'on error errMsg',
|
|
657
|
+
'try',
|
|
658
|
+
'close access fileRef',
|
|
659
|
+
'end try',
|
|
660
|
+
'error errMsg',
|
|
661
|
+
'end try',
|
|
662
|
+
].join('\n');
|
|
663
|
+
try {
|
|
664
|
+
await execFileAsync('osascript', ['-e', script], { timeout: 10000 });
|
|
665
|
+
return await readTempFile();
|
|
666
|
+
}
|
|
667
|
+
catch (osascriptError) {
|
|
668
|
+
const details = osascriptError instanceof Error
|
|
669
|
+
? osascriptError.message
|
|
670
|
+
: pngpasteError instanceof Error
|
|
671
|
+
? pngpasteError.message
|
|
672
|
+
: 'unknown error';
|
|
673
|
+
throw new Error(`Could not read an image from the clipboard. Copy an image, install pngpaste, or pass bytes through --stdin/--stdin-base64/--stdin-json. Details: ${details}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
await unlink(filePath).catch(() => undefined);
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
|
|
681
|
+
if (descriptor.kind === 'source') {
|
|
682
|
+
return descriptor.source;
|
|
683
|
+
}
|
|
684
|
+
const file = descriptor.kind === 'file'
|
|
685
|
+
? await readBlob(descriptor.path, descriptor.contentType)
|
|
686
|
+
: descriptor.blob;
|
|
687
|
+
const fileName = descriptor.kind === 'file'
|
|
688
|
+
? basename(descriptor.path)
|
|
689
|
+
: descriptor.fileName;
|
|
690
|
+
const upload = await client.v2.uploadFile({
|
|
691
|
+
file,
|
|
692
|
+
fileName: ensureFileNameExtension(fileName, mediaType, descriptor.contentType),
|
|
693
|
+
mediaType,
|
|
694
|
+
});
|
|
695
|
+
return { fileRef: upload.fileRef };
|
|
696
|
+
};
|
|
377
697
|
const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
698
|
+
const providedCount = sourceInputCount(flags);
|
|
699
|
+
if (providedCount === 0) {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
if (providedCount > 1) {
|
|
703
|
+
throw new Error(exactlyOneSourceMessage);
|
|
704
|
+
}
|
|
378
705
|
const sourceResourceId = getSourceResourceIdFlag(flags);
|
|
379
706
|
if (sourceResourceId != null) {
|
|
380
|
-
return {
|
|
707
|
+
return {
|
|
708
|
+
resourceId: assertFlagHasValue(sourceResourceId, '--source-id/--source-resource-id <id>'),
|
|
709
|
+
};
|
|
381
710
|
}
|
|
382
|
-
const sourceUrl =
|
|
711
|
+
const sourceUrl = getSourceUrlFlag(flags);
|
|
383
712
|
if (sourceUrl != null) {
|
|
384
|
-
return {
|
|
713
|
+
return {
|
|
714
|
+
url: assertFlagHasValue(sourceUrl, '--source-url/--image-url <url>'),
|
|
715
|
+
};
|
|
385
716
|
}
|
|
386
717
|
const fileRefJson = getFlag(flags, 'file-ref-json');
|
|
387
718
|
if (fileRefJson != null) {
|
|
388
|
-
return {
|
|
719
|
+
return {
|
|
720
|
+
fileRef: parseFileRef(assertFlagHasValue(fileRefJson, '--file-ref-json <json>')),
|
|
721
|
+
};
|
|
389
722
|
}
|
|
390
723
|
const filePath = getFlag(flags, 'file');
|
|
391
724
|
if (filePath != null) {
|
|
392
|
-
|
|
393
|
-
file:
|
|
394
|
-
|
|
725
|
+
return await uploadSourceDescriptor({
|
|
726
|
+
descriptor: { kind: 'file', path: assertFlagHasValue(filePath, '--file <path>') },
|
|
727
|
+
client,
|
|
395
728
|
mediaType: fileMediaType,
|
|
396
729
|
});
|
|
397
|
-
return { fileRef: fileRef.fileRef };
|
|
398
730
|
}
|
|
399
731
|
const dataUri = getFlag(flags, 'data-uri');
|
|
400
732
|
if (dataUri != null) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
733
|
+
return await uploadSourceDescriptor({
|
|
734
|
+
descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
|
|
735
|
+
client,
|
|
736
|
+
mediaType: fileMediaType,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
if (getFlag(flags, 'source-json') != null) {
|
|
740
|
+
const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
|
|
741
|
+
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
742
|
+
mediaType: fileMediaType,
|
|
743
|
+
fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
744
|
+
});
|
|
745
|
+
if (descriptor == null) {
|
|
746
|
+
throw new Error('--source-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
|
|
747
|
+
}
|
|
748
|
+
return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
|
|
749
|
+
}
|
|
750
|
+
if (hasFlag(flags, 'stdin-json')) {
|
|
751
|
+
const parsed = parseJsonValue(await readStdin(), '--stdin-json');
|
|
752
|
+
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
753
|
+
mediaType: fileMediaType,
|
|
754
|
+
fallbackFileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
755
|
+
});
|
|
756
|
+
if (descriptor == null) {
|
|
757
|
+
throw new Error('--stdin-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
|
|
758
|
+
}
|
|
759
|
+
return await uploadSourceDescriptor({ descriptor, client, mediaType: fileMediaType });
|
|
760
|
+
}
|
|
761
|
+
if (hasFlag(flags, 'stdin-data-uri')) {
|
|
762
|
+
return await uploadSourceDescriptor({
|
|
763
|
+
descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
|
|
764
|
+
client,
|
|
765
|
+
mediaType: fileMediaType,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (hasFlag(flags, 'stdin-base64')) {
|
|
769
|
+
return await uploadSourceDescriptor({
|
|
770
|
+
descriptor: descriptorFromBase64({
|
|
771
|
+
value: await readStdin(),
|
|
772
|
+
contentType: getContentTypeFlag(flags),
|
|
773
|
+
fileName: getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType),
|
|
774
|
+
mediaType: fileMediaType,
|
|
775
|
+
flagName: '--stdin-base64',
|
|
776
|
+
}),
|
|
777
|
+
client,
|
|
778
|
+
mediaType: fileMediaType,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
if (hasFlag(flags, 'clipboard')) {
|
|
782
|
+
if (fileMediaType !== 'image') {
|
|
783
|
+
throw new Error('--clipboard is only supported for image inputs.');
|
|
784
|
+
}
|
|
785
|
+
return await uploadSourceDescriptor({
|
|
786
|
+
descriptor: {
|
|
787
|
+
kind: 'blob',
|
|
788
|
+
blob: await readClipboardImage(),
|
|
789
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'clipboard.png', fileMediaType, 'image/png'),
|
|
790
|
+
contentType: 'image/png',
|
|
791
|
+
},
|
|
792
|
+
client,
|
|
406
793
|
mediaType: fileMediaType,
|
|
407
794
|
});
|
|
408
|
-
return { fileRef: fileRef.fileRef };
|
|
409
795
|
}
|
|
410
796
|
if (hasFlag(flags, 'stdin')) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
797
|
+
return await uploadSourceDescriptor({
|
|
798
|
+
descriptor: {
|
|
799
|
+
kind: 'blob',
|
|
800
|
+
blob: await readStdinBlob(getContentTypeFlag(flags)),
|
|
801
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
|
|
802
|
+
contentType: getContentTypeFlag(flags),
|
|
803
|
+
},
|
|
804
|
+
client,
|
|
415
805
|
mediaType: fileMediaType,
|
|
416
806
|
});
|
|
417
|
-
return { fileRef: fileRef.fileRef };
|
|
418
807
|
}
|
|
419
808
|
return undefined;
|
|
420
809
|
};
|
|
421
810
|
const requireSource = async (options) => {
|
|
422
811
|
const source = await resolveSource(options);
|
|
423
812
|
if (source == null) {
|
|
424
|
-
throw new Error(
|
|
813
|
+
throw new Error(missingSourceMessage);
|
|
425
814
|
}
|
|
426
815
|
return source;
|
|
427
816
|
};
|
|
@@ -438,10 +827,6 @@ const sourceToV1ImageInput = (source) => {
|
|
|
438
827
|
throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
|
|
439
828
|
};
|
|
440
829
|
const resolveProductionAnalyzeSource = async (ctx) => {
|
|
441
|
-
const imageUrl = getFlag(ctx.flags, 'image-url');
|
|
442
|
-
if (imageUrl != null) {
|
|
443
|
-
return { imageUrl };
|
|
444
|
-
}
|
|
445
830
|
const source = await requireSource({
|
|
446
831
|
flags: ctx.flags,
|
|
447
832
|
client: ctx.client,
|
|
@@ -454,10 +839,28 @@ const resolveProductionAnalyzeSource = async (ctx) => {
|
|
|
454
839
|
return { imageUrl: source.url };
|
|
455
840
|
}
|
|
456
841
|
if (source.fileRef == null) {
|
|
457
|
-
throw new Error(
|
|
842
|
+
throw new Error(missingSourceMessage);
|
|
843
|
+
}
|
|
844
|
+
return { imageBlobLocation: source.fileRef };
|
|
845
|
+
};
|
|
846
|
+
const productionAnalyzeSourceFromSource = (source) => {
|
|
847
|
+
if (source.resourceId != null) {
|
|
848
|
+
return { imageAssetId: source.resourceId };
|
|
849
|
+
}
|
|
850
|
+
if (source.url != null) {
|
|
851
|
+
return { imageUrl: source.url };
|
|
458
852
|
}
|
|
459
853
|
return { imageBlobLocation: source.fileRef };
|
|
460
854
|
};
|
|
855
|
+
const sourceFromProductionAnalyzeSource = (source) => {
|
|
856
|
+
if (source.imageAssetId != null) {
|
|
857
|
+
return { resourceId: source.imageAssetId };
|
|
858
|
+
}
|
|
859
|
+
if (source.imageUrl != null) {
|
|
860
|
+
return { url: source.imageUrl };
|
|
861
|
+
}
|
|
862
|
+
return { fileRef: source.imageBlobLocation };
|
|
863
|
+
};
|
|
461
864
|
const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
|
|
462
865
|
const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
|
|
463
866
|
const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
|
|
@@ -502,54 +905,20 @@ const commandSource = async (subcommand, ctx) => {
|
|
|
502
905
|
throw new Error('Unknown source command. Use "source create".');
|
|
503
906
|
}
|
|
504
907
|
const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
sourceResourceId != null,
|
|
513
|
-
filePath != null,
|
|
514
|
-
useStdin,
|
|
515
|
-
dataUri != null,
|
|
516
|
-
].filter(Boolean);
|
|
517
|
-
if (provided.length !== 1) {
|
|
518
|
-
throw new Error('Pass exactly one source input: --file, --source-url, --source-id, --source-resource-id, --stdin, or --data-uri.');
|
|
519
|
-
}
|
|
520
|
-
if (sourceUrl != null) {
|
|
521
|
-
print({ source: { url: sourceUrl }, url: sourceUrl, mediaType });
|
|
908
|
+
const source = await requireSource({
|
|
909
|
+
flags: ctx.flags,
|
|
910
|
+
client: ctx.client,
|
|
911
|
+
fileMediaType: mediaType,
|
|
912
|
+
});
|
|
913
|
+
if (source.url != null) {
|
|
914
|
+
print({ source, url: source.url, mediaType });
|
|
522
915
|
return;
|
|
523
916
|
}
|
|
524
|
-
if (
|
|
525
|
-
print({
|
|
526
|
-
source: { resourceId: sourceResourceId },
|
|
527
|
-
resourceId: sourceResourceId,
|
|
528
|
-
mediaType,
|
|
529
|
-
});
|
|
917
|
+
if (source.resourceId != null) {
|
|
918
|
+
print({ source, resourceId: source.resourceId, mediaType });
|
|
530
919
|
return;
|
|
531
920
|
}
|
|
532
|
-
|
|
533
|
-
? await ctx.client.v2.uploadFile({
|
|
534
|
-
file: await readBlob(filePath),
|
|
535
|
-
fileName: basename(filePath),
|
|
536
|
-
mediaType,
|
|
537
|
-
})
|
|
538
|
-
: dataUri != null
|
|
539
|
-
? await (async () => {
|
|
540
|
-
const { blob, contentType } = blobFromDataUri(dataUri);
|
|
541
|
-
return await ctx.client.v2.uploadFile({
|
|
542
|
-
file: blob,
|
|
543
|
-
fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? 'data-uri', mediaType, contentType),
|
|
544
|
-
mediaType,
|
|
545
|
-
});
|
|
546
|
-
})()
|
|
547
|
-
: await ctx.client.v2.uploadFile({
|
|
548
|
-
file: await readStdinBlob(),
|
|
549
|
-
fileName: ensureFileNameExtension(getFlag(ctx.flags, 'file-name') ?? defaultFileNameForMediaType(mediaType), mediaType),
|
|
550
|
-
mediaType,
|
|
551
|
-
});
|
|
552
|
-
print({ source: { fileRef: upload.fileRef }, fileRef: upload.fileRef, mediaType });
|
|
921
|
+
print({ source, fileRef: source.fileRef, mediaType });
|
|
553
922
|
};
|
|
554
923
|
const commandModels = async (subcommand, positionals, ctx) => {
|
|
555
924
|
if (subcommand === 'list') {
|
|
@@ -885,6 +1254,106 @@ const withOptionalDownloads = async (flags, value) => {
|
|
|
885
1254
|
const downloads = await maybeDownloadUrls(flags, value);
|
|
886
1255
|
return downloads == null ? value : { ...value, downloads };
|
|
887
1256
|
};
|
|
1257
|
+
const firstStringValueForKeys = (value, keys) => {
|
|
1258
|
+
if (value == null || typeof value !== 'object') {
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
if (Array.isArray(value)) {
|
|
1262
|
+
for (const item of value) {
|
|
1263
|
+
const found = firstStringValueForKeys(item, keys);
|
|
1264
|
+
if (found != null) {
|
|
1265
|
+
return found;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1271
|
+
if (keys.has(key) && typeof entry === 'string' && entry.trim() !== '') {
|
|
1272
|
+
return entry;
|
|
1273
|
+
}
|
|
1274
|
+
const found = firstStringValueForKeys(entry, keys);
|
|
1275
|
+
if (found != null) {
|
|
1276
|
+
return found;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return undefined;
|
|
1280
|
+
};
|
|
1281
|
+
const firstBlobLocation = (value) => {
|
|
1282
|
+
if (value == null || typeof value !== 'object') {
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1285
|
+
if (Array.isArray(value)) {
|
|
1286
|
+
for (const item of value) {
|
|
1287
|
+
const found = firstBlobLocation(item);
|
|
1288
|
+
if (found != null) {
|
|
1289
|
+
return found;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return undefined;
|
|
1293
|
+
}
|
|
1294
|
+
const record = value;
|
|
1295
|
+
if (record.type === 'supabase' && typeof record.bucket === 'string' && typeof record.path === 'string') {
|
|
1296
|
+
return {
|
|
1297
|
+
type: 'supabase',
|
|
1298
|
+
bucket: record.bucket,
|
|
1299
|
+
path: record.path,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
if (record.type === 'signedSupabase' && typeof record.signedUrl === 'string') {
|
|
1303
|
+
return {
|
|
1304
|
+
type: 'signedSupabase',
|
|
1305
|
+
signedUrl: record.signedUrl,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
if (record.type === 'asset' && typeof record.assetId === 'string') {
|
|
1309
|
+
return {
|
|
1310
|
+
type: 'asset',
|
|
1311
|
+
assetId: record.assetId,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
for (const entry of Object.values(record)) {
|
|
1315
|
+
const found = firstBlobLocation(entry);
|
|
1316
|
+
if (found != null) {
|
|
1317
|
+
return found;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return undefined;
|
|
1321
|
+
};
|
|
1322
|
+
const imageSourceFromCompletedJob = (job) => {
|
|
1323
|
+
if (job.resourceId != null && job.resourceId.trim() !== '') {
|
|
1324
|
+
return { imageAssetId: job.resourceId };
|
|
1325
|
+
}
|
|
1326
|
+
const resourceId = firstStringValueForKeys(job, new Set(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
|
|
1327
|
+
if (resourceId != null) {
|
|
1328
|
+
return { imageAssetId: resourceId };
|
|
1329
|
+
}
|
|
1330
|
+
const imageUrl = firstStringValueForKeys(job, new Set(['imageUrl', 'image_url', 'url', 'publicUrl', 'public_url']));
|
|
1331
|
+
if (imageUrl != null) {
|
|
1332
|
+
return { imageUrl };
|
|
1333
|
+
}
|
|
1334
|
+
const blobLocation = firstBlobLocation(job);
|
|
1335
|
+
if (blobLocation != null) {
|
|
1336
|
+
return { imageBlobLocation: blobLocation };
|
|
1337
|
+
}
|
|
1338
|
+
throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
|
|
1339
|
+
};
|
|
1340
|
+
const errorToOutput = (error) => {
|
|
1341
|
+
if (error instanceof AssetHubApiError) {
|
|
1342
|
+
return {
|
|
1343
|
+
name: error.name,
|
|
1344
|
+
message: error.message,
|
|
1345
|
+
status: error.status,
|
|
1346
|
+
code: error.code,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (error instanceof Error) {
|
|
1350
|
+
return {
|
|
1351
|
+
name: error.name,
|
|
1352
|
+
message: error.message,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return { message: String(error) };
|
|
1356
|
+
};
|
|
888
1357
|
const readyTaskIdsFromStatus = (status) => {
|
|
889
1358
|
const missions = status != null && typeof status === 'object' && 'missions' in status
|
|
890
1359
|
? status.missions
|
|
@@ -996,83 +1465,167 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
996
1465
|
throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
|
|
997
1466
|
};
|
|
998
1467
|
const commandParts = async (subcommand, ctx) => {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1468
|
+
const runPartSplit = async (source) => {
|
|
1469
|
+
let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
|
|
1470
|
+
if (orderId != null) {
|
|
1471
|
+
orderId = assertFlagHasValue(orderId, '--order-id <id>');
|
|
1472
|
+
}
|
|
1473
|
+
let analyzed;
|
|
1474
|
+
let status;
|
|
1475
|
+
if (orderId == null) {
|
|
1476
|
+
const result = await ctx.client.v1.analyzeProduction({
|
|
1477
|
+
...(source ?? (await resolveProductionAnalyzeSource(ctx))),
|
|
1478
|
+
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1479
|
+
name: getFlag(ctx.flags, 'name'),
|
|
1480
|
+
});
|
|
1481
|
+
analyzed = result;
|
|
1482
|
+
orderId = result.orderId;
|
|
1483
|
+
if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1484
|
+
status = await pollProductionStatus({
|
|
1485
|
+
ctx,
|
|
1486
|
+
orderId,
|
|
1487
|
+
terminalStatuses: productionAnalyzeTerminalStatuses,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1014
1492
|
status = await pollProductionStatus({
|
|
1015
1493
|
ctx,
|
|
1016
1494
|
orderId,
|
|
1017
1495
|
terminalStatuses: productionAnalyzeTerminalStatuses,
|
|
1018
1496
|
});
|
|
1019
1497
|
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1498
|
+
let taskIds = getFlagValues(ctx.flags, 'task-id');
|
|
1499
|
+
if (hasFlag(ctx.flags, 'all-ready')) {
|
|
1500
|
+
if (status == null) {
|
|
1501
|
+
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1502
|
+
}
|
|
1503
|
+
taskIds = readyTaskIdsFromStatus(status);
|
|
1504
|
+
if (taskIds.length === 0) {
|
|
1505
|
+
throw new Error('No ready tasks were found for --all-ready.');
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
let executed;
|
|
1509
|
+
let executionStatus;
|
|
1510
|
+
if (taskIds.length > 0) {
|
|
1511
|
+
if (status == null) {
|
|
1512
|
+
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1513
|
+
}
|
|
1514
|
+
const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
|
|
1515
|
+
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
1516
|
+
const agentVersion = partExtractorFlag == null
|
|
1517
|
+
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1518
|
+
: resolvePartExtractorPublicName(partExtractorFlag);
|
|
1519
|
+
const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
|
|
1520
|
+
if (partExtractionMode != null &&
|
|
1521
|
+
partExtractionMode !== 'fast' &&
|
|
1522
|
+
partExtractionMode !== 'high_quality') {
|
|
1523
|
+
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
1524
|
+
}
|
|
1525
|
+
executed = await ctx.client.v1.executeProduction({
|
|
1526
|
+
orderId,
|
|
1527
|
+
missionId,
|
|
1528
|
+
confirmedTaskIds: taskIds,
|
|
1529
|
+
agentVersion,
|
|
1530
|
+
partExtractionMode,
|
|
1531
|
+
});
|
|
1532
|
+
if (hasFlag(ctx.flags, 'wait')) {
|
|
1533
|
+
executionStatus = await pollProductionStatus({
|
|
1534
|
+
ctx,
|
|
1535
|
+
orderId,
|
|
1536
|
+
terminalStatuses: productionExecuteTerminalStatuses,
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return sanitizeProductionOutput({
|
|
1541
|
+
analyzed,
|
|
1542
|
+
status,
|
|
1543
|
+
executed,
|
|
1544
|
+
executionStatus,
|
|
1026
1545
|
});
|
|
1546
|
+
};
|
|
1547
|
+
if (subcommand === 'split') {
|
|
1548
|
+
if (getFlag(ctx.flags, 'order-id') != null && sourceInputCount(ctx.flags) > 0) {
|
|
1549
|
+
throw new Error('Pass either --order-id or one image source, not both.');
|
|
1550
|
+
}
|
|
1551
|
+
print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
|
|
1552
|
+
return;
|
|
1027
1553
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1554
|
+
if (subcommand === 'compare') {
|
|
1555
|
+
if (getFlag(ctx.flags, 'order-id') != null) {
|
|
1556
|
+
throw new Error('parts compare requires an image source. Use --file, --source-url, --source-id, --image-url, --file-ref-json, --stdin, --stdin-base64, --stdin-data-uri, --stdin-json, --source-json, --data-uri, or --clipboard.');
|
|
1032
1557
|
}
|
|
1033
|
-
|
|
1034
|
-
if (
|
|
1035
|
-
throw new Error('
|
|
1558
|
+
const preprocessPrompt = getFlag(ctx.flags, 'preprocess-prompt');
|
|
1559
|
+
if (preprocessPrompt === 'true') {
|
|
1560
|
+
throw new Error('Missing required flag value: --preprocess-prompt <text>');
|
|
1036
1561
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
let executionStatus;
|
|
1040
|
-
if (taskIds.length > 0) {
|
|
1041
|
-
if (status == null) {
|
|
1042
|
-
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1562
|
+
if (preprocessPrompt != null && getFlagValues(ctx.flags, 'task-id').length > 0) {
|
|
1563
|
+
throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
|
|
1043
1564
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const agentVersion = partExtractorFlag == null
|
|
1047
|
-
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1048
|
-
: resolvePartExtractorPublicName(partExtractorFlag);
|
|
1049
|
-
const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
|
|
1050
|
-
if (partExtractionMode != null &&
|
|
1051
|
-
partExtractionMode !== 'fast' &&
|
|
1052
|
-
partExtractionMode !== 'high_quality') {
|
|
1053
|
-
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
1565
|
+
if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
|
|
1566
|
+
throw new Error('parts compare preprocessing creates a second order. Omit --mission-id and let the CLI infer it.');
|
|
1054
1567
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
confirmedTaskIds: taskIds,
|
|
1059
|
-
agentVersion,
|
|
1060
|
-
partExtractionMode,
|
|
1061
|
-
});
|
|
1062
|
-
if (hasFlag(ctx.flags, 'wait')) {
|
|
1063
|
-
executionStatus = await pollProductionStatus({
|
|
1064
|
-
ctx,
|
|
1065
|
-
orderId,
|
|
1066
|
-
terminalStatuses: productionExecuteTerminalStatuses,
|
|
1067
|
-
});
|
|
1568
|
+
const preprocessModelId = getFlag(ctx.flags, 'preprocess-model-id');
|
|
1569
|
+
if (preprocessModelId === 'true') {
|
|
1570
|
+
throw new Error('Missing required flag value: --preprocess-model-id <id>');
|
|
1068
1571
|
}
|
|
1572
|
+
const inputSource = productionAnalyzeSourceFromSource(await requireSource({
|
|
1573
|
+
flags: ctx.flags,
|
|
1574
|
+
client: ctx.client,
|
|
1575
|
+
fileMediaType: 'image',
|
|
1576
|
+
}));
|
|
1577
|
+
const direct = await runPartSplit(inputSource);
|
|
1578
|
+
let preprocessed = null;
|
|
1579
|
+
if (preprocessPrompt != null && preprocessPrompt !== 'true') {
|
|
1580
|
+
try {
|
|
1581
|
+
const queued = await ctx.client.v2.generateImage({
|
|
1582
|
+
source: sourceFromProductionAnalyzeSource(inputSource),
|
|
1583
|
+
prompt: preprocessPrompt,
|
|
1584
|
+
modelId: preprocessModelId ?? defaultImageModelId,
|
|
1585
|
+
batchSize: 1,
|
|
1586
|
+
negativePrompt: getFlag(ctx.flags, 'preprocess-negative-prompt'),
|
|
1587
|
+
seed: getFlag(ctx.flags, 'preprocess-seed') == null
|
|
1588
|
+
? undefined
|
|
1589
|
+
: parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
|
|
1590
|
+
});
|
|
1591
|
+
const job = await ctx.client.v2.pollJob(queued.jobId, {
|
|
1592
|
+
intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
|
|
1593
|
+
timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
|
|
1594
|
+
onPoll: current => {
|
|
1595
|
+
stderr.write(`[poll] preprocess job=${current.id} status=${current.status}\n`);
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
1598
|
+
if (job.status !== 'completed') {
|
|
1599
|
+
throw new Error(`Preprocessing failed: job=${job.id} status=${job.status}`);
|
|
1600
|
+
}
|
|
1601
|
+
const generatedSource = imageSourceFromCompletedJob(job);
|
|
1602
|
+
preprocessed = {
|
|
1603
|
+
ok: true,
|
|
1604
|
+
image: {
|
|
1605
|
+
queued,
|
|
1606
|
+
job,
|
|
1607
|
+
source: generatedSource,
|
|
1608
|
+
},
|
|
1609
|
+
split: await runPartSplit(generatedSource),
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
catch (error) {
|
|
1613
|
+
if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
|
|
1614
|
+
throw error;
|
|
1615
|
+
}
|
|
1616
|
+
preprocessed = {
|
|
1617
|
+
ok: false,
|
|
1618
|
+
error: errorToOutput(error),
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
print(await withOptionalDownloads(ctx.flags, {
|
|
1623
|
+
direct,
|
|
1624
|
+
preprocessed,
|
|
1625
|
+
}));
|
|
1626
|
+
return;
|
|
1069
1627
|
}
|
|
1070
|
-
|
|
1071
|
-
analyzed,
|
|
1072
|
-
status,
|
|
1073
|
-
executed,
|
|
1074
|
-
executionStatus,
|
|
1075
|
-
})));
|
|
1628
|
+
throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
|
|
1076
1629
|
};
|
|
1077
1630
|
const run = async () => {
|
|
1078
1631
|
const parsed = parseArgs(argv.slice(2));
|