@assethub/cli 0.1.0 → 0.1.2
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 +28 -4
- package/dist/index.js +563 -28
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -8,10 +8,11 @@ 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
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
Configuration:
|
|
@@ -24,12 +25,35 @@ Agent-friendly login:
|
|
|
24
25
|
|
|
25
26
|
```sh
|
|
26
27
|
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 --
|
|
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 --model-id meshGen.hunyuan31 --wait --download --out-dir ./out/mesh
|
|
30
|
+
cat ./character.png | assethub parts split --stdin --file-name character.png --part-extractor "V1.5" --wait --all-ready --download --out-dir ./out/parts
|
|
29
31
|
```
|
|
30
32
|
|
|
33
|
+
Agent-oriented input:
|
|
34
|
+
|
|
35
|
+
- `--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 an
|
|
37
|
+
extension when possible.
|
|
38
|
+
- `--data-uri <uri>` accepts `data:image/png;base64,...` style inputs and uploads
|
|
39
|
+
them before running the target command.
|
|
40
|
+
- `--source-url`, `--source-id`/`--source-resource-id`, and `--file-ref-json`
|
|
41
|
+
can be reused across image, mesh, and part extraction commands where the API
|
|
42
|
+
supports them.
|
|
43
|
+
|
|
44
|
+
Agent-oriented output:
|
|
45
|
+
|
|
46
|
+
- stdout is one final JSON object.
|
|
47
|
+
- polling progress goes to stderr.
|
|
48
|
+
- `--download --out-dir <dir>` recursively downloads public artifact URLs from
|
|
49
|
+
job or production status payloads and writes a `manifest.json`.
|
|
50
|
+
|
|
31
51
|
Image generation defaults to the deployed `v1` endpoint. Pass `--api-version v2`
|
|
32
52
|
only when the `v2` image endpoint is available in the target environment.
|
|
33
53
|
|
|
54
|
+
Part extraction uses the same public names shown in the AssetHub product:
|
|
55
|
+
`V1`, `V1.5 alpha`, `V1.5`, `V2.0 alpha`, and `V2.1 alpha`. Internal agent
|
|
56
|
+
IDs are intentionally not part of the public CLI interface.
|
|
57
|
+
|
|
34
58
|
The CLI only uses AssetHub's public API surface and is designed so this package
|
|
35
59
|
can be published independently from the private application monorepo.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,52 @@
|
|
|
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';
|
|
8
8
|
const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
|
|
9
9
|
const defaultMeshModelId = 'meshGen.hunyuan31';
|
|
10
10
|
const defaultProfileName = 'default';
|
|
11
|
+
const defaultPartExtractorName = 'V1.5';
|
|
12
|
+
const partExtractorOptions = [
|
|
13
|
+
{
|
|
14
|
+
publicName: 'Part Extractor V3',
|
|
15
|
+
aliases: ['part extractor v3', 'v3'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
publicName: 'V1',
|
|
19
|
+
aliases: ['v1', '1'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
publicName: 'V1.5 alpha',
|
|
23
|
+
aliases: ['v1.5 alpha', 'v1.5-alpha', '1.5 alpha', '1.5-alpha'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
publicName: 'V1.5',
|
|
27
|
+
aliases: ['v1.5', '1.5', 'latest', 'default'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
publicName: 'V2.0 alpha',
|
|
31
|
+
aliases: ['v2.0 alpha', 'v2 alpha', 'v2.0-alpha', 'v2-alpha', '2.0 alpha'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
publicName: 'V2.1 alpha',
|
|
35
|
+
aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const partExtractorPublicNames = partExtractorOptions
|
|
39
|
+
.map(option => option.publicName)
|
|
40
|
+
.join(', ');
|
|
41
|
+
const normalizePartExtractorName = (value) => value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
42
|
+
const partExtractorPublicNameByInput = new Map(partExtractorOptions.flatMap(option => [
|
|
43
|
+
[normalizePartExtractorName(option.publicName), option.publicName],
|
|
44
|
+
...option.aliases.map(alias => [
|
|
45
|
+
normalizePartExtractorName(alias),
|
|
46
|
+
option.publicName,
|
|
47
|
+
]),
|
|
48
|
+
]));
|
|
49
|
+
const isInternalPartExtractorId = (value) => value.trim().includes('_');
|
|
11
50
|
const usage = `AssetHub CLI
|
|
12
51
|
|
|
13
52
|
Usage:
|
|
@@ -19,15 +58,17 @@ Usage:
|
|
|
19
58
|
assethub models get <model-id>
|
|
20
59
|
assethub files import <url> --media-type image|mesh
|
|
21
60
|
assethub files upload <file> --media-type image|mesh
|
|
22
|
-
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]
|
|
23
63
|
assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
|
|
24
|
-
assethub mesh generate (--file <path> | --source-url <url> | --source-resource-id <id> | --file-ref-json <json>) [--model-id <id>] [--name <name>] [--wait]
|
|
25
|
-
assethub mesh retopo (--source-resource-id <id> | --source-url <url> | --file-ref-json <json>) --model-id <id> [--wait]
|
|
26
|
-
assethub jobs get <job-id>
|
|
27
|
-
assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>]
|
|
28
|
-
assethub production analyze --
|
|
29
|
-
assethub production status <order-id>
|
|
30
|
-
assethub production execute --order-id <id> --mission-id <id> --task-id <id> [--task-id <id>...] --
|
|
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>]
|
|
31
72
|
|
|
32
73
|
Global options:
|
|
33
74
|
--api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
|
|
@@ -41,8 +82,12 @@ Examples:
|
|
|
41
82
|
assethub auth login --api-key-stdin
|
|
42
83
|
assethub models list --domain meshGen --ids-only
|
|
43
84
|
assethub files upload ./input.png --media-type image
|
|
85
|
+
assethub source create --file ./input.png --media-type image
|
|
86
|
+
cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
|
|
44
87
|
assethub image generate --prompt "stylized prop concept" --wait
|
|
45
88
|
assethub mesh generate --file ./input.png --model-id meshGen.hunyuan31 --wait
|
|
89
|
+
assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
|
|
90
|
+
assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
|
|
46
91
|
`;
|
|
47
92
|
const print = (value) => {
|
|
48
93
|
stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -112,6 +157,40 @@ const requireFlag = (flags, name) => {
|
|
|
112
157
|
}
|
|
113
158
|
return value;
|
|
114
159
|
};
|
|
160
|
+
const getPartExtractorFlag = (flags) => getFlag(flags, 'part-extractor') ??
|
|
161
|
+
getFlag(flags, 'part-extractor-version') ??
|
|
162
|
+
getFlag(flags, 'agent-version');
|
|
163
|
+
const resolvePartExtractorPublicName = (value) => {
|
|
164
|
+
if (isInternalPartExtractorId(value)) {
|
|
165
|
+
throw new Error(`Internal part extractor IDs are not accepted by the public CLI. Use one of: ${partExtractorPublicNames}.`);
|
|
166
|
+
}
|
|
167
|
+
const publicName = partExtractorPublicNameByInput.get(normalizePartExtractorName(value));
|
|
168
|
+
if (publicName == null) {
|
|
169
|
+
throw new Error(`Unknown part extractor: ${value}. Use one of: ${partExtractorPublicNames}.`);
|
|
170
|
+
}
|
|
171
|
+
return publicName;
|
|
172
|
+
};
|
|
173
|
+
const resolvePartExtractorForAnalyze = (flags) => resolvePartExtractorPublicName(getPartExtractorFlag(flags) ?? defaultPartExtractorName);
|
|
174
|
+
const publicPartExtractorName = (value) => {
|
|
175
|
+
if (value == null) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return (isInternalPartExtractorId(value) ? 'Unknown' : value);
|
|
179
|
+
};
|
|
180
|
+
const sanitizeProductionOutput = (value) => {
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return value.map(item => sanitizeProductionOutput(item));
|
|
183
|
+
}
|
|
184
|
+
if (value == null || typeof value !== 'object') {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
188
|
+
key,
|
|
189
|
+
key === 'agentVersion' && typeof entry === 'string'
|
|
190
|
+
? publicPartExtractorName(entry)
|
|
191
|
+
: sanitizeProductionOutput(entry),
|
|
192
|
+
]));
|
|
193
|
+
};
|
|
115
194
|
const parsePositiveIntegerFlag = (flags, name, fallback) => {
|
|
116
195
|
const raw = getFlag(flags, name);
|
|
117
196
|
if (raw == null) {
|
|
@@ -142,6 +221,10 @@ const parseMediaType = (value) => {
|
|
|
142
221
|
}
|
|
143
222
|
throw new Error('--media-type must be "image" or "mesh"');
|
|
144
223
|
};
|
|
224
|
+
const defaultFileNameForMediaType = (mediaType) => mediaType === 'image' ? 'stdin.png' : 'stdin.glb';
|
|
225
|
+
const getSourceResourceIdFlag = (flags) => getFlag(flags, 'source-resource-id') ??
|
|
226
|
+
getFlag(flags, 'source-id') ??
|
|
227
|
+
getFlag(flags, 'resource-id');
|
|
145
228
|
const getProfileName = (flags) => getFlag(flags, 'profile') ?? defaultProfileName;
|
|
146
229
|
const getConfigPath = (flags) => getFlag(flags, 'config') ??
|
|
147
230
|
env.ASSETHUB_CLI_CONFIG ??
|
|
@@ -183,6 +266,18 @@ const readStdin = async () => await new Promise((resolve, reject) => {
|
|
|
183
266
|
stdin.on('end', () => resolve(value));
|
|
184
267
|
stdin.on('error', reject);
|
|
185
268
|
});
|
|
269
|
+
const readStdinBlob = async (type) => await new Promise((resolve, reject) => {
|
|
270
|
+
const chunks = [];
|
|
271
|
+
stdin.on('data', chunk => {
|
|
272
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
273
|
+
});
|
|
274
|
+
stdin.on('end', () => {
|
|
275
|
+
const buffer = Buffer.concat(chunks);
|
|
276
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
277
|
+
resolve(new Blob([arrayBuffer], type == null ? undefined : { type }));
|
|
278
|
+
});
|
|
279
|
+
stdin.on('error', reject);
|
|
280
|
+
});
|
|
186
281
|
const resolveAuth = async (flags) => {
|
|
187
282
|
const profile = getProfileName(flags);
|
|
188
283
|
const baseUrlFromFlags = getFlag(flags, 'base-url');
|
|
@@ -241,8 +336,46 @@ const readBlob = async (filePath) => {
|
|
|
241
336
|
const buffer = await readFile(filePath);
|
|
242
337
|
return new Blob([new Uint8Array(buffer)]);
|
|
243
338
|
};
|
|
339
|
+
const blobFromDataUri = (value) => {
|
|
340
|
+
const match = value.match(/^data:([^;,]+)?(;base64)?,(.*)$/s);
|
|
341
|
+
if (match == null) {
|
|
342
|
+
throw new Error('--data-uri must be a valid data URI.');
|
|
343
|
+
}
|
|
344
|
+
const contentType = match[1] === '' ? undefined : match[1];
|
|
345
|
+
const isBase64 = match[2] != null;
|
|
346
|
+
const payload = match[3] ?? '';
|
|
347
|
+
const buffer = isBase64
|
|
348
|
+
? Buffer.from(payload, 'base64')
|
|
349
|
+
: Buffer.from(decodeURIComponent(payload), 'utf8');
|
|
350
|
+
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
351
|
+
return {
|
|
352
|
+
blob: new Blob([arrayBuffer], contentType == null ? undefined : { type: contentType }),
|
|
353
|
+
contentType,
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
const extensionFromMimeType = (mimeType, mediaType) => {
|
|
357
|
+
if (mimeType === 'image/png')
|
|
358
|
+
return 'png';
|
|
359
|
+
if (mimeType === 'image/jpeg')
|
|
360
|
+
return 'jpg';
|
|
361
|
+
if (mimeType === 'image/webp')
|
|
362
|
+
return 'webp';
|
|
363
|
+
if (mimeType === 'image/gif')
|
|
364
|
+
return 'gif';
|
|
365
|
+
if (mimeType === 'model/gltf-binary')
|
|
366
|
+
return 'glb';
|
|
367
|
+
if (mimeType === 'model/gltf+json')
|
|
368
|
+
return 'gltf';
|
|
369
|
+
return mediaType === 'image' ? 'png' : 'glb';
|
|
370
|
+
};
|
|
371
|
+
const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
372
|
+
if (extname(fileName) !== '') {
|
|
373
|
+
return fileName;
|
|
374
|
+
}
|
|
375
|
+
return `${fileName}.${extensionFromMimeType(mimeType, mediaType)}`;
|
|
376
|
+
};
|
|
244
377
|
const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
245
|
-
const sourceResourceId =
|
|
378
|
+
const sourceResourceId = getSourceResourceIdFlag(flags);
|
|
246
379
|
if (sourceResourceId != null) {
|
|
247
380
|
return { resourceId: sourceResourceId };
|
|
248
381
|
}
|
|
@@ -263,12 +396,32 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
263
396
|
});
|
|
264
397
|
return { fileRef: fileRef.fileRef };
|
|
265
398
|
}
|
|
399
|
+
const dataUri = getFlag(flags, 'data-uri');
|
|
400
|
+
if (dataUri != null) {
|
|
401
|
+
const { blob, contentType } = blobFromDataUri(dataUri);
|
|
402
|
+
const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? 'data-uri', fileMediaType, contentType);
|
|
403
|
+
const fileRef = await client.v2.uploadFile({
|
|
404
|
+
file: blob,
|
|
405
|
+
fileName,
|
|
406
|
+
mediaType: fileMediaType,
|
|
407
|
+
});
|
|
408
|
+
return { fileRef: fileRef.fileRef };
|
|
409
|
+
}
|
|
410
|
+
if (hasFlag(flags, 'stdin')) {
|
|
411
|
+
const fileName = ensureFileNameExtension(getFlag(flags, 'file-name') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType);
|
|
412
|
+
const fileRef = await client.v2.uploadFile({
|
|
413
|
+
file: await readStdinBlob(),
|
|
414
|
+
fileName,
|
|
415
|
+
mediaType: fileMediaType,
|
|
416
|
+
});
|
|
417
|
+
return { fileRef: fileRef.fileRef };
|
|
418
|
+
}
|
|
266
419
|
return undefined;
|
|
267
420
|
};
|
|
268
421
|
const requireSource = async (options) => {
|
|
269
422
|
const source = await resolveSource(options);
|
|
270
423
|
if (source == null) {
|
|
271
|
-
throw new Error('Missing source. Pass --file, --source-url, --source-resource-id,
|
|
424
|
+
throw new Error('Missing source. Pass --file, --source-url, --source-resource-id, --source-id, --file-ref-json, --stdin, or --data-uri.');
|
|
272
425
|
}
|
|
273
426
|
return source;
|
|
274
427
|
};
|
|
@@ -284,6 +437,44 @@ const sourceToV1ImageInput = (source) => {
|
|
|
284
437
|
}
|
|
285
438
|
throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
|
|
286
439
|
};
|
|
440
|
+
const resolveProductionAnalyzeSource = async (ctx) => {
|
|
441
|
+
const imageUrl = getFlag(ctx.flags, 'image-url');
|
|
442
|
+
if (imageUrl != null) {
|
|
443
|
+
return { imageUrl };
|
|
444
|
+
}
|
|
445
|
+
const source = await requireSource({
|
|
446
|
+
flags: ctx.flags,
|
|
447
|
+
client: ctx.client,
|
|
448
|
+
fileMediaType: 'image',
|
|
449
|
+
});
|
|
450
|
+
if (source.resourceId != null) {
|
|
451
|
+
return { imageAssetId: source.resourceId };
|
|
452
|
+
}
|
|
453
|
+
if (source.url != null) {
|
|
454
|
+
return { imageUrl: source.url };
|
|
455
|
+
}
|
|
456
|
+
if (source.fileRef == null) {
|
|
457
|
+
throw new Error('Missing source. Pass --file, --source-url, --source-id, --file-ref-json, --stdin, or --data-uri.');
|
|
458
|
+
}
|
|
459
|
+
return { imageBlobLocation: source.fileRef };
|
|
460
|
+
};
|
|
461
|
+
const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
|
|
462
|
+
const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
|
|
463
|
+
const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
|
|
464
|
+
const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
|
|
465
|
+
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
|
|
466
|
+
const startedAt = Date.now();
|
|
467
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
468
|
+
const status = await ctx.client.v1.getProductionStatus(orderId);
|
|
469
|
+
const orderStatus = status.order.status;
|
|
470
|
+
stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
|
|
471
|
+
if (terminalStatuses.has(orderStatus)) {
|
|
472
|
+
return status;
|
|
473
|
+
}
|
|
474
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Polling timeout: order=${orderId} did not reach terminal state within ${timeoutMs}ms`);
|
|
477
|
+
};
|
|
287
478
|
const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
|
|
288
479
|
if (!hasFlag(ctx.flags, 'wait')) {
|
|
289
480
|
return undefined;
|
|
@@ -306,6 +497,60 @@ const requirePositional = (positionals, index, label) => {
|
|
|
306
497
|
}
|
|
307
498
|
return value;
|
|
308
499
|
};
|
|
500
|
+
const commandSource = async (subcommand, ctx) => {
|
|
501
|
+
if (subcommand !== 'create') {
|
|
502
|
+
throw new Error('Unknown source command. Use "source create".');
|
|
503
|
+
}
|
|
504
|
+
const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
|
|
505
|
+
const sourceUrl = getFlag(ctx.flags, 'source-url') ?? getFlag(ctx.flags, 'image-url');
|
|
506
|
+
const sourceResourceId = getSourceResourceIdFlag(ctx.flags);
|
|
507
|
+
const filePath = getFlag(ctx.flags, 'file');
|
|
508
|
+
const useStdin = hasFlag(ctx.flags, 'stdin');
|
|
509
|
+
const dataUri = getFlag(ctx.flags, 'data-uri');
|
|
510
|
+
const provided = [
|
|
511
|
+
sourceUrl != null,
|
|
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 });
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (sourceResourceId != null) {
|
|
525
|
+
print({
|
|
526
|
+
source: { resourceId: sourceResourceId },
|
|
527
|
+
resourceId: sourceResourceId,
|
|
528
|
+
mediaType,
|
|
529
|
+
});
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const upload = filePath != null
|
|
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 });
|
|
553
|
+
};
|
|
309
554
|
const commandModels = async (subcommand, positionals, ctx) => {
|
|
310
555
|
if (subcommand === 'list') {
|
|
311
556
|
const domain = getFlag(ctx.flags, 'domain');
|
|
@@ -451,7 +696,7 @@ const commandImage = async (subcommand, ctx) => {
|
|
|
451
696
|
source,
|
|
452
697
|
});
|
|
453
698
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId, apiVersion });
|
|
454
|
-
print({ queued, job, apiVersion });
|
|
699
|
+
print(await withOptionalDownloads(ctx.flags, { queued, job, apiVersion }));
|
|
455
700
|
};
|
|
456
701
|
const commandMesh = async (subcommand, ctx) => {
|
|
457
702
|
if (subcommand === 'generate') {
|
|
@@ -469,7 +714,7 @@ const commandMesh = async (subcommand, ctx) => {
|
|
|
469
714
|
isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
|
|
470
715
|
});
|
|
471
716
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
472
|
-
print({ queued, job });
|
|
717
|
+
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
473
718
|
return;
|
|
474
719
|
}
|
|
475
720
|
if (subcommand === 'retopo') {
|
|
@@ -485,7 +730,7 @@ const commandMesh = async (subcommand, ctx) => {
|
|
|
485
730
|
polygonType: getFlag(ctx.flags, 'polygon-type'),
|
|
486
731
|
});
|
|
487
732
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
488
|
-
print({ queued, job });
|
|
733
|
+
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
489
734
|
return;
|
|
490
735
|
}
|
|
491
736
|
throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
|
|
@@ -493,7 +738,8 @@ const commandMesh = async (subcommand, ctx) => {
|
|
|
493
738
|
const commandJobs = async (subcommand, positionals, ctx) => {
|
|
494
739
|
const jobId = requirePositional(positionals, 2, 'job-id');
|
|
495
740
|
if (subcommand === 'get') {
|
|
496
|
-
|
|
741
|
+
const job = await ctx.client.v2.getJob(jobId);
|
|
742
|
+
print(await withOptionalDownloads(ctx.flags, { job }));
|
|
497
743
|
return;
|
|
498
744
|
}
|
|
499
745
|
if (subcommand === 'watch') {
|
|
@@ -504,23 +750,213 @@ const commandJobs = async (subcommand, positionals, ctx) => {
|
|
|
504
750
|
stderr.write(`[poll] job=${current.id} status=${current.status}\n`);
|
|
505
751
|
},
|
|
506
752
|
});
|
|
507
|
-
print({ job });
|
|
753
|
+
print(await withOptionalDownloads(ctx.flags, { job }));
|
|
508
754
|
return;
|
|
509
755
|
}
|
|
510
756
|
throw new Error('Unknown jobs command. Use "jobs get" or "jobs watch".');
|
|
511
757
|
};
|
|
758
|
+
const resolvePartExtractorFromMission = async (ctx, orderId, missionId) => {
|
|
759
|
+
const status = await ctx.client.v1.getProductionStatus(orderId);
|
|
760
|
+
const mission = status.missions.find(item => item.id === missionId);
|
|
761
|
+
if (mission?.agentVersion == null || mission.agentVersion.trim() === '') {
|
|
762
|
+
throw new Error('Could not infer the part extractor from the mission. Pass --part-extractor with one of: ' +
|
|
763
|
+
partExtractorPublicNames);
|
|
764
|
+
}
|
|
765
|
+
if (isInternalPartExtractorId(mission.agentVersion)) {
|
|
766
|
+
throw new Error('Could not infer the public part extractor from the mission. Pass --part-extractor with one of: ' +
|
|
767
|
+
partExtractorPublicNames);
|
|
768
|
+
}
|
|
769
|
+
return resolvePartExtractorPublicName(mission.agentVersion);
|
|
770
|
+
};
|
|
771
|
+
const redactUrl = (value) => {
|
|
772
|
+
try {
|
|
773
|
+
const url = new URL(value);
|
|
774
|
+
url.search = '';
|
|
775
|
+
url.hash = '';
|
|
776
|
+
return url.toString();
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return value;
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
const collectHttpUrls = (value, urls = new Set()) => {
|
|
783
|
+
if (typeof value === 'string') {
|
|
784
|
+
try {
|
|
785
|
+
const url = new URL(value);
|
|
786
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
787
|
+
urls.add(value);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
// Ignore non-URL strings.
|
|
792
|
+
}
|
|
793
|
+
return urls;
|
|
794
|
+
}
|
|
795
|
+
if (Array.isArray(value)) {
|
|
796
|
+
for (const item of value) {
|
|
797
|
+
collectHttpUrls(item, urls);
|
|
798
|
+
}
|
|
799
|
+
return urls;
|
|
800
|
+
}
|
|
801
|
+
if (value != null && typeof value === 'object') {
|
|
802
|
+
for (const item of Object.values(value)) {
|
|
803
|
+
collectHttpUrls(item, urls);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return urls;
|
|
807
|
+
};
|
|
808
|
+
const extensionFromContentType = (contentType) => {
|
|
809
|
+
if (contentType == null) {
|
|
810
|
+
return '';
|
|
811
|
+
}
|
|
812
|
+
const clean = contentType.split(';')[0]?.trim().toLowerCase();
|
|
813
|
+
switch (clean) {
|
|
814
|
+
case 'image/png':
|
|
815
|
+
return '.png';
|
|
816
|
+
case 'image/jpeg':
|
|
817
|
+
return '.jpg';
|
|
818
|
+
case 'image/webp':
|
|
819
|
+
return '.webp';
|
|
820
|
+
case 'image/gif':
|
|
821
|
+
return '.gif';
|
|
822
|
+
case 'model/gltf-binary':
|
|
823
|
+
return '.glb';
|
|
824
|
+
case 'model/gltf+json':
|
|
825
|
+
return '.gltf';
|
|
826
|
+
case 'application/octet-stream':
|
|
827
|
+
default:
|
|
828
|
+
return '';
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const safeDownloadFileName = (value, index, contentType) => {
|
|
832
|
+
let name = `asset-${index + 1}`;
|
|
833
|
+
try {
|
|
834
|
+
const url = new URL(value);
|
|
835
|
+
const fromPath = basename(url.pathname);
|
|
836
|
+
if (fromPath !== '' && fromPath !== '/' && fromPath !== '.') {
|
|
837
|
+
name = fromPath;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// Keep fallback name.
|
|
842
|
+
}
|
|
843
|
+
name = name.replace(/[^A-Za-z0-9._-]/g, '_').replace(/^_+/, '');
|
|
844
|
+
if (name === '' || name === '.' || name === '..') {
|
|
845
|
+
name = `asset-${index + 1}`;
|
|
846
|
+
}
|
|
847
|
+
if (extname(name) === '') {
|
|
848
|
+
name = `${name}${extensionFromContentType(contentType)}`;
|
|
849
|
+
}
|
|
850
|
+
return `${String(index + 1).padStart(3, '0')}-${name}`;
|
|
851
|
+
};
|
|
852
|
+
const maybeDownloadUrls = async (flags, value) => {
|
|
853
|
+
if (!hasFlag(flags, 'download')) {
|
|
854
|
+
return undefined;
|
|
855
|
+
}
|
|
856
|
+
const outDir = getFlag(flags, 'out-dir') ?? getFlag(flags, 'out');
|
|
857
|
+
if (outDir == null || outDir === 'true') {
|
|
858
|
+
throw new Error('Missing download output directory. Pass --out-dir <dir> or --out <dir>.');
|
|
859
|
+
}
|
|
860
|
+
await mkdir(outDir, { recursive: true });
|
|
861
|
+
const urls = [...collectHttpUrls(value)];
|
|
862
|
+
const files = [];
|
|
863
|
+
for (const [index, url] of urls.entries()) {
|
|
864
|
+
const response = await fetch(url);
|
|
865
|
+
if (!response.ok) {
|
|
866
|
+
throw new Error(`Download failed: ${redactUrl(url)} returned HTTP ${response.status}`);
|
|
867
|
+
}
|
|
868
|
+
const contentType = response.headers.get('content-type');
|
|
869
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
870
|
+
const fileName = safeDownloadFileName(url, index, contentType);
|
|
871
|
+
const filePath = join(outDir, fileName);
|
|
872
|
+
await writeFile(filePath, bytes);
|
|
873
|
+
files.push({
|
|
874
|
+
url: redactUrl(url),
|
|
875
|
+
path: filePath,
|
|
876
|
+
contentType,
|
|
877
|
+
bytes: bytes.byteLength,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const manifest = { generatedAt: new Date().toISOString(), files };
|
|
881
|
+
await writeFile(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
882
|
+
return { outDir, files };
|
|
883
|
+
};
|
|
884
|
+
const withOptionalDownloads = async (flags, value) => {
|
|
885
|
+
const downloads = await maybeDownloadUrls(flags, value);
|
|
886
|
+
return downloads == null ? value : { ...value, downloads };
|
|
887
|
+
};
|
|
888
|
+
const readyTaskIdsFromStatus = (status) => {
|
|
889
|
+
const missions = status != null && typeof status === 'object' && 'missions' in status
|
|
890
|
+
? status.missions
|
|
891
|
+
: undefined;
|
|
892
|
+
if (!Array.isArray(missions)) {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
return missions.flatMap(mission => {
|
|
896
|
+
if (mission == null || typeof mission !== 'object' || !('tasks' in mission)) {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
const tasks = mission.tasks;
|
|
900
|
+
if (!Array.isArray(tasks)) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
return tasks.flatMap(task => {
|
|
904
|
+
if (task == null || typeof task !== 'object') {
|
|
905
|
+
return [];
|
|
906
|
+
}
|
|
907
|
+
const id = task.id;
|
|
908
|
+
const taskStatus = task.status;
|
|
909
|
+
return typeof id === 'string' &&
|
|
910
|
+
typeof taskStatus === 'string' &&
|
|
911
|
+
['ready', 'pending', 'pending_review'].includes(taskStatus)
|
|
912
|
+
? [id]
|
|
913
|
+
: [];
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
};
|
|
917
|
+
const singleMissionIdFromStatus = (status) => {
|
|
918
|
+
const missions = status != null && typeof status === 'object' && 'missions' in status
|
|
919
|
+
? status.missions
|
|
920
|
+
: undefined;
|
|
921
|
+
if (!Array.isArray(missions) || missions.length !== 1) {
|
|
922
|
+
throw new Error('Pass --mission-id when the production order does not have exactly one mission.');
|
|
923
|
+
}
|
|
924
|
+
const missionId = missions[0].id;
|
|
925
|
+
if (typeof missionId !== 'string' || missionId.trim() === '') {
|
|
926
|
+
throw new Error('Could not infer mission id. Pass --mission-id.');
|
|
927
|
+
}
|
|
928
|
+
return missionId;
|
|
929
|
+
};
|
|
512
930
|
const commandProduction = async (subcommand, positionals, ctx) => {
|
|
513
931
|
if (subcommand === 'analyze') {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
agentVersion:
|
|
932
|
+
const analyzed = await ctx.client.v1.analyzeProduction({
|
|
933
|
+
...(await resolveProductionAnalyzeSource(ctx)),
|
|
934
|
+
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
517
935
|
name: getFlag(ctx.flags, 'name'),
|
|
518
|
-
})
|
|
936
|
+
});
|
|
937
|
+
const status = hasFlag(ctx.flags, 'wait')
|
|
938
|
+
? await pollProductionStatus({
|
|
939
|
+
ctx,
|
|
940
|
+
orderId: analyzed.orderId,
|
|
941
|
+
terminalStatuses: productionAnalyzeTerminalStatuses,
|
|
942
|
+
})
|
|
943
|
+
: undefined;
|
|
944
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ analyzed, status })));
|
|
519
945
|
return;
|
|
520
946
|
}
|
|
521
947
|
if (subcommand === 'status') {
|
|
522
948
|
const orderId = requirePositional(positionals, 2, 'order-id');
|
|
523
|
-
|
|
949
|
+
const status = hasFlag(ctx.flags, 'wait')
|
|
950
|
+
? await pollProductionStatus({
|
|
951
|
+
ctx,
|
|
952
|
+
orderId,
|
|
953
|
+
terminalStatuses: new Set([
|
|
954
|
+
...productionAnalyzeTerminalStatuses,
|
|
955
|
+
...productionExecuteTerminalStatuses,
|
|
956
|
+
]),
|
|
957
|
+
})
|
|
958
|
+
: await ctx.client.v1.getProductionStatus(orderId);
|
|
959
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ status })));
|
|
524
960
|
return;
|
|
525
961
|
}
|
|
526
962
|
if (subcommand === 'execute') {
|
|
@@ -531,20 +967,113 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
531
967
|
const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
|
|
532
968
|
if (partExtractionMode != null &&
|
|
533
969
|
partExtractionMode !== 'fast' &&
|
|
534
|
-
partExtractionMode !== '
|
|
535
|
-
throw new Error('--part-extraction-mode must be "fast" or "
|
|
970
|
+
partExtractionMode !== 'high_quality') {
|
|
971
|
+
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
536
972
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
973
|
+
const orderId = requireFlag(ctx.flags, 'order-id');
|
|
974
|
+
const missionId = requireFlag(ctx.flags, 'mission-id');
|
|
975
|
+
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
976
|
+
const agentVersion = partExtractorFlag == null
|
|
977
|
+
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
978
|
+
: resolvePartExtractorPublicName(partExtractorFlag);
|
|
979
|
+
const executed = await ctx.client.v1.executeProduction({
|
|
980
|
+
orderId,
|
|
981
|
+
missionId,
|
|
540
982
|
confirmedTaskIds,
|
|
541
|
-
agentVersion
|
|
983
|
+
agentVersion,
|
|
542
984
|
partExtractionMode,
|
|
543
|
-
})
|
|
985
|
+
});
|
|
986
|
+
const status = hasFlag(ctx.flags, 'wait')
|
|
987
|
+
? await pollProductionStatus({
|
|
988
|
+
ctx,
|
|
989
|
+
orderId,
|
|
990
|
+
terminalStatuses: productionExecuteTerminalStatuses,
|
|
991
|
+
})
|
|
992
|
+
: undefined;
|
|
993
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ executed, status })));
|
|
544
994
|
return;
|
|
545
995
|
}
|
|
546
996
|
throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
|
|
547
997
|
};
|
|
998
|
+
const commandParts = async (subcommand, ctx) => {
|
|
999
|
+
if (subcommand !== 'split') {
|
|
1000
|
+
throw new Error('Unknown parts command. Use "parts split".');
|
|
1001
|
+
}
|
|
1002
|
+
let orderId = getFlag(ctx.flags, 'order-id');
|
|
1003
|
+
let analyzed;
|
|
1004
|
+
let status;
|
|
1005
|
+
if (orderId == null) {
|
|
1006
|
+
const result = await ctx.client.v1.analyzeProduction({
|
|
1007
|
+
...(await resolveProductionAnalyzeSource(ctx)),
|
|
1008
|
+
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1009
|
+
name: getFlag(ctx.flags, 'name'),
|
|
1010
|
+
});
|
|
1011
|
+
analyzed = result;
|
|
1012
|
+
orderId = result.orderId;
|
|
1013
|
+
if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1014
|
+
status = await pollProductionStatus({
|
|
1015
|
+
ctx,
|
|
1016
|
+
orderId,
|
|
1017
|
+
terminalStatuses: productionAnalyzeTerminalStatuses,
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
else if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1022
|
+
status = await pollProductionStatus({
|
|
1023
|
+
ctx,
|
|
1024
|
+
orderId,
|
|
1025
|
+
terminalStatuses: productionAnalyzeTerminalStatuses,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
let taskIds = getFlagValues(ctx.flags, 'task-id');
|
|
1029
|
+
if (hasFlag(ctx.flags, 'all-ready')) {
|
|
1030
|
+
if (status == null) {
|
|
1031
|
+
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1032
|
+
}
|
|
1033
|
+
taskIds = readyTaskIdsFromStatus(status);
|
|
1034
|
+
if (taskIds.length === 0) {
|
|
1035
|
+
throw new Error('No ready tasks were found for --all-ready.');
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
let executed;
|
|
1039
|
+
let executionStatus;
|
|
1040
|
+
if (taskIds.length > 0) {
|
|
1041
|
+
if (status == null) {
|
|
1042
|
+
status = await ctx.client.v1.getProductionStatus(orderId);
|
|
1043
|
+
}
|
|
1044
|
+
const missionId = getFlag(ctx.flags, 'mission-id') ?? singleMissionIdFromStatus(status);
|
|
1045
|
+
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
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"');
|
|
1054
|
+
}
|
|
1055
|
+
executed = await ctx.client.v1.executeProduction({
|
|
1056
|
+
orderId,
|
|
1057
|
+
missionId,
|
|
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
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({
|
|
1071
|
+
analyzed,
|
|
1072
|
+
status,
|
|
1073
|
+
executed,
|
|
1074
|
+
executionStatus,
|
|
1075
|
+
})));
|
|
1076
|
+
};
|
|
548
1077
|
const run = async () => {
|
|
549
1078
|
const parsed = parseArgs(argv.slice(2));
|
|
550
1079
|
if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
|
|
@@ -564,6 +1093,9 @@ const run = async () => {
|
|
|
564
1093
|
case 'files':
|
|
565
1094
|
await commandFiles(subcommand, parsed.positionals, ctx);
|
|
566
1095
|
return;
|
|
1096
|
+
case 'source':
|
|
1097
|
+
await commandSource(subcommand, ctx);
|
|
1098
|
+
return;
|
|
567
1099
|
case 'image':
|
|
568
1100
|
await commandImage(subcommand, ctx);
|
|
569
1101
|
return;
|
|
@@ -576,6 +1108,9 @@ const run = async () => {
|
|
|
576
1108
|
case 'production':
|
|
577
1109
|
await commandProduction(subcommand, parsed.positionals, ctx);
|
|
578
1110
|
return;
|
|
1111
|
+
case 'parts':
|
|
1112
|
+
await commandParts(subcommand, ctx);
|
|
1113
|
+
return;
|
|
579
1114
|
default:
|
|
580
1115
|
throw new Error(`Unknown command: ${command}`);
|
|
581
1116
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assethub/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
+
}
|