@assethub/cli 0.1.5 → 0.1.7
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 +349 -5
- package/dist/autopilot.d.ts +13 -0
- package/dist/autopilot.d.ts.map +1 -0
- package/dist/autopilot.js +158 -0
- package/dist/canvas.d.ts +27 -0
- package/dist/canvas.d.ts.map +1 -0
- package/dist/canvas.js +82 -0
- package/dist/canvasAssetImport.d.ts +45 -0
- package/dist/canvasAssetImport.d.ts.map +1 -0
- package/dist/canvasAssetImport.js +154 -0
- package/dist/canvasComparison.d.ts +21 -0
- package/dist/canvasComparison.d.ts.map +1 -0
- package/dist/canvasComparison.js +58 -0
- package/dist/execution.d.ts +175 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +234 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1641 -192
- package/dist/projectSources.d.ts +65 -0
- package/dist/projectSources.d.ts.map +1 -0
- package/dist/projectSources.js +369 -0
- package/dist/projectSourcesWorkbook.d.ts +3 -0
- package/dist/projectSourcesWorkbook.d.ts.map +1 -0
- package/dist/projectSourcesWorkbook.js +143 -0
- package/dist/runsUpload/artifactGraphModel.d.ts +48 -0
- package/dist/runsUpload/artifactGraphModel.d.ts.map +1 -0
- package/dist/runsUpload/artifactGraphModel.js +127 -0
- package/dist/runsUpload/blobRefs.d.ts +13 -0
- package/dist/runsUpload/blobRefs.d.ts.map +1 -0
- package/dist/runsUpload/blobRefs.js +73 -0
- package/dist/runsUpload/buildRunUpload.d.ts +63 -0
- package/dist/runsUpload/buildRunUpload.d.ts.map +1 -0
- package/dist/runsUpload/buildRunUpload.js +428 -0
- package/dist/runsUpload/canonicalJson.d.ts +5 -0
- package/dist/runsUpload/canonicalJson.d.ts.map +1 -0
- package/dist/runsUpload/canonicalJson.js +108 -0
- package/dist/runsUpload/controlEndpoints.d.ts +63 -0
- package/dist/runsUpload/controlEndpoints.d.ts.map +1 -0
- package/dist/runsUpload/controlEndpoints.js +64 -0
- package/dist/runsUpload/graphFolder.d.ts +25 -0
- package/dist/runsUpload/graphFolder.d.ts.map +1 -0
- package/dist/runsUpload/graphFolder.js +147 -0
- package/dist/runsUpload/runUploadError.d.ts +23 -0
- package/dist/runsUpload/runUploadError.d.ts.map +1 -0
- package/dist/runsUpload/runUploadError.js +24 -0
- package/dist/runsUpload/uploadRun.d.ts +43 -0
- package/dist/runsUpload/uploadRun.d.ts.map +1 -0
- package/dist/runsUpload/uploadRun.js +217 -0
- package/package.json +11 -8
package/dist/index.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
import { ingestProjectSources } from './projectSources.js';
|
|
4
|
+
import { writeCanvasComparison } from './canvasComparison.js';
|
|
5
|
+
import { importCanvasAssetWithState } from './canvasAssetImport.js';
|
|
6
|
+
import { execFile, spawn } from 'node:child_process';
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { realpathSync } from 'node:fs';
|
|
9
|
+
import { chmod, mkdir, readFile, rename, unlink, writeFile, } from 'node:fs/promises';
|
|
4
10
|
import { homedir, tmpdir } from 'node:os';
|
|
5
|
-
import { basename, dirname, extname, join } from 'node:path';
|
|
11
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
6
12
|
import { argv, env, exit, stdin, stderr, stdout } from 'node:process';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
7
14
|
import { promisify } from 'node:util';
|
|
8
15
|
import { AssetHubApiError, createAssetHubClient, } from '@assethub/api-client';
|
|
16
|
+
import { resolveCanvasSelection, saveCanvasSelection } from './canvas.js';
|
|
17
|
+
import { activeExecution, CliExecutionError, executeRecorded, hasRecordedOperation, openExecutionSession, resumeRecorded, waitForExecution, } from './execution.js';
|
|
18
|
+
import { buildRunUploadPlan, formatBytes } from './runsUpload/buildRunUpload.js';
|
|
19
|
+
import { readGraphFolder } from './runsUpload/graphFolder.js';
|
|
20
|
+
import { uploadRun } from './runsUpload/uploadRun.js';
|
|
9
21
|
const defaultBaseUrl = 'https://app.assethub.io';
|
|
10
|
-
|
|
22
|
+
// Mirrors PUBLIC_IMAGE_GENERATE_DEFAULT_MODEL_ID in apps/frontend
|
|
23
|
+
// (src/feature/imageGen/util/publicImageGenDefaultModel.ts). This package does
|
|
24
|
+
// not depend on the app, so the value is duplicated: keep the two in step so
|
|
25
|
+
// `assethub image generate` without --model matches the API's own default.
|
|
26
|
+
const defaultImageModelId = 'imageGen.nanoBanana2.openrouter';
|
|
11
27
|
const defaultMeshModelId = 'meshGen.hunyuan31';
|
|
12
28
|
const defaultProfileName = 'default';
|
|
13
29
|
const defaultPartExtractorName = 'V1.5';
|
|
30
|
+
const artifactGraphPartExtractorApiValue = 'ah_agent_graph_v3_6_1';
|
|
14
31
|
const partExtractorOptions = [
|
|
15
|
-
{
|
|
16
|
-
publicName: 'Part Extractor V3',
|
|
17
|
-
apiValue: 'ah_part_extractor_v3',
|
|
18
|
-
aliases: ['part extractor v3', 'v3'],
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
publicName: 'V1',
|
|
22
|
-
apiValue: 'ah_part_extractor_v5',
|
|
23
|
-
aliases: ['v1', '1'],
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
publicName: 'V1.5 alpha',
|
|
27
|
-
apiValue: 'ah_part_extractor_v55',
|
|
28
|
-
aliases: ['v1.5 alpha', 'v1.5-alpha', '1.5 alpha', '1.5-alpha'],
|
|
29
|
-
},
|
|
30
32
|
{
|
|
31
33
|
publicName: 'V1.5',
|
|
32
34
|
apiValue: 'ah_part_extractor_v65',
|
|
@@ -42,6 +44,11 @@ const partExtractorOptions = [
|
|
|
42
44
|
apiValue: 'ah_part_extractor_v21_alpha',
|
|
43
45
|
aliases: ['v2.1 alpha', 'v2.1-alpha', '2.1 alpha'],
|
|
44
46
|
},
|
|
47
|
+
{
|
|
48
|
+
publicName: 'V3.6.1',
|
|
49
|
+
apiValue: artifactGraphPartExtractorApiValue,
|
|
50
|
+
aliases: ['v3.6.1', '3.6.1', 'v3.6.1 primary images first'],
|
|
51
|
+
},
|
|
45
52
|
];
|
|
46
53
|
const partExtractorPublicNames = partExtractorOptions
|
|
47
54
|
.map(option => option.publicName)
|
|
@@ -49,37 +56,78 @@ const partExtractorPublicNames = partExtractorOptions
|
|
|
49
56
|
const normalizePartExtractorName = (value) => value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
50
57
|
const partExtractorByInput = new Map(partExtractorOptions.flatMap(option => [
|
|
51
58
|
[normalizePartExtractorName(option.publicName), option],
|
|
52
|
-
...option.aliases.map(alias => [
|
|
53
|
-
normalizePartExtractorName(alias),
|
|
54
|
-
option,
|
|
55
|
-
]),
|
|
59
|
+
...option.aliases.map(alias => [normalizePartExtractorName(alias), option]),
|
|
56
60
|
]));
|
|
57
61
|
const partExtractorPublicNameByApiValue = new Map(partExtractorOptions.map(option => [option.apiValue, option.publicName]));
|
|
58
62
|
const isInternalPartExtractorId = (value) => value.trim().includes('_');
|
|
59
63
|
const execFileAsync = promisify(execFile);
|
|
60
|
-
const usage = `AssetHub CLI
|
|
64
|
+
export const usage = `AssetHub CLI
|
|
65
|
+
|
|
66
|
+
Internal preview: canvas/context/layout and moodboards require workspace feature access.
|
|
61
67
|
|
|
62
68
|
Usage:
|
|
63
69
|
assethub auth login --api-key <key> [--base-url <url>] [--profile <name>]
|
|
64
70
|
assethub auth login --api-key-stdin [--base-url <url>] [--profile <name>]
|
|
65
71
|
assethub auth status [--profile <name>]
|
|
66
72
|
assethub auth logout [--profile <name>]
|
|
73
|
+
assethub workspace prepare|sync|analyze|verify --internal --manifest <json> --state <json>
|
|
74
|
+
assethub workspace self-check --internal
|
|
75
|
+
assethub workspace context put --internal --org <uuid> --actor <uuid> --canvas <id> --file <json> [--if-version <n>] [--order <uuid>]
|
|
76
|
+
assethub workspace context get --internal --org <uuid> --actor <uuid> --canvas <id> [--out <json>] [--order <uuid>]
|
|
77
|
+
assethub project ingest <source-dir> --out-dir <directory> [--exclude-dir <relative-path>...]
|
|
78
|
+
assethub language text|vision --input-json @<json> --operation-id <uuid> [--out <json>]
|
|
79
|
+
assethub canvas import --canvas <id> --file <image> [--name <name>]
|
|
80
|
+
assethub canvas context get --canvas <id> [--out <json>]
|
|
81
|
+
assethub canvas context put --canvas <id> --file <json> [--if-version <n>]
|
|
82
|
+
assethub canvas layout --canvas <id> --input-json @<json>
|
|
83
|
+
assethub canvas compare --canvas <id> --source-id <asset-id> --asset-id <asset-id> --out <html>
|
|
84
|
+
assethub moodboard create --input-json @<json> [--operation-id <uuid>]
|
|
85
|
+
assethub moodboard list|get|update|archive [<board-id>] [--input-json @<json>]
|
|
86
|
+
assethub moodboard analyze <board-id> [--revision <uuid>] [--wait]
|
|
87
|
+
assethub image generate --canvas <id> --context <id> [--context-version <n>] [--context-source <key>] [--moodboard-revision <uuid>] --prompt <text> --wait
|
|
67
88
|
assethub models list [--domain <domain>] [--ids-only] [--limit <n>]
|
|
68
89
|
assethub models get <model-id>
|
|
69
90
|
assethub files import <url> --media-type image|mesh
|
|
70
91
|
assethub files upload <file> --media-type image|mesh
|
|
71
92
|
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>]
|
|
72
|
-
assethub
|
|
93
|
+
assethub capabilities
|
|
94
|
+
assethub canvas create [--name <name>] [--operation-id <uuid>]
|
|
95
|
+
assethub canvas list [--cursor <cursor>] [--limit <n>]
|
|
96
|
+
assethub canvas get|use|open [<id>]
|
|
97
|
+
assethub runs list [--canvas <id>] [--cursor <cursor>] [--limit <n>]
|
|
98
|
+
assethub runs get|watch <run-id> [--timeout-ms <n>]
|
|
99
|
+
assethub runs resume <operation-id> [--wait]
|
|
100
|
+
assethub graph show|export [--canvas <id>] [--artifact <node-id>] [--out <path>] [--allow-truncated]
|
|
101
|
+
assethub graph lineage --artifact <node-id> [--canvas <id>] [--direction ancestors|descendants|both] [--depth <n>]
|
|
102
|
+
assethub evaluate list
|
|
103
|
+
assethub evaluations submit --artifact <asset-id> --report <json|file|@file|@-> [--agent <name>] [--canvas <id>] [--reference <asset-id>...] [--run <run-id>] [--operation-id <uuid>] [--require-pass]
|
|
104
|
+
assethub evaluations list [--canvas <id>] [--artifact <asset-id>]
|
|
105
|
+
assethub evaluations get <evaluation-id>
|
|
106
|
+
assethub image generate --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--agent <name>] [--derived-from-run <run-id>] [--from-node <graphId/nodeId> --graph-revision <rev>] [--wait]
|
|
107
|
+
assethub mesh generate --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--agent <name>] [--derived-from-run <run-id>] [--from-node <graphId/nodeId> --graph-revision <rev>] [--wait]
|
|
108
|
+
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>] [--canvas <id>] [--operation-id <uuid>] [--api-version v2]
|
|
73
109
|
assethub image generate --prompt <text> --source-resource-id <id> --api-version v2 [--model-id <id>] [--batch-size <n>] [--wait]
|
|
74
110
|
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>]
|
|
75
111
|
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>]
|
|
112
|
+
assethub rig check (--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>) [--name <name>] [--wait] [--download --out-dir <dir>]
|
|
113
|
+
assethub rig create (--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>) [--model v1.0-20240301|v2.5-20260210] [--rig-type biped|quadruped|hexapod|octopod|avian|serpentine|aquatic] [--spec tripo|mixamo] [--out-format glb|fbx] [--name <name>] [--wait] [--download --out-dir <dir>]
|
|
114
|
+
assethub animate presets [--model v1.0-20240301|v2.5-20260210] [--rig-type <type>]
|
|
115
|
+
assethub animate retarget --resource-id <id> --animation <preset-id> [--animation <preset-id>... up to 5 total] [--out-format glb|fbx] [--bake-animation true|false] [--export-with-geometry true|false] [--animate-in-place true|false] [--name <name>] [--wait] [--download --out-dir <dir>]
|
|
76
116
|
assethub jobs get <job-id> [--download --out-dir <dir>]
|
|
77
117
|
assethub jobs watch <job-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
|
|
78
118
|
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>]
|
|
119
|
+
assethub production agents
|
|
120
|
+
assethub production automation --input-json <json|@file|@-> [--canvas <id>] [--operation-id <uuid>] [--wait]
|
|
79
121
|
assethub production status <order-id> [--wait] [--download --out-dir <dir>]
|
|
80
122
|
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>]
|
|
123
|
+
assethub production run --order-id <id> --mission-id <id> [--run-mode full_auto|approval] [--task-id <id>...] [--part-extractor <name>] [--part-extraction-mode fast|high_quality] [--allowed-model-id <id>...] [--max-iterations <n>] [--max-cost-credits <n>] [--wait] [--download --out-dir <dir>]
|
|
124
|
+
assethub production watch <order-id> [--interval-ms <ms>] [--timeout-ms <ms>] [--download --out-dir <dir>]
|
|
125
|
+
assethub production intervene <order-id> (--exclude <target-id> | --include <target-id> | --add-part <name> | --rename <target-id>=<name> | --reject <target-id>[=<reason>] | --regenerate <target-id>=<mode> | --set-param <key>=<value> | --ops-json <json|@file|@->)... [--idempotency-key <key>]
|
|
126
|
+
assethub production interventions <order-id>
|
|
127
|
+
assethub runs upload <path> [--graph-id <id>] [--stream-id <id>] [--rev <n>] [--description <text>] [--tag <tag>...] [--skip-register] [--dry-run]
|
|
81
128
|
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>]
|
|
82
129
|
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>]
|
|
130
|
+
assethub autopilot [<image>] --demo [--yolo] [--json] [--max-regen <n>] [--stall-rounds <n>] [--tie-epsilon <x>] [--multiview 2-view|4-view|6-view] [--ab-mode serial|cross] [--credit-budget <n>]
|
|
83
131
|
|
|
84
132
|
Global options:
|
|
85
133
|
--api-key <key> Overrides saved auth and ASSETHUB_API_KEY.
|
|
@@ -101,7 +149,16 @@ Examples:
|
|
|
101
149
|
cat ./input.png | assethub parts split --stdin --file-name input.png --part-extractor "V1.5" --wait
|
|
102
150
|
assethub image generate --prompt "stylized prop concept" --wait
|
|
103
151
|
assethub mesh generate --file ./input.png --wait
|
|
152
|
+
assethub animate presets --rig-type biped
|
|
153
|
+
assethub rig create --source-resource-id mesh_x --rig-type biped --wait
|
|
154
|
+
assethub animate retarget --resource-id rig_x --animation preset:idle --wait
|
|
104
155
|
assethub production analyze --file ./input.png --part-extractor "V1.5" --wait
|
|
156
|
+
assethub production run --order-id ord_x --mission-id ms_x --run-mode full_auto --max-cost-credits 200 --wait
|
|
157
|
+
assethub production watch ord_x
|
|
158
|
+
assethub production intervene ord_x --exclude 7 --rename 3="left wing" --idempotency-key review-2026-07-29
|
|
159
|
+
assethub production interventions ord_x
|
|
160
|
+
assethub runs upload ./out/pluffy-run-2026-07-30 --dry-run
|
|
161
|
+
assethub runs upload ./out/pluffy-run-2026-07-30 --tag pluffy --description "V3 humanoid, 18 parts"
|
|
105
162
|
assethub parts split --file ./input.png --part-extractor "V1.5" --wait --all-ready
|
|
106
163
|
assethub parts compare --file ./input.png --preprocess-prompt "clean white background, centered product photo" --part-extractor "V1.5" --wait --all-ready
|
|
107
164
|
`;
|
|
@@ -127,7 +184,7 @@ const parseArgs = (args) => {
|
|
|
127
184
|
const value = inlineValue != null
|
|
128
185
|
? inlineValue
|
|
129
186
|
: next != null && !next.startsWith('--')
|
|
130
|
-
? (index += 1, next)
|
|
187
|
+
? ((index += 1), next)
|
|
131
188
|
: true;
|
|
132
189
|
const previous = flags[key];
|
|
133
190
|
if (previous == null) {
|
|
@@ -211,11 +268,15 @@ const sanitizeProductionOutput = (value) => {
|
|
|
211
268
|
if (value == null || typeof value !== 'object') {
|
|
212
269
|
return value;
|
|
213
270
|
}
|
|
214
|
-
return Object.fromEntries(Object.entries(value)
|
|
271
|
+
return Object.fromEntries(Object.entries(value)
|
|
272
|
+
.filter(([key]) => key !== 'publicAccessToken')
|
|
273
|
+
.map(([key, entry]) => [
|
|
215
274
|
key,
|
|
216
|
-
key === '
|
|
217
|
-
?
|
|
218
|
-
:
|
|
275
|
+
key === 'execution'
|
|
276
|
+
? entry
|
|
277
|
+
: key === 'agentVersion' && typeof entry === 'string'
|
|
278
|
+
? publicPartExtractorName(entry)
|
|
279
|
+
: sanitizeProductionOutput(entry),
|
|
219
280
|
]));
|
|
220
281
|
};
|
|
221
282
|
const parsePositiveIntegerFlag = (flags, name, fallback) => {
|
|
@@ -242,6 +303,24 @@ const parseBooleanFlag = (flags, name) => {
|
|
|
242
303
|
}
|
|
243
304
|
throw new Error(`--${name} must be true or false`);
|
|
244
305
|
};
|
|
306
|
+
/**
|
|
307
|
+
* An optional flag whose value must come from a fixed set.
|
|
308
|
+
*
|
|
309
|
+
* The server stays the authority on what it accepts; this only turns a typo
|
|
310
|
+
* into an immediate local error that names the valid values, instead of a
|
|
311
|
+
* round trip that comes back as an opaque 400. Same contract as
|
|
312
|
+
* `parseBooleanFlag`: absent means `undefined`, present-but-wrong throws.
|
|
313
|
+
*/
|
|
314
|
+
const parseEnumFlag = (flags, name, allowed) => {
|
|
315
|
+
const raw = getFlag(flags, name);
|
|
316
|
+
if (raw == null) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
if (allowed.includes(raw)) {
|
|
320
|
+
return raw;
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`--${name} must be one of: ${allowed.join(', ')}`);
|
|
323
|
+
};
|
|
245
324
|
const parseMediaType = (value) => {
|
|
246
325
|
if (value === 'image' || value === 'mesh') {
|
|
247
326
|
return value;
|
|
@@ -335,15 +414,18 @@ const resolveAuth = async (flags) => {
|
|
|
335
414
|
return {
|
|
336
415
|
apiKey: storedProfile.apiKey,
|
|
337
416
|
baseUrl: baseUrlFromFlags ?? env.ASSETHUB_API_BASE_URL ?? storedProfile.baseUrl,
|
|
338
|
-
profile: config.profiles?.[profile] != null
|
|
417
|
+
profile: config.profiles?.[profile] != null
|
|
418
|
+
? profile
|
|
419
|
+
: (fallbackProfileName ?? profile),
|
|
339
420
|
source: 'profile',
|
|
340
421
|
};
|
|
341
422
|
}
|
|
342
423
|
throw new Error('Missing API key. Run "assethub auth login --api-key-stdin", set ASSETHUB_API_KEY, or pass --api-key.');
|
|
343
424
|
};
|
|
344
|
-
const createContext = async (flags) => {
|
|
425
|
+
const createContext = async (flags, args) => {
|
|
345
426
|
const auth = await resolveAuth(flags);
|
|
346
427
|
return {
|
|
428
|
+
args,
|
|
347
429
|
auth,
|
|
348
430
|
flags,
|
|
349
431
|
client: createAssetHubClient({
|
|
@@ -362,8 +444,7 @@ const isBlobLocation = (value) => {
|
|
|
362
444
|
typeof value.path === 'string') {
|
|
363
445
|
return true;
|
|
364
446
|
}
|
|
365
|
-
if (value.type === 'signedSupabase' &&
|
|
366
|
-
typeof value.signedUrl === 'string') {
|
|
447
|
+
if (value.type === 'signedSupabase' && typeof value.signedUrl === 'string') {
|
|
367
448
|
return true;
|
|
368
449
|
}
|
|
369
450
|
if (value.type === 'asset' && typeof value.assetId === 'string') {
|
|
@@ -406,7 +487,10 @@ const blobFromBase64 = (value, contentType, flagName) => {
|
|
|
406
487
|
if (trimmed.startsWith('data:')) {
|
|
407
488
|
return blobFromDataUri(trimmed, flagName);
|
|
408
489
|
}
|
|
409
|
-
const normalized = trimmed
|
|
490
|
+
const normalized = trimmed
|
|
491
|
+
.replace(/\s+/g, '')
|
|
492
|
+
.replace(/-/g, '+')
|
|
493
|
+
.replace(/_/g, '/');
|
|
410
494
|
if (normalized === '') {
|
|
411
495
|
throw new Error(`${flagName} input is empty.`);
|
|
412
496
|
}
|
|
@@ -437,6 +521,61 @@ const extensionFromMimeType = (mimeType, mediaType) => {
|
|
|
437
521
|
return 'gltf';
|
|
438
522
|
return mediaType === 'image' ? 'png' : 'glb';
|
|
439
523
|
};
|
|
524
|
+
const contentTypeFromExtension = (extension) => {
|
|
525
|
+
switch (extension.toLowerCase()) {
|
|
526
|
+
case 'png':
|
|
527
|
+
return 'image/png';
|
|
528
|
+
case 'jpg':
|
|
529
|
+
case 'jpeg':
|
|
530
|
+
return 'image/jpeg';
|
|
531
|
+
case 'webp':
|
|
532
|
+
return 'image/webp';
|
|
533
|
+
case 'gif':
|
|
534
|
+
return 'image/gif';
|
|
535
|
+
case 'bmp':
|
|
536
|
+
return 'image/bmp';
|
|
537
|
+
case 'tif':
|
|
538
|
+
case 'tiff':
|
|
539
|
+
return 'image/tiff';
|
|
540
|
+
case 'glb':
|
|
541
|
+
return 'model/gltf-binary';
|
|
542
|
+
case 'gltf':
|
|
543
|
+
return 'model/gltf+json';
|
|
544
|
+
case 'obj':
|
|
545
|
+
return 'model/obj';
|
|
546
|
+
case 'fbx':
|
|
547
|
+
return 'application/fbx';
|
|
548
|
+
case 'stl':
|
|
549
|
+
return 'model/stl';
|
|
550
|
+
case 'ply':
|
|
551
|
+
return 'text/plain';
|
|
552
|
+
case 'usdz':
|
|
553
|
+
return 'model/vnd.usdz+zip';
|
|
554
|
+
case 'usd':
|
|
555
|
+
case 'usdc':
|
|
556
|
+
return 'model/vnd.usd';
|
|
557
|
+
default:
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
const isNonspecificContentType = (contentType) => {
|
|
562
|
+
const clean = contentType?.split(';')[0]?.trim().toLowerCase() ?? '';
|
|
563
|
+
return (clean === '' ||
|
|
564
|
+
clean === 'application/octet-stream' ||
|
|
565
|
+
clean === 'binary/octet-stream');
|
|
566
|
+
};
|
|
567
|
+
const inferContentTypeFromFileName = (fileName) => {
|
|
568
|
+
const extension = extname(fileName).replace(/^\./, '');
|
|
569
|
+
if (extension === '')
|
|
570
|
+
return undefined;
|
|
571
|
+
return contentTypeFromExtension(extension);
|
|
572
|
+
};
|
|
573
|
+
const resolveUploadContentType = (fileName, explicitContentType) => {
|
|
574
|
+
if (!isNonspecificContentType(explicitContentType)) {
|
|
575
|
+
return explicitContentType;
|
|
576
|
+
}
|
|
577
|
+
return inferContentTypeFromFileName(fileName);
|
|
578
|
+
};
|
|
440
579
|
const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
441
580
|
if (extname(fileName) !== '') {
|
|
442
581
|
return fileName;
|
|
@@ -445,7 +584,9 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
|
445
584
|
};
|
|
446
585
|
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.';
|
|
447
586
|
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.';
|
|
448
|
-
const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ??
|
|
587
|
+
const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ??
|
|
588
|
+
getFlag(flags, 'image-url') ??
|
|
589
|
+
getFlag(flags, 'url');
|
|
449
590
|
const assertFlagHasValue = (value, flagDescription) => {
|
|
450
591
|
if (value === 'true' || value.trim() === '') {
|
|
451
592
|
throw new Error(`Missing required flag value: ${flagDescription}`);
|
|
@@ -570,7 +711,15 @@ const sourceDescriptorFromJson = (value, options) => {
|
|
|
570
711
|
if (resourceId != null) {
|
|
571
712
|
return { kind: 'source', source: { resourceId } };
|
|
572
713
|
}
|
|
573
|
-
const
|
|
714
|
+
const uploadId = getFirstString(value, ['uploadId', 'upload_id']);
|
|
715
|
+
if (uploadId != null)
|
|
716
|
+
return { kind: 'source', source: { uploadId } };
|
|
717
|
+
const fileRefValue = value.fileRef ??
|
|
718
|
+
value.file_ref ??
|
|
719
|
+
value.imageBlobLocation ??
|
|
720
|
+
value.image_blob_location ??
|
|
721
|
+
value.blobLocation ??
|
|
722
|
+
value.blob_location;
|
|
574
723
|
if (isBlobLocation(fileRefValue)) {
|
|
575
724
|
return { kind: 'source', source: { fileRef: fileRefValue } };
|
|
576
725
|
}
|
|
@@ -597,7 +746,13 @@ const sourceDescriptorFromJson = (value, options) => {
|
|
|
597
746
|
flagName: '--source-json',
|
|
598
747
|
});
|
|
599
748
|
}
|
|
600
|
-
const url = getFirstString(value, [
|
|
749
|
+
const url = getFirstString(value, [
|
|
750
|
+
'sourceUrl',
|
|
751
|
+
'source_url',
|
|
752
|
+
'url',
|
|
753
|
+
'publicUrl',
|
|
754
|
+
'public_url',
|
|
755
|
+
]);
|
|
601
756
|
if (url != null) {
|
|
602
757
|
const source = sourceFromString(url);
|
|
603
758
|
if (source != null) {
|
|
@@ -695,24 +850,31 @@ const readClipboardImage = async () => {
|
|
|
695
850
|
await unlink(filePath).catch(() => undefined);
|
|
696
851
|
}
|
|
697
852
|
};
|
|
698
|
-
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
|
|
853
|
+
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, preferUploadId, }) => {
|
|
699
854
|
if (descriptor.kind === 'source') {
|
|
700
855
|
return descriptor.source;
|
|
701
856
|
}
|
|
857
|
+
const fileName = descriptor.kind === 'file' ? basename(descriptor.path) : descriptor.fileName;
|
|
858
|
+
const fileNameWithExtension = ensureFileNameExtension(fileName, mediaType, descriptor.contentType);
|
|
859
|
+
const contentType = resolveUploadContentType(fileNameWithExtension, descriptor.contentType);
|
|
702
860
|
const file = descriptor.kind === 'file'
|
|
703
|
-
? await readBlob(descriptor.path,
|
|
704
|
-
: descriptor.blob
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
: descriptor.fileName;
|
|
861
|
+
? await readBlob(descriptor.path, contentType)
|
|
862
|
+
: isNonspecificContentType(descriptor.blob.type) && contentType != null
|
|
863
|
+
? new Blob([descriptor.blob], { type: contentType })
|
|
864
|
+
: descriptor.blob;
|
|
708
865
|
const upload = await client.v2.uploadFile({
|
|
709
866
|
file,
|
|
710
|
-
fileName:
|
|
867
|
+
fileName: fileNameWithExtension,
|
|
711
868
|
mediaType,
|
|
712
869
|
});
|
|
870
|
+
if (preferUploadId) {
|
|
871
|
+
if (!upload.uploadId)
|
|
872
|
+
throw new Error('Server did not return a durable upload ID');
|
|
873
|
+
return { uploadId: upload.uploadId };
|
|
874
|
+
}
|
|
713
875
|
return { fileRef: upload.fileRef };
|
|
714
876
|
};
|
|
715
|
-
const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
877
|
+
const resolveSource = async ({ flags, client, fileMediaType, preferUploadId, }) => {
|
|
716
878
|
const providedCount = sourceInputCount(flags);
|
|
717
879
|
if (providedCount === 0) {
|
|
718
880
|
return undefined;
|
|
@@ -741,9 +903,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
741
903
|
const filePath = getFlag(flags, 'file');
|
|
742
904
|
if (filePath != null) {
|
|
743
905
|
return await uploadSourceDescriptor({
|
|
744
|
-
descriptor: {
|
|
906
|
+
descriptor: {
|
|
907
|
+
kind: 'file',
|
|
908
|
+
path: assertFlagHasValue(filePath, '--file <path>'),
|
|
909
|
+
},
|
|
745
910
|
client,
|
|
746
911
|
mediaType: fileMediaType,
|
|
912
|
+
preferUploadId,
|
|
747
913
|
});
|
|
748
914
|
}
|
|
749
915
|
const dataUri = getFlag(flags, 'data-uri');
|
|
@@ -752,35 +918,49 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
752
918
|
descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
|
|
753
919
|
client,
|
|
754
920
|
mediaType: fileMediaType,
|
|
921
|
+
preferUploadId,
|
|
755
922
|
});
|
|
756
923
|
}
|
|
757
924
|
if (getFlag(flags, 'source-json') != null) {
|
|
758
925
|
const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
|
|
759
926
|
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
760
927
|
mediaType: fileMediaType,
|
|
761
|
-
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
928
|
+
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
929
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
762
930
|
});
|
|
763
931
|
if (descriptor == null) {
|
|
764
932
|
throw new Error('--source-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
|
|
765
933
|
}
|
|
766
|
-
return await uploadSourceDescriptor({
|
|
934
|
+
return await uploadSourceDescriptor({
|
|
935
|
+
descriptor,
|
|
936
|
+
client,
|
|
937
|
+
mediaType: fileMediaType,
|
|
938
|
+
preferUploadId,
|
|
939
|
+
});
|
|
767
940
|
}
|
|
768
941
|
if (hasFlag(flags, 'stdin-json')) {
|
|
769
942
|
const parsed = parseJsonValue(await readStdin(), '--stdin-json');
|
|
770
943
|
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
771
944
|
mediaType: fileMediaType,
|
|
772
|
-
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
945
|
+
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
946
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
773
947
|
});
|
|
774
948
|
if (descriptor == null) {
|
|
775
949
|
throw new Error('--stdin-json did not contain a usable source. Expected url, image_url.url, resourceId, fileRef, data URI, base64 data, or path.');
|
|
776
950
|
}
|
|
777
|
-
return await uploadSourceDescriptor({
|
|
951
|
+
return await uploadSourceDescriptor({
|
|
952
|
+
descriptor,
|
|
953
|
+
client,
|
|
954
|
+
mediaType: fileMediaType,
|
|
955
|
+
preferUploadId,
|
|
956
|
+
});
|
|
778
957
|
}
|
|
779
958
|
if (hasFlag(flags, 'stdin-data-uri')) {
|
|
780
959
|
return await uploadSourceDescriptor({
|
|
781
960
|
descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
|
|
782
961
|
client,
|
|
783
962
|
mediaType: fileMediaType,
|
|
963
|
+
preferUploadId,
|
|
784
964
|
});
|
|
785
965
|
}
|
|
786
966
|
if (hasFlag(flags, 'stdin-base64')) {
|
|
@@ -788,12 +968,14 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
788
968
|
descriptor: descriptorFromBase64({
|
|
789
969
|
value: await readStdin(),
|
|
790
970
|
contentType: getContentTypeFlag(flags),
|
|
791
|
-
fileName: getFlag(flags, 'file-name') ??
|
|
971
|
+
fileName: getFlag(flags, 'file-name') ??
|
|
972
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
792
973
|
mediaType: fileMediaType,
|
|
793
974
|
flagName: '--stdin-base64',
|
|
794
975
|
}),
|
|
795
976
|
client,
|
|
796
977
|
mediaType: fileMediaType,
|
|
978
|
+
preferUploadId,
|
|
797
979
|
});
|
|
798
980
|
}
|
|
799
981
|
if (hasFlag(flags, 'clipboard')) {
|
|
@@ -809,6 +991,7 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
809
991
|
},
|
|
810
992
|
client,
|
|
811
993
|
mediaType: fileMediaType,
|
|
994
|
+
preferUploadId,
|
|
812
995
|
});
|
|
813
996
|
}
|
|
814
997
|
if (hasFlag(flags, 'stdin')) {
|
|
@@ -816,11 +999,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
816
999
|
descriptor: {
|
|
817
1000
|
kind: 'blob',
|
|
818
1001
|
blob: await readStdinBlob(getContentTypeFlag(flags)),
|
|
819
|
-
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ??
|
|
1002
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ??
|
|
1003
|
+
defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
|
|
820
1004
|
contentType: getContentTypeFlag(flags),
|
|
821
1005
|
},
|
|
822
1006
|
client,
|
|
823
1007
|
mediaType: fileMediaType,
|
|
1008
|
+
preferUploadId,
|
|
824
1009
|
});
|
|
825
1010
|
}
|
|
826
1011
|
return undefined;
|
|
@@ -832,69 +1017,126 @@ const requireSource = async (options) => {
|
|
|
832
1017
|
}
|
|
833
1018
|
return source;
|
|
834
1019
|
};
|
|
835
|
-
const sourceToV1ImageInput = (source) => {
|
|
836
|
-
if (source == null) {
|
|
837
|
-
return {};
|
|
838
|
-
}
|
|
839
|
-
if ('url' in source) {
|
|
840
|
-
return { imageUrl: source.url };
|
|
841
|
-
}
|
|
842
|
-
if ('fileRef' in source) {
|
|
843
|
-
return { imageBlobLocation: source.fileRef };
|
|
844
|
-
}
|
|
845
|
-
throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
|
|
846
|
-
};
|
|
847
1020
|
const resolveProductionAnalyzeSource = async (ctx) => {
|
|
848
|
-
const source = await requireSource({
|
|
1021
|
+
const source = await recordedSource(ctx, await requireSource({
|
|
849
1022
|
flags: ctx.flags,
|
|
850
1023
|
client: ctx.client,
|
|
851
1024
|
fileMediaType: 'image',
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
if (source.url != null) {
|
|
857
|
-
return { imageUrl: source.url };
|
|
858
|
-
}
|
|
859
|
-
if (source.fileRef == null) {
|
|
860
|
-
throw new Error(missingSourceMessage);
|
|
861
|
-
}
|
|
862
|
-
return { imageBlobLocation: source.fileRef };
|
|
1025
|
+
preferUploadId: true,
|
|
1026
|
+
}));
|
|
1027
|
+
return productionAnalyzeSourceFromSource(source);
|
|
863
1028
|
};
|
|
864
1029
|
const productionAnalyzeSourceFromSource = (source) => {
|
|
865
|
-
if (source.
|
|
1030
|
+
if (source.uploadId)
|
|
1031
|
+
return { uploadId: source.uploadId };
|
|
1032
|
+
if (source.resourceId)
|
|
866
1033
|
return { imageAssetId: source.resourceId };
|
|
867
|
-
|
|
868
|
-
if (source.url != null) {
|
|
1034
|
+
if (source.url)
|
|
869
1035
|
return { imageUrl: source.url };
|
|
870
|
-
|
|
871
|
-
|
|
1036
|
+
if (source.fileRef)
|
|
1037
|
+
return { imageBlobLocation: source.fileRef };
|
|
1038
|
+
throw new Error(missingSourceMessage);
|
|
872
1039
|
};
|
|
873
1040
|
const sourceFromProductionAnalyzeSource = (source) => {
|
|
1041
|
+
if (source.uploadId)
|
|
1042
|
+
return { uploadId: source.uploadId };
|
|
874
1043
|
if (source.imageAssetId != null) {
|
|
875
1044
|
return { resourceId: source.imageAssetId };
|
|
876
1045
|
}
|
|
877
1046
|
if (source.imageUrl != null) {
|
|
878
1047
|
return { url: source.imageUrl };
|
|
879
1048
|
}
|
|
880
|
-
|
|
1049
|
+
if (source.imageBlobLocation)
|
|
1050
|
+
return { fileRef: source.imageBlobLocation };
|
|
1051
|
+
throw new Error(missingSourceMessage);
|
|
881
1052
|
};
|
|
882
|
-
const productionAnalyzeTerminalStatuses = new Set([
|
|
1053
|
+
const productionAnalyzeTerminalStatuses = new Set([
|
|
1054
|
+
'ready',
|
|
1055
|
+
'completed',
|
|
1056
|
+
'failed',
|
|
1057
|
+
]);
|
|
883
1058
|
const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
|
|
884
|
-
const
|
|
1059
|
+
const parseRunModeFlag = (flags) => {
|
|
1060
|
+
const raw = getFlag(flags, 'run-mode') ?? 'full_auto';
|
|
1061
|
+
if (raw === 'approval' || raw === 'full_auto') {
|
|
1062
|
+
return raw;
|
|
1063
|
+
}
|
|
1064
|
+
throw new Error('--run-mode must be "approval" or "full_auto"');
|
|
1065
|
+
};
|
|
1066
|
+
/**
|
|
1067
|
+
* Derive a human-facing run phase from a production status payload. Beyond the
|
|
1068
|
+
* raw order status, this surfaces `waiting_approval` — the state an approval
|
|
1069
|
+
* run parks in once parts are generated but not yet reviewed.
|
|
1070
|
+
*/
|
|
1071
|
+
const deriveRunPhase = (status) => {
|
|
1072
|
+
const order = status != null && typeof status === 'object' && 'order' in status
|
|
1073
|
+
? status.order
|
|
1074
|
+
: undefined;
|
|
1075
|
+
const orderStatus = order != null && typeof order.status === 'string' ? order.status : 'unknown';
|
|
1076
|
+
if (orderStatus !== 'in_progress') {
|
|
1077
|
+
return orderStatus;
|
|
1078
|
+
}
|
|
1079
|
+
const summary = status != null && typeof status === 'object' && 'summary' in status
|
|
1080
|
+
? status.summary
|
|
1081
|
+
: undefined;
|
|
1082
|
+
const pending = summary != null && typeof summary.pendingTasks === 'number'
|
|
1083
|
+
? summary.pendingTasks
|
|
1084
|
+
: undefined;
|
|
1085
|
+
const completed = summary != null && typeof summary.completedTasks === 'number'
|
|
1086
|
+
? summary.completedTasks
|
|
1087
|
+
: undefined;
|
|
1088
|
+
if (pending === 0 && completed != null && completed > 0) {
|
|
1089
|
+
return 'waiting_approval';
|
|
1090
|
+
}
|
|
1091
|
+
return 'in_progress';
|
|
1092
|
+
};
|
|
1093
|
+
const productionWatchTerminalPhases = new Set([
|
|
1094
|
+
'completed',
|
|
1095
|
+
'failed',
|
|
1096
|
+
'waiting_approval',
|
|
1097
|
+
]);
|
|
1098
|
+
const watchProductionRun = async ({ ctx, orderId, }) => {
|
|
885
1099
|
const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
|
|
886
|
-
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms',
|
|
1100
|
+
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 1800000);
|
|
887
1101
|
const startedAt = Date.now();
|
|
1102
|
+
let lastPhase;
|
|
888
1103
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
889
1104
|
const status = await ctx.client.v1.getProductionStatus(orderId);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
1105
|
+
const phase = deriveRunPhase(status);
|
|
1106
|
+
if (phase !== lastPhase) {
|
|
1107
|
+
stderr.write(`[watch] order=${orderId} phase=${phase}\n`);
|
|
1108
|
+
lastPhase = phase;
|
|
1109
|
+
}
|
|
1110
|
+
if (productionWatchTerminalPhases.has(phase)) {
|
|
893
1111
|
return status;
|
|
894
1112
|
}
|
|
895
1113
|
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
896
1114
|
}
|
|
897
|
-
throw new Error(`
|
|
1115
|
+
throw new Error(`Watch timeout: order=${orderId} did not reach a terminal phase within ${timeoutMs}ms`);
|
|
1116
|
+
};
|
|
1117
|
+
const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
|
|
1118
|
+
const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
|
|
1119
|
+
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
|
|
1120
|
+
const deadline = Date.now() + timeoutMs;
|
|
1121
|
+
const timeout = () => new CliExecutionError(`Wait timed out for order ${orderId}; server execution continues.`, 3, activeExecution.operationId, activeExecution.execution, activeExecution.runId);
|
|
1122
|
+
while (Date.now() < deadline) {
|
|
1123
|
+
const signal = AbortSignal.timeout(Math.max(1, deadline - Date.now()));
|
|
1124
|
+
let status;
|
|
1125
|
+
try {
|
|
1126
|
+
status = await ctx.client.v1.getProductionStatus(orderId, { signal });
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
if (signal.aborted)
|
|
1130
|
+
throw timeout();
|
|
1131
|
+
throw error;
|
|
1132
|
+
}
|
|
1133
|
+
const orderStatus = status.order.status;
|
|
1134
|
+
stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
|
|
1135
|
+
if (terminalStatuses.has(orderStatus))
|
|
1136
|
+
return status;
|
|
1137
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
|
|
1138
|
+
}
|
|
1139
|
+
throw timeout();
|
|
898
1140
|
};
|
|
899
1141
|
const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
|
|
900
1142
|
if (!hasFlag(ctx.flags, 'wait')) {
|
|
@@ -923,11 +1165,18 @@ const commandSource = async (subcommand, ctx) => {
|
|
|
923
1165
|
throw new Error('Unknown source command. Use "source create".');
|
|
924
1166
|
}
|
|
925
1167
|
const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
|
|
926
|
-
const source = await
|
|
1168
|
+
const source = await resolveSource({
|
|
927
1169
|
flags: ctx.flags,
|
|
928
1170
|
client: ctx.client,
|
|
929
1171
|
fileMediaType: mediaType,
|
|
1172
|
+
preferUploadId: true,
|
|
930
1173
|
});
|
|
1174
|
+
if (!source)
|
|
1175
|
+
throw new Error(missingSourceMessage);
|
|
1176
|
+
if (source.uploadId != null) {
|
|
1177
|
+
print({ source, uploadId: source.uploadId, mediaType });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
931
1180
|
if (source.url != null) {
|
|
932
1181
|
print({ source, url: source.url, mediaType });
|
|
933
1182
|
return;
|
|
@@ -1045,82 +1294,349 @@ const commandFiles = async (subcommand, positionals, ctx) => {
|
|
|
1045
1294
|
}
|
|
1046
1295
|
throw new Error('Unknown files command. Use "files import" or "files upload".');
|
|
1047
1296
|
};
|
|
1297
|
+
const stateOptions = (ctx) => ({
|
|
1298
|
+
client: ctx.client,
|
|
1299
|
+
cwd: process.cwd(),
|
|
1300
|
+
stateDir: env.ASSETHUB_CLI_STATE_DIR ??
|
|
1301
|
+
join(dirname(getConfigPath(ctx.flags)), 'state'),
|
|
1302
|
+
});
|
|
1303
|
+
const selectedCanvasId = (flags) => getFlag(flags, 'canvas') == null
|
|
1304
|
+
? undefined
|
|
1305
|
+
: parsePositiveIntegerFlag(flags, 'canvas', 1);
|
|
1306
|
+
const executionOptions = (flags) => ({
|
|
1307
|
+
operationId: getFlag(flags, 'operation-id'),
|
|
1308
|
+
parentRunId: getFlag(flags, 'derived-from-run') ?? getFlag(flags, 'parent-run'),
|
|
1309
|
+
agent: getFlag(flags, 'agent')
|
|
1310
|
+
? {
|
|
1311
|
+
name: getFlag(flags, 'agent'),
|
|
1312
|
+
sessionId: getFlag(flags, 'agent-session'),
|
|
1313
|
+
}
|
|
1314
|
+
: undefined,
|
|
1315
|
+
});
|
|
1316
|
+
const watchOptions = (flags) => ({
|
|
1317
|
+
intervalMs: parsePositiveIntegerFlag(flags, 'interval-ms', 5000),
|
|
1318
|
+
timeoutMs: parsePositiveIntegerFlag(flags, 'timeout-ms', 900000),
|
|
1319
|
+
onProgress: (run) => stderr.write(`[run] ${run.runId} ${run.status}\n`),
|
|
1320
|
+
});
|
|
1321
|
+
const readJsonArgument = async (value, flag) => {
|
|
1322
|
+
const text = value === '@-'
|
|
1323
|
+
? await readStdin()
|
|
1324
|
+
: value.startsWith('@')
|
|
1325
|
+
? await readFile(resolve(value.slice(1)), 'utf8')
|
|
1326
|
+
: value;
|
|
1327
|
+
const parsed = parseJsonValue(text, flag);
|
|
1328
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
1329
|
+
throw new Error(`${flag} must contain a JSON object`);
|
|
1330
|
+
return parsed;
|
|
1331
|
+
};
|
|
1332
|
+
const recordedSource = async (ctx, source) => {
|
|
1333
|
+
if (!source)
|
|
1334
|
+
return undefined;
|
|
1335
|
+
if (source.url) {
|
|
1336
|
+
const imported = await ctx.client.v2.importFile({
|
|
1337
|
+
url: source.url,
|
|
1338
|
+
mediaType: 'image',
|
|
1339
|
+
});
|
|
1340
|
+
if (!imported.uploadId)
|
|
1341
|
+
throw new Error('Server did not return a durable upload ID');
|
|
1342
|
+
return { uploadId: imported.uploadId };
|
|
1343
|
+
}
|
|
1344
|
+
if (source.fileRef)
|
|
1345
|
+
throw new Error('Canvas history requires --file, --source-id, --source-url or an uploadId; raw fileRef is unsupported');
|
|
1346
|
+
return source;
|
|
1347
|
+
};
|
|
1348
|
+
const graphSourceInput = async (ctx, canvasId) => {
|
|
1349
|
+
const fromNode = getFlag(ctx.flags, 'from-node');
|
|
1350
|
+
if (!fromNode) {
|
|
1351
|
+
if (getFlag(ctx.flags, 'graph-revision'))
|
|
1352
|
+
throw new Error('--graph-revision requires --from-node');
|
|
1353
|
+
return undefined;
|
|
1354
|
+
}
|
|
1355
|
+
if (sourceInputCount(ctx.flags) || getFlag(ctx.flags, 'input-json'))
|
|
1356
|
+
throw new Error('--from-node cannot be combined with another source or --input-json');
|
|
1357
|
+
const separator = fromNode.indexOf('/');
|
|
1358
|
+
if (separator < 1)
|
|
1359
|
+
throw new Error('--from-node must be <graphId>/<nodeId>');
|
|
1360
|
+
const graphId = fromNode.slice(0, separator), nodeId = fromNode.slice(separator + 1);
|
|
1361
|
+
const revision = requireFlag(ctx.flags, 'graph-revision');
|
|
1362
|
+
const graph = await ctx.client.v2.getCanvasGraph(canvasId);
|
|
1363
|
+
if (graph.graphId !== graphId || graph.revision !== revision)
|
|
1364
|
+
throw new Error('Graph revision changed or belongs to another canvas; read graph show again');
|
|
1365
|
+
const node = graph.nodes.find(item => item.id === nodeId);
|
|
1366
|
+
const assetId = node?.metadata.assetId;
|
|
1367
|
+
if (typeof assetId !== 'string')
|
|
1368
|
+
throw new Error('Graph node has no source asset in this graph projection');
|
|
1369
|
+
return {
|
|
1370
|
+
source: { resourceId: assetId },
|
|
1371
|
+
graphSource: { graphId, nodeId, revision },
|
|
1372
|
+
};
|
|
1373
|
+
};
|
|
1374
|
+
const executionExitCode = (execution) => ['failed', 'partial', 'cancelled'].includes(execution.status)
|
|
1375
|
+
? 1
|
|
1376
|
+
: execution.status === 'needs_review' ||
|
|
1377
|
+
execution.history.status !== 'recorded'
|
|
1378
|
+
? 3
|
|
1379
|
+
: 0;
|
|
1380
|
+
const printRecorded = async (ctx, queued) => {
|
|
1381
|
+
const execution = hasFlag(ctx.flags, 'wait')
|
|
1382
|
+
? await waitForExecution(ctx.client, queued.execution.runId, watchOptions(ctx.flags))
|
|
1383
|
+
: queued.execution;
|
|
1384
|
+
const job = hasFlag(ctx.flags, 'wait') && execution.jobIds[0]
|
|
1385
|
+
? await ctx.client.v2.getJob(execution.jobIds[0])
|
|
1386
|
+
: undefined;
|
|
1387
|
+
const downloads = await maybeDownloadUrls(ctx.flags, execution.outputs);
|
|
1388
|
+
print({
|
|
1389
|
+
queued: execution.operation.startsWith('production.')
|
|
1390
|
+
? sanitizeProductionOutput(queued)
|
|
1391
|
+
: queued,
|
|
1392
|
+
job,
|
|
1393
|
+
execution,
|
|
1394
|
+
apiVersion: execution.operation.startsWith('production.') ? 'v1' : 'v2',
|
|
1395
|
+
...(downloads ? { downloads } : {}),
|
|
1396
|
+
});
|
|
1397
|
+
process.exitCode = executionExitCode(execution);
|
|
1398
|
+
};
|
|
1048
1399
|
const commandImage = async (subcommand, ctx) => {
|
|
1049
|
-
if (subcommand !== 'generate')
|
|
1400
|
+
if (subcommand !== 'generate')
|
|
1050
1401
|
throw new Error('Unknown image command. Use "image generate".');
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1402
|
+
if (getFlag(ctx.flags, 'api-version') &&
|
|
1403
|
+
getFlag(ctx.flags, 'api-version') !== 'v2')
|
|
1404
|
+
throw new Error('Canvas generation requires --api-version v2');
|
|
1405
|
+
const inputJson = getFlag(ctx.flags, 'input-json');
|
|
1406
|
+
const input = inputJson
|
|
1407
|
+
? await readJsonArgument(inputJson, '--input-json')
|
|
1408
|
+
: {
|
|
1409
|
+
prompt: requireFlag(ctx.flags, 'prompt'),
|
|
1410
|
+
modelId: getFlag(ctx.flags, 'model-id') ?? defaultImageModelId,
|
|
1411
|
+
batchSize: parsePositiveIntegerFlag(ctx.flags, 'batch-size', 1),
|
|
1412
|
+
negativePrompt: getFlag(ctx.flags, 'negative-prompt'),
|
|
1413
|
+
seed: getFlag(ctx.flags, 'seed') == null
|
|
1414
|
+
? undefined
|
|
1415
|
+
: parsePositiveIntegerFlag(ctx.flags, 'seed', 1),
|
|
1416
|
+
steps: getFlag(ctx.flags, 'steps') == null
|
|
1417
|
+
? undefined
|
|
1418
|
+
: parsePositiveIntegerFlag(ctx.flags, 'steps', 1),
|
|
1419
|
+
cfgScale: getFlag(ctx.flags, 'cfg-scale') == null
|
|
1420
|
+
? undefined
|
|
1421
|
+
: Number(getFlag(ctx.flags, 'cfg-scale')),
|
|
1422
|
+
};
|
|
1423
|
+
if (typeof input.prompt !== 'string' || !input.prompt.trim())
|
|
1424
|
+
throw new Error('Image prompt is required');
|
|
1425
|
+
const contextCanvas = getFlag(ctx.flags, 'context') == null
|
|
1426
|
+
? undefined
|
|
1427
|
+
: parsePositiveIntegerFlag(ctx.flags, 'context', 1);
|
|
1428
|
+
const explicitCanvas = selectedCanvasId(ctx.flags);
|
|
1429
|
+
if (contextCanvas != null &&
|
|
1430
|
+
explicitCanvas != null &&
|
|
1431
|
+
contextCanvas !== explicitCanvas)
|
|
1432
|
+
throw new Error('--context must match --canvas');
|
|
1433
|
+
const session = await openExecutionSession({
|
|
1434
|
+
...stateOptions(ctx),
|
|
1435
|
+
canvasId: explicitCanvas ?? contextCanvas,
|
|
1436
|
+
operation: 'image.generate',
|
|
1056
1437
|
});
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1438
|
+
if (contextCanvas != null) {
|
|
1439
|
+
const saved = await ctx.client.v2.getCanvasContext(contextCanvas);
|
|
1440
|
+
input.projectContext = {
|
|
1441
|
+
canvasId: contextCanvas,
|
|
1442
|
+
version: getFlag(ctx.flags, 'context-version') == null
|
|
1443
|
+
? saved.projectContext.version
|
|
1444
|
+
: parsePositiveIntegerFlag(ctx.flags, 'context-version', 1),
|
|
1445
|
+
...(getFlagValues(ctx.flags, 'context-source').length
|
|
1446
|
+
? { sourceKeys: getFlagValues(ctx.flags, 'context-source') }
|
|
1447
|
+
: {}),
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
const moodboardRevisionId = getFlag(ctx.flags, 'moodboard-revision');
|
|
1451
|
+
if (moodboardRevisionId)
|
|
1452
|
+
input.moodboardRevisionId = moodboardRevisionId;
|
|
1453
|
+
const graphInput = await graphSourceInput(ctx, session.canvas.id);
|
|
1454
|
+
const source = graphInput?.source ??
|
|
1455
|
+
(await recordedSource(ctx, inputJson
|
|
1456
|
+
? input.source
|
|
1457
|
+
: await resolveSource({
|
|
1458
|
+
flags: ctx.flags,
|
|
1459
|
+
client: ctx.client,
|
|
1460
|
+
fileMediaType: 'image',
|
|
1461
|
+
preferUploadId: true,
|
|
1462
|
+
})));
|
|
1463
|
+
if (source)
|
|
1464
|
+
input.source = source;
|
|
1465
|
+
if (Array.isArray(input.sources))
|
|
1466
|
+
input.sources = await Promise.all(input.sources.map(source => recordedSource(ctx, source)));
|
|
1467
|
+
await printRecorded(ctx, await executeRecorded(session, 'image.generate', input, { ...executionOptions(ctx.flags), graphSource: graphInput?.graphSource }));
|
|
1087
1468
|
};
|
|
1088
1469
|
const commandMesh = async (subcommand, ctx) => {
|
|
1089
1470
|
if (subcommand === 'generate') {
|
|
1090
|
-
const
|
|
1471
|
+
const inputJson = getFlag(ctx.flags, 'input-json');
|
|
1472
|
+
const input = inputJson
|
|
1473
|
+
? await readJsonArgument(inputJson, '--input-json')
|
|
1474
|
+
: {
|
|
1475
|
+
modelId: getFlag(ctx.flags, 'model-id') ?? defaultMeshModelId,
|
|
1476
|
+
name: getFlag(ctx.flags, 'name'),
|
|
1477
|
+
faceLimit: getFlag(ctx.flags, 'face-limit') == null
|
|
1478
|
+
? undefined
|
|
1479
|
+
: parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
|
|
1480
|
+
isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
|
|
1481
|
+
};
|
|
1482
|
+
const session = await openExecutionSession({
|
|
1483
|
+
...stateOptions(ctx),
|
|
1484
|
+
canvasId: selectedCanvasId(ctx.flags),
|
|
1485
|
+
operation: 'mesh.generate',
|
|
1486
|
+
});
|
|
1487
|
+
const graphInput = await graphSourceInput(ctx, session.canvas.id);
|
|
1488
|
+
const source = graphInput?.source ??
|
|
1489
|
+
(await recordedSource(ctx, inputJson
|
|
1490
|
+
? input.source
|
|
1491
|
+
: await resolveSource({
|
|
1492
|
+
flags: ctx.flags,
|
|
1493
|
+
client: ctx.client,
|
|
1494
|
+
fileMediaType: 'image',
|
|
1495
|
+
preferUploadId: true,
|
|
1496
|
+
})));
|
|
1497
|
+
if (source)
|
|
1498
|
+
input.source = source;
|
|
1499
|
+
if (Array.isArray(input.sources))
|
|
1500
|
+
input.sources = await Promise.all(input.sources.map(source => recordedSource(ctx, source)));
|
|
1501
|
+
if (!input.source && !input.sources)
|
|
1502
|
+
throw new Error(missingSourceMessage);
|
|
1503
|
+
await printRecorded(ctx, await executeRecorded(session, 'mesh.generate', input, { ...executionOptions(ctx.flags), graphSource: graphInput?.graphSource }));
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (subcommand === 'retopo') {
|
|
1507
|
+
const queued = await ctx.client.v2.retopo({
|
|
1091
1508
|
source: await requireSource({
|
|
1092
1509
|
flags: ctx.flags,
|
|
1093
1510
|
client: ctx.client,
|
|
1094
|
-
fileMediaType: '
|
|
1511
|
+
fileMediaType: 'mesh',
|
|
1095
1512
|
}),
|
|
1096
|
-
modelId:
|
|
1513
|
+
modelId: requireFlag(ctx.flags, 'model-id'),
|
|
1097
1514
|
name: getFlag(ctx.flags, 'name'),
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
: parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
|
|
1101
|
-
isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
|
|
1515
|
+
faceLevel: getFlag(ctx.flags, 'face-level'),
|
|
1516
|
+
polygonType: getFlag(ctx.flags, 'polygon-type'),
|
|
1102
1517
|
});
|
|
1103
1518
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1104
1519
|
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1105
1520
|
return;
|
|
1106
1521
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1522
|
+
throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
|
|
1523
|
+
};
|
|
1524
|
+
/**
|
|
1525
|
+
* Accepted values for the rig/animate enum flags, kept next to the commands
|
|
1526
|
+
* that read them. `satisfies` ties each list to the client's own type, so a
|
|
1527
|
+
* new rig type or output format added there fails to compile here rather than
|
|
1528
|
+
* silently going unvalidated.
|
|
1529
|
+
*/
|
|
1530
|
+
const RIG_MODEL_VERSIONS = [
|
|
1531
|
+
'v1.0-20240301',
|
|
1532
|
+
'v2.5-20260210',
|
|
1533
|
+
];
|
|
1534
|
+
const RIG_TYPES = [
|
|
1535
|
+
'biped',
|
|
1536
|
+
'quadruped',
|
|
1537
|
+
'hexapod',
|
|
1538
|
+
'octopod',
|
|
1539
|
+
'avian',
|
|
1540
|
+
'serpentine',
|
|
1541
|
+
'aquatic',
|
|
1542
|
+
];
|
|
1543
|
+
const RIG_SPECS = ['tripo', 'mixamo'];
|
|
1544
|
+
const RIG_OUT_FORMATS = ['glb', 'fbx'];
|
|
1545
|
+
/**
|
|
1546
|
+
* `/animations/retarget` caps `animations` at five server-side. Rejecting the
|
|
1547
|
+
* sixth here means the user sees the limit before a request is built, rather
|
|
1548
|
+
* than a 400 after typing them all out.
|
|
1549
|
+
*/
|
|
1550
|
+
const MAX_RETARGET_ANIMATIONS = 5;
|
|
1551
|
+
/**
|
|
1552
|
+
* The `animate retarget` request, built from flags alone.
|
|
1553
|
+
*
|
|
1554
|
+
* Split out of the command — like `parseRunsUploadArgs` — so the parts worth
|
|
1555
|
+
* testing (the required/at-most-five guards and the single-vs-plural field
|
|
1556
|
+
* choice) need no client or network.
|
|
1557
|
+
*/
|
|
1558
|
+
export const buildAnimationRetargetRequest = (flags) => {
|
|
1559
|
+
const resourceId = requireFlag(flags, 'resource-id');
|
|
1560
|
+
const animations = getFlagValues(flags, 'animation');
|
|
1561
|
+
const [firstAnimation] = animations;
|
|
1562
|
+
if (firstAnimation == null) {
|
|
1563
|
+
throw new Error('Missing required flag: --animation');
|
|
1564
|
+
}
|
|
1565
|
+
if (animations.length > MAX_RETARGET_ANIMATIONS) {
|
|
1566
|
+
throw new Error(`--animation accepts at most ${MAX_RETARGET_ANIMATIONS} values, got ${animations.length}`);
|
|
1567
|
+
}
|
|
1568
|
+
const base = {
|
|
1569
|
+
resourceId,
|
|
1570
|
+
outFormat: parseEnumFlag(flags, 'out-format', RIG_OUT_FORMATS),
|
|
1571
|
+
bakeAnimation: parseBooleanFlag(flags, 'bake-animation'),
|
|
1572
|
+
exportWithGeometry: parseBooleanFlag(flags, 'export-with-geometry'),
|
|
1573
|
+
animateInPlace: parseBooleanFlag(flags, 'animate-in-place'),
|
|
1574
|
+
name: getFlag(flags, 'name'),
|
|
1575
|
+
};
|
|
1576
|
+
// Built per branch rather than spread-then-cast: `animation` and
|
|
1577
|
+
// `animations` are mutually exclusive in the request type, and a cast over
|
|
1578
|
+
// the whole object would stop checking the other six fields too.
|
|
1579
|
+
return animations.length === 1
|
|
1580
|
+
? { ...base, animation: firstAnimation }
|
|
1581
|
+
: { ...base, animations };
|
|
1582
|
+
};
|
|
1583
|
+
/**
|
|
1584
|
+
* Everything `rig create` sends apart from the source, which needs a client to
|
|
1585
|
+
* resolve. Separated for the same reason as `buildAnimationRetargetRequest`:
|
|
1586
|
+
* the flag validation is the part worth testing.
|
|
1587
|
+
*/
|
|
1588
|
+
export const buildRigCreateOptions = (flags) => ({
|
|
1589
|
+
model: parseEnumFlag(flags, 'model', RIG_MODEL_VERSIONS),
|
|
1590
|
+
rigType: parseEnumFlag(flags, 'rig-type', RIG_TYPES),
|
|
1591
|
+
spec: parseEnumFlag(flags, 'spec', RIG_SPECS),
|
|
1592
|
+
outFormat: parseEnumFlag(flags, 'out-format', RIG_OUT_FORMATS),
|
|
1593
|
+
name: getFlag(flags, 'name'),
|
|
1594
|
+
});
|
|
1595
|
+
const commandRig = async (subcommand, ctx) => {
|
|
1596
|
+
if (subcommand === 'check') {
|
|
1597
|
+
const queued = await ctx.client.v2.checkRig({
|
|
1109
1598
|
source: await requireSource({
|
|
1110
1599
|
flags: ctx.flags,
|
|
1111
1600
|
client: ctx.client,
|
|
1112
1601
|
fileMediaType: 'mesh',
|
|
1113
1602
|
}),
|
|
1114
|
-
modelId: requireFlag(ctx.flags, 'model-id'),
|
|
1115
1603
|
name: getFlag(ctx.flags, 'name'),
|
|
1116
|
-
faceLevel: getFlag(ctx.flags, 'face-level'),
|
|
1117
|
-
polygonType: getFlag(ctx.flags, 'polygon-type'),
|
|
1118
1604
|
});
|
|
1119
1605
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1120
1606
|
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1121
1607
|
return;
|
|
1122
1608
|
}
|
|
1123
|
-
|
|
1609
|
+
if (subcommand === 'create') {
|
|
1610
|
+
const queued = await ctx.client.v2.rigMesh({
|
|
1611
|
+
source: await requireSource({
|
|
1612
|
+
flags: ctx.flags,
|
|
1613
|
+
client: ctx.client,
|
|
1614
|
+
fileMediaType: 'mesh',
|
|
1615
|
+
}),
|
|
1616
|
+
...buildRigCreateOptions(ctx.flags),
|
|
1617
|
+
});
|
|
1618
|
+
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1619
|
+
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
throw new Error('Unknown rig command. Use "rig check" or "rig create".');
|
|
1623
|
+
};
|
|
1624
|
+
const commandAnimate = async (subcommand, ctx) => {
|
|
1625
|
+
if (subcommand === 'presets') {
|
|
1626
|
+
const result = await ctx.client.v2.listAnimationPresets({
|
|
1627
|
+
model: parseEnumFlag(ctx.flags, 'model', RIG_MODEL_VERSIONS),
|
|
1628
|
+
rigType: parseEnumFlag(ctx.flags, 'rig-type', RIG_TYPES),
|
|
1629
|
+
});
|
|
1630
|
+
print(result);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (subcommand === 'retarget') {
|
|
1634
|
+
const queued = await ctx.client.v2.retargetAnimation(buildAnimationRetargetRequest(ctx.flags));
|
|
1635
|
+
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1636
|
+
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
throw new Error('Unknown animate command. Use "animate presets" or "animate retarget".');
|
|
1124
1640
|
};
|
|
1125
1641
|
const commandJobs = async (subcommand, positionals, ctx) => {
|
|
1126
1642
|
const jobId = requirePositional(positionals, 2, 'job-id');
|
|
@@ -1311,14 +1827,17 @@ const firstBlobLocation = (value) => {
|
|
|
1311
1827
|
return undefined;
|
|
1312
1828
|
}
|
|
1313
1829
|
const record = value;
|
|
1314
|
-
if (record.type === 'supabase' &&
|
|
1830
|
+
if (record.type === 'supabase' &&
|
|
1831
|
+
typeof record.bucket === 'string' &&
|
|
1832
|
+
typeof record.path === 'string') {
|
|
1315
1833
|
return {
|
|
1316
1834
|
type: 'supabase',
|
|
1317
1835
|
bucket: record.bucket,
|
|
1318
1836
|
path: record.path,
|
|
1319
1837
|
};
|
|
1320
1838
|
}
|
|
1321
|
-
if (record.type === 'signedSupabase' &&
|
|
1839
|
+
if (record.type === 'signedSupabase' &&
|
|
1840
|
+
typeof record.signedUrl === 'string') {
|
|
1322
1841
|
return {
|
|
1323
1842
|
type: 'signedSupabase',
|
|
1324
1843
|
signedUrl: record.signedUrl,
|
|
@@ -1342,7 +1861,13 @@ const imageSourceFromCompletedJob = (job) => {
|
|
|
1342
1861
|
if (job.resourceId != null && job.resourceId.trim() !== '') {
|
|
1343
1862
|
return { imageAssetId: job.resourceId };
|
|
1344
1863
|
}
|
|
1345
|
-
const resourceId = firstStringValueForKeys(job, new Set([
|
|
1864
|
+
const resourceId = firstStringValueForKeys(job, new Set([
|
|
1865
|
+
'resourceId',
|
|
1866
|
+
'resource_id',
|
|
1867
|
+
'assetId',
|
|
1868
|
+
'asset_id',
|
|
1869
|
+
'imageAssetId',
|
|
1870
|
+
]));
|
|
1346
1871
|
if (resourceId != null) {
|
|
1347
1872
|
return { imageAssetId: resourceId };
|
|
1348
1873
|
}
|
|
@@ -1357,6 +1882,14 @@ const imageSourceFromCompletedJob = (job) => {
|
|
|
1357
1882
|
throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
|
|
1358
1883
|
};
|
|
1359
1884
|
const errorToOutput = (error) => {
|
|
1885
|
+
if (error instanceof CliExecutionError)
|
|
1886
|
+
return {
|
|
1887
|
+
message: error.message,
|
|
1888
|
+
exitCode: error.exitCode,
|
|
1889
|
+
operationId: error.operationId,
|
|
1890
|
+
runId: error.runId,
|
|
1891
|
+
execution: error.execution,
|
|
1892
|
+
};
|
|
1360
1893
|
if (error instanceof AssetHubApiError) {
|
|
1361
1894
|
return {
|
|
1362
1895
|
name: error.name,
|
|
@@ -1381,7 +1914,9 @@ const readyTaskIdsFromStatus = (status) => {
|
|
|
1381
1914
|
return [];
|
|
1382
1915
|
}
|
|
1383
1916
|
return missions.flatMap(mission => {
|
|
1384
|
-
if (mission == null ||
|
|
1917
|
+
if (mission == null ||
|
|
1918
|
+
typeof mission !== 'object' ||
|
|
1919
|
+
!('tasks' in mission)) {
|
|
1385
1920
|
return [];
|
|
1386
1921
|
}
|
|
1387
1922
|
const tasks = mission.tasks;
|
|
@@ -1415,21 +1950,203 @@ const singleMissionIdFromStatus = (status) => {
|
|
|
1415
1950
|
}
|
|
1416
1951
|
return missionId;
|
|
1417
1952
|
};
|
|
1953
|
+
/**
|
|
1954
|
+
* Flag forms for the intervention ops, one per contract op.
|
|
1955
|
+
*
|
|
1956
|
+
* `params.set` has no `--params-set`: parameters arrive as repeated
|
|
1957
|
+
* `--set-param key=value` and merge into a single op (see below).
|
|
1958
|
+
*/
|
|
1959
|
+
const interventionOpFlags = [
|
|
1960
|
+
'add-part',
|
|
1961
|
+
'exclude',
|
|
1962
|
+
'include',
|
|
1963
|
+
'regenerate',
|
|
1964
|
+
'reject',
|
|
1965
|
+
'rename',
|
|
1966
|
+
'set-param',
|
|
1967
|
+
];
|
|
1968
|
+
const isInterventionOpFlag = (name) => interventionOpFlags.includes(name);
|
|
1969
|
+
/** Splits on the FIRST `=` so a value may itself contain one. */
|
|
1970
|
+
const splitOnFirstEquals = (value) => {
|
|
1971
|
+
const index = value.indexOf('=');
|
|
1972
|
+
return index === -1
|
|
1973
|
+
? [value, undefined]
|
|
1974
|
+
: [value.slice(0, index), value.slice(index + 1)];
|
|
1975
|
+
};
|
|
1976
|
+
const requireOpValue = (flag, value, shape) => {
|
|
1977
|
+
if (value == null || value.trim().length === 0) {
|
|
1978
|
+
throw new Error(`--${flag} requires a value: --${flag} ${shape}`);
|
|
1979
|
+
}
|
|
1980
|
+
return value;
|
|
1981
|
+
};
|
|
1982
|
+
/**
|
|
1983
|
+
* Builds an intervention batch from the raw argument slice.
|
|
1984
|
+
*
|
|
1985
|
+
* Raw argv rather than the parsed `Flags` record on purpose: that record is
|
|
1986
|
+
* keyed by flag name, so it can report every `--exclude` but not whether one of
|
|
1987
|
+
* them preceded an `--add-part`. The batch's order becomes the log's `seq`
|
|
1988
|
+
* order, which is the order the operator made the decisions in, so it has to
|
|
1989
|
+
* survive parsing.
|
|
1990
|
+
*
|
|
1991
|
+
* Repeated `--set-param` flags merge into ONE `params.set` op, emitted at the
|
|
1992
|
+
* position of the first of them. Two parameters set in the same invocation are
|
|
1993
|
+
* one decision; writing them as two log rows would make the log claim the
|
|
1994
|
+
* operator changed their mind halfway through.
|
|
1995
|
+
*
|
|
1996
|
+
* Values are not validated against the contract's key set. The CLI carries no
|
|
1997
|
+
* zod and must not carry a second copy of the contract either -- an unknown
|
|
1998
|
+
* param key is the server's 400 to give, from the one place the contract is
|
|
1999
|
+
* enforced. Only the shape this function itself has to parse is checked here.
|
|
2000
|
+
*/
|
|
2001
|
+
export const interventionOpsFromArgs = (args) => {
|
|
2002
|
+
const ops = [];
|
|
2003
|
+
let paramsOp = null;
|
|
2004
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2005
|
+
const arg = args[index];
|
|
2006
|
+
if (!arg.startsWith('--'))
|
|
2007
|
+
continue;
|
|
2008
|
+
const [rawName, inlineValue] = splitOnFirstEquals(arg.slice(2));
|
|
2009
|
+
const flag = rawName.trim();
|
|
2010
|
+
if (!isInterventionOpFlag(flag))
|
|
2011
|
+
continue;
|
|
2012
|
+
const next = args[index + 1];
|
|
2013
|
+
const value = inlineValue != null
|
|
2014
|
+
? inlineValue
|
|
2015
|
+
: next != null && !next.startsWith('--')
|
|
2016
|
+
? ((index += 1), next)
|
|
2017
|
+
: undefined;
|
|
2018
|
+
if (flag === 'add-part') {
|
|
2019
|
+
ops.push({ op: 'part.add', name: requireOpValue(flag, value, '<name>') });
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
if (flag === 'exclude' || flag === 'include') {
|
|
2023
|
+
ops.push({
|
|
2024
|
+
op: flag === 'exclude' ? 'part.exclude' : 'part.include',
|
|
2025
|
+
targetId: requireOpValue(flag, value, '<target-id>'),
|
|
2026
|
+
});
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
if (flag === 'rename') {
|
|
2030
|
+
const [targetId, name] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>=<name>'));
|
|
2031
|
+
if (targetId.trim().length === 0 ||
|
|
2032
|
+
name == null ||
|
|
2033
|
+
name.trim().length === 0) {
|
|
2034
|
+
throw new Error('--rename requires --rename <target-id>=<name>');
|
|
2035
|
+
}
|
|
2036
|
+
ops.push({ op: 'part.rename', targetId, name });
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (flag === 'regenerate') {
|
|
2040
|
+
const [targetId, mode] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>=<mode>'));
|
|
2041
|
+
if (targetId.trim().length === 0 ||
|
|
2042
|
+
mode == null ||
|
|
2043
|
+
mode.trim().length === 0) {
|
|
2044
|
+
throw new Error('--regenerate requires --regenerate <target-id>=<mode>');
|
|
2045
|
+
}
|
|
2046
|
+
ops.push({ op: 'part.regenerate', targetId, mode });
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
if (flag === 'reject') {
|
|
2050
|
+
const [targetId, reason] = splitOnFirstEquals(requireOpValue(flag, value, '<target-id>[=<reason>]'));
|
|
2051
|
+
if (targetId.trim().length === 0) {
|
|
2052
|
+
throw new Error('--reject requires --reject <target-id>[=<reason>]');
|
|
2053
|
+
}
|
|
2054
|
+
// `--reject 7=` is a typo, not "no reason": the contract rejects a blank
|
|
2055
|
+
// one, so it is refused here rather than round-tripped for a 400.
|
|
2056
|
+
if (reason != null && reason.trim().length === 0) {
|
|
2057
|
+
throw new Error('--reject reason must not be blank');
|
|
2058
|
+
}
|
|
2059
|
+
ops.push(reason == null
|
|
2060
|
+
? { op: 'part.reject', targetId }
|
|
2061
|
+
: { op: 'part.reject', targetId, reason });
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
const [key, paramValue] = splitOnFirstEquals(requireOpValue(flag, value, '<key>=<value>'));
|
|
2065
|
+
if (key.trim().length === 0 ||
|
|
2066
|
+
paramValue == null ||
|
|
2067
|
+
paramValue.trim().length === 0) {
|
|
2068
|
+
throw new Error('--set-param requires --set-param <key>=<value>');
|
|
2069
|
+
}
|
|
2070
|
+
if (paramsOp == null) {
|
|
2071
|
+
paramsOp = { op: 'params.set', patch: {} };
|
|
2072
|
+
// Cast because the patch is intentionally open here: the contract's key
|
|
2073
|
+
// set is enforced server-side, in one place, not duplicated in the CLI.
|
|
2074
|
+
ops.push(paramsOp);
|
|
2075
|
+
}
|
|
2076
|
+
paramsOp.patch[key] = paramValue;
|
|
2077
|
+
}
|
|
2078
|
+
return ops;
|
|
2079
|
+
};
|
|
2080
|
+
const readOpsJsonFlag = async (flags) => {
|
|
2081
|
+
const value = getFlag(flags, 'ops-json');
|
|
2082
|
+
if (value == null || value === 'true' || value.trim() === '') {
|
|
2083
|
+
throw new Error('Missing required flag value: --ops-json <json|@file|@->');
|
|
2084
|
+
}
|
|
2085
|
+
const raw = value === '@-' || value === '-'
|
|
2086
|
+
? await readStdin()
|
|
2087
|
+
: value.startsWith('@')
|
|
2088
|
+
? await readFile(value.slice(1), 'utf8')
|
|
2089
|
+
: value;
|
|
2090
|
+
const parsed = parseJsonValue(raw, '--ops-json');
|
|
2091
|
+
if (!Array.isArray(parsed)) {
|
|
2092
|
+
throw new Error('--ops-json must be a JSON array of intervention ops');
|
|
2093
|
+
}
|
|
2094
|
+
return parsed;
|
|
2095
|
+
};
|
|
2096
|
+
const productionSession = (ctx, operation) => openExecutionSession({
|
|
2097
|
+
...stateOptions(ctx),
|
|
2098
|
+
canvasId: selectedCanvasId(ctx.flags),
|
|
2099
|
+
operation,
|
|
2100
|
+
});
|
|
2101
|
+
const childOperationId = (parent, step) => {
|
|
2102
|
+
const hash = createHash('sha256')
|
|
2103
|
+
.update(JSON.stringify([parent, step]))
|
|
2104
|
+
.digest('hex');
|
|
2105
|
+
return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
2106
|
+
};
|
|
1418
2107
|
const commandProduction = async (subcommand, positionals, ctx) => {
|
|
2108
|
+
if (subcommand === 'agents') {
|
|
2109
|
+
print(sanitizeProductionOutput(await ctx.client.v1.getProductionAgents()));
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
1419
2112
|
if (subcommand === 'analyze') {
|
|
1420
|
-
const
|
|
2113
|
+
const session = await productionSession(ctx, 'production.analyze');
|
|
2114
|
+
const analyzed = await executeRecorded(session, 'production.analyze', {
|
|
1421
2115
|
...(await resolveProductionAnalyzeSource(ctx)),
|
|
1422
2116
|
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1423
2117
|
name: getFlag(ctx.flags, 'name'),
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
2118
|
+
}, executionOptions(ctx.flags));
|
|
2119
|
+
await printRecorded(ctx, analyzed);
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (subcommand === 'automation') {
|
|
2123
|
+
const session = await productionSession(ctx, 'production.automation');
|
|
2124
|
+
const input = getFlag(ctx.flags, 'input-json');
|
|
2125
|
+
const body = input
|
|
2126
|
+
? await readJsonArgument(input, '--input-json')
|
|
2127
|
+
: {
|
|
2128
|
+
images: [await resolveProductionAnalyzeSource(ctx)],
|
|
2129
|
+
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
2130
|
+
name: getFlag(ctx.flags, 'name'),
|
|
2131
|
+
};
|
|
2132
|
+
if (!Array.isArray(body.images) || !body.images.length)
|
|
2133
|
+
throw new Error('production automation requires non-empty images[]');
|
|
2134
|
+
const images = await Promise.all(body.images.map(async (image) => {
|
|
2135
|
+
if (!image || typeof image !== 'object')
|
|
2136
|
+
throw new Error('Invalid automation image source');
|
|
2137
|
+
return productionAnalyzeSourceFromSource((await recordedSource(ctx, sourceFromProductionAnalyzeSource(image))));
|
|
2138
|
+
}));
|
|
2139
|
+
const queued = await executeRecorded(session, 'production.automation', {
|
|
2140
|
+
...body,
|
|
2141
|
+
images,
|
|
2142
|
+
agentVersion: body.agentVersion ?? resolvePartExtractorForAnalyze(ctx.flags),
|
|
2143
|
+
...(getFlag(ctx.flags, 'max-cost-credits')
|
|
2144
|
+
? {
|
|
2145
|
+
maxCostCredits: parsePositiveIntegerFlag(ctx.flags, 'max-cost-credits', 1),
|
|
2146
|
+
}
|
|
2147
|
+
: {}),
|
|
2148
|
+
}, executionOptions(ctx.flags));
|
|
2149
|
+
await printRecorded(ctx, queued);
|
|
1433
2150
|
return;
|
|
1434
2151
|
}
|
|
1435
2152
|
if (subcommand === 'status') {
|
|
@@ -1464,41 +2181,182 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
1464
2181
|
const agentVersion = partExtractorFlag == null
|
|
1465
2182
|
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1466
2183
|
: resolvePartExtractorApiValue(partExtractorFlag);
|
|
1467
|
-
const
|
|
2184
|
+
const session = await productionSession(ctx, 'production.execute');
|
|
2185
|
+
const executed = await executeRecorded(session, 'production.execute', {
|
|
1468
2186
|
orderId,
|
|
1469
2187
|
missionId,
|
|
1470
2188
|
confirmedTaskIds,
|
|
1471
2189
|
agentVersion,
|
|
1472
2190
|
partExtractionMode,
|
|
2191
|
+
}, executionOptions(ctx.flags));
|
|
2192
|
+
await printRecorded(ctx, executed);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
if (subcommand === 'run') {
|
|
2196
|
+
const orderId = requireFlag(ctx.flags, 'order-id');
|
|
2197
|
+
const missionId = requireFlag(ctx.flags, 'mission-id');
|
|
2198
|
+
const runMode = parseRunModeFlag(ctx.flags);
|
|
2199
|
+
const partExtractionMode = getFlag(ctx.flags, 'part-extraction-mode');
|
|
2200
|
+
if (partExtractionMode != null &&
|
|
2201
|
+
partExtractionMode !== 'fast' &&
|
|
2202
|
+
partExtractionMode !== 'high_quality') {
|
|
2203
|
+
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
2204
|
+
}
|
|
2205
|
+
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
2206
|
+
const agentVersion = partExtractorFlag == null
|
|
2207
|
+
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
2208
|
+
: resolvePartExtractorApiValue(partExtractorFlag);
|
|
2209
|
+
const confirmedTaskIds = getFlagValues(ctx.flags, 'task-id');
|
|
2210
|
+
const allowedModelIds = getFlagValues(ctx.flags, 'allowed-model-id');
|
|
2211
|
+
const maxIterations = getFlag(ctx.flags, 'max-iterations') == null
|
|
2212
|
+
? undefined
|
|
2213
|
+
: parsePositiveIntegerFlag(ctx.flags, 'max-iterations', 1);
|
|
2214
|
+
const maxCostCredits = getFlag(ctx.flags, 'max-cost-credits') == null
|
|
2215
|
+
? undefined
|
|
2216
|
+
: parsePositiveIntegerFlag(ctx.flags, 'max-cost-credits', 1);
|
|
2217
|
+
const started = await ctx.client.v1.runProduction({
|
|
2218
|
+
orderId,
|
|
2219
|
+
missionId,
|
|
2220
|
+
agentVersion,
|
|
2221
|
+
runMode,
|
|
2222
|
+
...(confirmedTaskIds.length > 0 ? { confirmedTaskIds } : {}),
|
|
2223
|
+
...(allowedModelIds.length > 0 ? { allowedModelIds } : {}),
|
|
2224
|
+
...(partExtractionMode != null
|
|
2225
|
+
? { partExtractionMode: partExtractionMode }
|
|
2226
|
+
: {}),
|
|
2227
|
+
...(maxIterations != null ? { maxIterations } : {}),
|
|
2228
|
+
...(maxCostCredits != null ? { maxCostCredits } : {}),
|
|
1473
2229
|
});
|
|
1474
2230
|
const status = hasFlag(ctx.flags, 'wait')
|
|
1475
|
-
? await
|
|
1476
|
-
ctx,
|
|
1477
|
-
orderId,
|
|
1478
|
-
terminalStatuses: productionExecuteTerminalStatuses,
|
|
1479
|
-
})
|
|
2231
|
+
? await watchProductionRun({ ctx, orderId })
|
|
1480
2232
|
: undefined;
|
|
1481
|
-
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({
|
|
2233
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ started, status })));
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
if (subcommand === 'interventions') {
|
|
2237
|
+
const orderId = requirePositional(positionals, 2, 'order-id');
|
|
2238
|
+
print(await ctx.client.v2.listInterventions(orderId));
|
|
1482
2239
|
return;
|
|
1483
2240
|
}
|
|
1484
|
-
|
|
2241
|
+
if (subcommand === 'intervene') {
|
|
2242
|
+
const orderId = requirePositional(positionals, 2, 'order-id');
|
|
2243
|
+
const usesOpsJson = hasFlag(ctx.flags, 'ops-json');
|
|
2244
|
+
const flagOps = interventionOpsFromArgs(ctx.args);
|
|
2245
|
+
// Refused rather than concatenated: with both present the batch's order
|
|
2246
|
+
// would depend on which source the CLI happened to read first, and that
|
|
2247
|
+
// order becomes the log's seq order.
|
|
2248
|
+
if (usesOpsJson && flagOps.length > 0) {
|
|
2249
|
+
throw new Error('Use either --ops-json or the individual op flags, not both.');
|
|
2250
|
+
}
|
|
2251
|
+
const interventions = usesOpsJson
|
|
2252
|
+
? await readOpsJsonFlag(ctx.flags)
|
|
2253
|
+
: flagOps;
|
|
2254
|
+
if (interventions.length === 0) {
|
|
2255
|
+
throw new Error('No interventions given. Use --exclude/--include/--add-part/--rename/--reject/--regenerate/--set-param, or --ops-json.');
|
|
2256
|
+
}
|
|
2257
|
+
const idempotencyKey = getFlag(ctx.flags, 'idempotency-key');
|
|
2258
|
+
const appended = await ctx.client.v2.appendInterventions(orderId, interventions,
|
|
2259
|
+
// Omitted rather than passed as undefined so the request carries no header
|
|
2260
|
+
// at all; the server then reports `idempotent: false` back.
|
|
2261
|
+
idempotencyKey == null ? {} : { idempotencyKey });
|
|
2262
|
+
print({ appended, interventions });
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (subcommand === 'watch') {
|
|
2266
|
+
const orderId = requirePositional(positionals, 2, 'order-id');
|
|
2267
|
+
const status = await watchProductionRun({ ctx, orderId });
|
|
2268
|
+
print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ status })));
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
throw new Error('Unknown production command. Use "production agents", "production analyze", "production automation", "production run", "production watch", "production status", "production execute", "production intervene", or "production interventions".');
|
|
2272
|
+
};
|
|
2273
|
+
// Multi-phase commands may contain several receipts; only their durable outputs
|
|
2274
|
+
// are downloadable, never canvas links, source URLs or evaluation evidence URLs.
|
|
2275
|
+
const recordedOutputs = (value) => {
|
|
2276
|
+
if (!value || typeof value !== 'object')
|
|
2277
|
+
return [];
|
|
2278
|
+
if (Array.isArray(value))
|
|
2279
|
+
return value.flatMap(recordedOutputs);
|
|
2280
|
+
return Object.entries(value).flatMap(([key, item]) => {
|
|
2281
|
+
if (key === 'execution' &&
|
|
2282
|
+
item &&
|
|
2283
|
+
typeof item === 'object' &&
|
|
2284
|
+
'outputs' in item &&
|
|
2285
|
+
Array.isArray(item.outputs))
|
|
2286
|
+
return item.outputs;
|
|
2287
|
+
return recordedOutputs(item);
|
|
2288
|
+
});
|
|
1485
2289
|
};
|
|
2290
|
+
const isArtifactGraphPartSplit = (value, execution) => (value != null &&
|
|
2291
|
+
typeof value === 'object' &&
|
|
2292
|
+
'engine' in value &&
|
|
2293
|
+
value.engine === 'artifact-graph') ||
|
|
2294
|
+
execution.resolvedInput?.agentVersion === artifactGraphPartExtractorApiValue;
|
|
1486
2295
|
const commandParts = async (subcommand, ctx) => {
|
|
1487
|
-
const runPartSplit = async (source) => {
|
|
2296
|
+
const runPartSplit = async (source, operationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID()) => {
|
|
1488
2297
|
let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
|
|
1489
2298
|
if (orderId != null) {
|
|
1490
2299
|
orderId = assertFlagHasValue(orderId, '--order-id <id>');
|
|
1491
2300
|
}
|
|
2301
|
+
const partExtractorFlag = getPartExtractorFlag(ctx.flags);
|
|
2302
|
+
const requestedAgentVersion = orderId == null || partExtractorFlag != null
|
|
2303
|
+
? resolvePartExtractorApiValue(partExtractorFlag ?? defaultPartExtractorName)
|
|
2304
|
+
: undefined;
|
|
2305
|
+
if (requestedAgentVersion === artifactGraphPartExtractorApiValue) {
|
|
2306
|
+
for (const flag of ['task-id', 'mission-id', 'part-extraction-mode']) {
|
|
2307
|
+
if (hasFlag(ctx.flags, flag))
|
|
2308
|
+
throw new Error(`--${flag} is not supported with V3.6.1`);
|
|
2309
|
+
}
|
|
2310
|
+
if (orderId != null)
|
|
2311
|
+
throw new Error('--order-id is not supported with V3.6.1; use runs resume <operation-id> to resume a graph split');
|
|
2312
|
+
}
|
|
2313
|
+
const session = await productionSession(ctx, orderId == null ? 'production.analyze' : 'production.execute');
|
|
2314
|
+
let execution;
|
|
1492
2315
|
let analyzed;
|
|
1493
2316
|
let status;
|
|
1494
2317
|
if (orderId == null) {
|
|
1495
|
-
const result = await
|
|
2318
|
+
const result = await executeRecorded(session, 'production.analyze', {
|
|
1496
2319
|
...(source ?? (await resolveProductionAnalyzeSource(ctx))),
|
|
1497
2320
|
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1498
2321
|
name: getFlag(ctx.flags, 'name'),
|
|
1499
|
-
});
|
|
2322
|
+
}, { ...executionOptions(ctx.flags), operationId });
|
|
1500
2323
|
analyzed = result;
|
|
1501
|
-
|
|
2324
|
+
execution = result.execution;
|
|
2325
|
+
if (isArtifactGraphPartSplit(result, execution)) {
|
|
2326
|
+
stderr.write(`[run] ${execution.runId} graph=${execution.orderIds[0] ?? execution.runId}\n`);
|
|
2327
|
+
if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready'))
|
|
2328
|
+
execution = await waitForExecution(ctx.client, execution.runId, watchOptions(ctx.flags));
|
|
2329
|
+
process.exitCode = executionExitCode(execution);
|
|
2330
|
+
return sanitizeProductionOutput({ execution, analyzed });
|
|
2331
|
+
}
|
|
2332
|
+
orderId = execution.orderIds[0];
|
|
2333
|
+
if (!orderId)
|
|
2334
|
+
throw new CliExecutionError('Analysis receipt omitted its order', 3, operationId, execution);
|
|
2335
|
+
stderr.write(`[run] ${execution.runId} order=${orderId}\n`);
|
|
2336
|
+
const executeOperationId = childOperationId(operationId, 'execute');
|
|
2337
|
+
if ((hasFlag(ctx.flags, 'all-ready') ||
|
|
2338
|
+
getFlagValues(ctx.flags, 'task-id').length > 0) &&
|
|
2339
|
+
(await hasRecordedOperation(session.stateDir, executeOperationId))) {
|
|
2340
|
+
const resumed = await resumeRecorded({
|
|
2341
|
+
...stateOptions(ctx),
|
|
2342
|
+
operationId: executeOperationId,
|
|
2343
|
+
expectedCanvasId: session.canvas.id,
|
|
2344
|
+
expectedBody: {
|
|
2345
|
+
partExtractionMode: getFlag(ctx.flags, 'part-extraction-mode'),
|
|
2346
|
+
...(getFlagValues(ctx.flags, 'task-id').length
|
|
2347
|
+
? { confirmedTaskIds: getFlagValues(ctx.flags, 'task-id') }
|
|
2348
|
+
: {}),
|
|
2349
|
+
...(getFlag(ctx.flags, 'mission-id')
|
|
2350
|
+
? { missionId: getFlag(ctx.flags, 'mission-id') }
|
|
2351
|
+
: {}),
|
|
2352
|
+
},
|
|
2353
|
+
});
|
|
2354
|
+
const execution = hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')
|
|
2355
|
+
? await waitForExecution(ctx.client, resumed.execution.runId, watchOptions(ctx.flags))
|
|
2356
|
+
: resumed.execution;
|
|
2357
|
+
process.exitCode = executionExitCode(execution);
|
|
2358
|
+
return { execution, executed: sanitizeProductionOutput(resumed) };
|
|
2359
|
+
}
|
|
1502
2360
|
if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1503
2361
|
status = await pollProductionStatus({
|
|
1504
2362
|
ctx,
|
|
@@ -1541,13 +2399,21 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1541
2399
|
partExtractionMode !== 'high_quality') {
|
|
1542
2400
|
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
1543
2401
|
}
|
|
1544
|
-
|
|
2402
|
+
const result = await executeRecorded(session, 'production.execute', {
|
|
1545
2403
|
orderId,
|
|
1546
2404
|
missionId,
|
|
1547
2405
|
confirmedTaskIds: taskIds,
|
|
1548
2406
|
agentVersion,
|
|
1549
2407
|
partExtractionMode,
|
|
2408
|
+
}, {
|
|
2409
|
+
...executionOptions(ctx.flags),
|
|
2410
|
+
operationId: execution
|
|
2411
|
+
? childOperationId(operationId, 'execute')
|
|
2412
|
+
: operationId,
|
|
2413
|
+
parentRunId: execution?.runId ?? executionOptions(ctx.flags).parentRunId,
|
|
1550
2414
|
});
|
|
2415
|
+
executed = result;
|
|
2416
|
+
execution = result.execution;
|
|
1551
2417
|
if (hasFlag(ctx.flags, 'wait')) {
|
|
1552
2418
|
executionStatus = await pollProductionStatus({
|
|
1553
2419
|
ctx,
|
|
@@ -1556,7 +2422,14 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1556
2422
|
});
|
|
1557
2423
|
}
|
|
1558
2424
|
}
|
|
2425
|
+
if (execution &&
|
|
2426
|
+
(hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready'))) {
|
|
2427
|
+
execution = await waitForExecution(ctx.client, execution.runId, watchOptions(ctx.flags));
|
|
2428
|
+
}
|
|
2429
|
+
if (execution)
|
|
2430
|
+
process.exitCode = executionExitCode(execution);
|
|
1559
2431
|
return sanitizeProductionOutput({
|
|
2432
|
+
execution,
|
|
1560
2433
|
analyzed,
|
|
1561
2434
|
status,
|
|
1562
2435
|
executed,
|
|
@@ -1564,10 +2437,13 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1564
2437
|
});
|
|
1565
2438
|
};
|
|
1566
2439
|
if (subcommand === 'split') {
|
|
1567
|
-
if (getFlag(ctx.flags, 'order-id') != null &&
|
|
2440
|
+
if (getFlag(ctx.flags, 'order-id') != null &&
|
|
2441
|
+
sourceInputCount(ctx.flags) > 0) {
|
|
1568
2442
|
throw new Error('Pass either --order-id or one image source, not both.');
|
|
1569
2443
|
}
|
|
1570
|
-
|
|
2444
|
+
const result = await runPartSplit();
|
|
2445
|
+
const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
|
|
2446
|
+
print({ ...result, ...(downloads ? { downloads } : {}) });
|
|
1571
2447
|
return;
|
|
1572
2448
|
}
|
|
1573
2449
|
if (subcommand === 'compare') {
|
|
@@ -1578,7 +2454,8 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1578
2454
|
if (preprocessPrompt === 'true') {
|
|
1579
2455
|
throw new Error('Missing required flag value: --preprocess-prompt <text>');
|
|
1580
2456
|
}
|
|
1581
|
-
if (preprocessPrompt != null &&
|
|
2457
|
+
if (preprocessPrompt != null &&
|
|
2458
|
+
getFlagValues(ctx.flags, 'task-id').length > 0) {
|
|
1582
2459
|
throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
|
|
1583
2460
|
}
|
|
1584
2461
|
if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
|
|
@@ -1588,17 +2465,22 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1588
2465
|
if (preprocessModelId === 'true') {
|
|
1589
2466
|
throw new Error('Missing required flag value: --preprocess-model-id <id>');
|
|
1590
2467
|
}
|
|
2468
|
+
const compareSession = await productionSession(ctx, 'production.analyze');
|
|
2469
|
+
const compareOperationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
|
|
1591
2470
|
const inputSource = productionAnalyzeSourceFromSource(await requireSource({
|
|
1592
2471
|
flags: ctx.flags,
|
|
1593
2472
|
client: ctx.client,
|
|
1594
2473
|
fileMediaType: 'image',
|
|
2474
|
+
preferUploadId: true,
|
|
1595
2475
|
}));
|
|
1596
|
-
const
|
|
2476
|
+
const durableInput = productionAnalyzeSourceFromSource((await recordedSource(ctx, sourceFromProductionAnalyzeSource(inputSource))));
|
|
2477
|
+
const direct = await runPartSplit(durableInput, compareOperationId);
|
|
1597
2478
|
let preprocessed = null;
|
|
1598
2479
|
if (preprocessPrompt != null && preprocessPrompt !== 'true') {
|
|
2480
|
+
const preprocessOperationId = childOperationId(compareOperationId, 'preprocess');
|
|
1599
2481
|
try {
|
|
1600
|
-
const queued = await
|
|
1601
|
-
source: sourceFromProductionAnalyzeSource(
|
|
2482
|
+
const queued = await executeRecorded(compareSession, 'image.generate', {
|
|
2483
|
+
source: sourceFromProductionAnalyzeSource(durableInput),
|
|
1602
2484
|
prompt: preprocessPrompt,
|
|
1603
2485
|
modelId: preprocessModelId ?? defaultImageModelId,
|
|
1604
2486
|
batchSize: 1,
|
|
@@ -1606,8 +2488,15 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1606
2488
|
seed: getFlag(ctx.flags, 'preprocess-seed') == null
|
|
1607
2489
|
? undefined
|
|
1608
2490
|
: parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
|
|
2491
|
+
}, {
|
|
2492
|
+
...executionOptions(ctx.flags),
|
|
2493
|
+
operationId: preprocessOperationId,
|
|
1609
2494
|
});
|
|
1610
|
-
const
|
|
2495
|
+
const completed = await waitForExecution(ctx.client, queued.execution.runId, watchOptions(ctx.flags));
|
|
2496
|
+
const jobId = completed.jobIds[0];
|
|
2497
|
+
if (!jobId)
|
|
2498
|
+
throw new CliExecutionError('Preprocessing omitted its job', 3, undefined, completed);
|
|
2499
|
+
const job = await ctx.client.v2.pollJob(jobId, {
|
|
1611
2500
|
intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
|
|
1612
2501
|
timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
|
|
1613
2502
|
onPoll: current => {
|
|
@@ -1625,27 +2514,445 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1625
2514
|
job,
|
|
1626
2515
|
source: generatedSource,
|
|
1627
2516
|
},
|
|
1628
|
-
split: await runPartSplit(generatedSource),
|
|
2517
|
+
split: await runPartSplit(generatedSource, childOperationId(compareOperationId, 'preprocessed-split')),
|
|
1629
2518
|
};
|
|
1630
2519
|
}
|
|
1631
2520
|
catch (error) {
|
|
2521
|
+
const failure = error instanceof CliExecutionError ||
|
|
2522
|
+
activeExecution.operationId !== preprocessOperationId
|
|
2523
|
+
? error
|
|
2524
|
+
: new CliExecutionError(error instanceof Error ? error.message : String(error), 1, preprocessOperationId, activeExecution.execution, activeExecution.runId);
|
|
1632
2525
|
if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
|
|
1633
|
-
throw
|
|
2526
|
+
throw failure;
|
|
1634
2527
|
}
|
|
2528
|
+
process.exitCode =
|
|
2529
|
+
failure instanceof CliExecutionError ? failure.exitCode : 1;
|
|
1635
2530
|
preprocessed = {
|
|
1636
2531
|
ok: false,
|
|
1637
|
-
error: errorToOutput(
|
|
2532
|
+
error: errorToOutput(failure),
|
|
1638
2533
|
};
|
|
1639
2534
|
}
|
|
1640
2535
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
}));
|
|
2536
|
+
const result = { direct, preprocessed };
|
|
2537
|
+
const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
|
|
2538
|
+
print({ ...result, ...(downloads ? { downloads } : {}) });
|
|
1645
2539
|
return;
|
|
1646
2540
|
}
|
|
1647
2541
|
throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
|
|
1648
2542
|
};
|
|
2543
|
+
const pageOptions = (flags) => ({
|
|
2544
|
+
cursor: getFlag(flags, 'cursor'),
|
|
2545
|
+
limit: getFlag(flags, 'limit') == null
|
|
2546
|
+
? undefined
|
|
2547
|
+
: parsePositiveIntegerFlag(flags, 'limit', 50),
|
|
2548
|
+
});
|
|
2549
|
+
const canvasForRead = (ctx) => openExecutionSession({
|
|
2550
|
+
...stateOptions(ctx),
|
|
2551
|
+
canvasId: selectedCanvasId(ctx.flags),
|
|
2552
|
+
create: false,
|
|
2553
|
+
});
|
|
2554
|
+
const commandCanvas = async (subcommand, positionals, ctx) => {
|
|
2555
|
+
if (subcommand === 'context' ||
|
|
2556
|
+
subcommand === 'import' ||
|
|
2557
|
+
subcommand === 'compare' ||
|
|
2558
|
+
subcommand === 'layout') {
|
|
2559
|
+
const { canvas } = await canvasForRead(ctx);
|
|
2560
|
+
if (subcommand === 'context') {
|
|
2561
|
+
const action = positionals[2];
|
|
2562
|
+
if (action !== 'get' && action !== 'put')
|
|
2563
|
+
throw new Error('Use canvas context get|put');
|
|
2564
|
+
const result = action === 'get'
|
|
2565
|
+
? await ctx.client.v2.getCanvasContext(canvas.id)
|
|
2566
|
+
: await ctx.client.v2.putCanvasContext(canvas.id, {
|
|
2567
|
+
document: await readJsonArgument(`@${requireFlag(ctx.flags, 'file')}`, '--file'),
|
|
2568
|
+
expectedVersion: parseNonNegativeIntegerFlag(ctx.flags, 'if-version'),
|
|
2569
|
+
});
|
|
2570
|
+
const out = getFlag(ctx.flags, 'out');
|
|
2571
|
+
if (out) {
|
|
2572
|
+
await mkdir(dirname(resolve(out)), { recursive: true });
|
|
2573
|
+
await writeFile(resolve(out), JSON.stringify(result.projectContext.document, null, 2) + '\n', { mode: 0o600 });
|
|
2574
|
+
}
|
|
2575
|
+
print(result);
|
|
2576
|
+
}
|
|
2577
|
+
else if (subcommand === 'layout') {
|
|
2578
|
+
const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
|
|
2579
|
+
print(await ctx.client.v2.appendCanvasLayout(canvas.id, input));
|
|
2580
|
+
}
|
|
2581
|
+
else if (subcommand === 'import') {
|
|
2582
|
+
const filePath = getFlag(ctx.flags, 'file');
|
|
2583
|
+
const uploadId = getFlag(ctx.flags, 'upload-id');
|
|
2584
|
+
const resourceId = getSourceResourceIdFlag(ctx.flags);
|
|
2585
|
+
if ([filePath, uploadId, resourceId].filter(value => value != null)
|
|
2586
|
+
.length !== 1 ||
|
|
2587
|
+
sourceInputCount(ctx.flags) > (uploadId == null ? 1 : 0))
|
|
2588
|
+
throw new Error('Provide exactly one of --file, --resource-id, or --upload-id');
|
|
2589
|
+
print(await importCanvasAssetWithState({
|
|
2590
|
+
client: ctx.client,
|
|
2591
|
+
baseUrl: ctx.client.baseUrl,
|
|
2592
|
+
ownerId: canvas.ownerId,
|
|
2593
|
+
canvasId: canvas.id,
|
|
2594
|
+
stateDir: stateOptions(ctx).stateDir,
|
|
2595
|
+
name: getFlag(ctx.flags, 'name'),
|
|
2596
|
+
...(filePath != null
|
|
2597
|
+
? {
|
|
2598
|
+
filePath: assertFlagHasValue(filePath, '--file <image>'),
|
|
2599
|
+
contentType: getContentTypeFlag(ctx.flags),
|
|
2600
|
+
}
|
|
2601
|
+
: {
|
|
2602
|
+
source: uploadId != null
|
|
2603
|
+
? {
|
|
2604
|
+
uploadId: assertFlagHasValue(uploadId, '--upload-id <id>'),
|
|
2605
|
+
}
|
|
2606
|
+
: {
|
|
2607
|
+
resourceId: assertFlagHasValue(resourceId, '--resource-id <id>'),
|
|
2608
|
+
},
|
|
2609
|
+
}),
|
|
2610
|
+
}));
|
|
2611
|
+
}
|
|
2612
|
+
else {
|
|
2613
|
+
print(await writeCanvasComparison({
|
|
2614
|
+
client: ctx.client,
|
|
2615
|
+
canvasId: canvas.id,
|
|
2616
|
+
sourceId: requireFlag(ctx.flags, 'source-id'),
|
|
2617
|
+
assetId: requireFlag(ctx.flags, 'asset-id'),
|
|
2618
|
+
out: requireFlag(ctx.flags, 'out'),
|
|
2619
|
+
title: getFlag(ctx.flags, 'title'),
|
|
2620
|
+
}));
|
|
2621
|
+
}
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (subcommand === 'list') {
|
|
2625
|
+
print(await ctx.client.v2.listCanvases(pageOptions(ctx.flags)));
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
const capabilities = await ctx.client.v2.getCapabilities();
|
|
2629
|
+
if (capabilities.executionContext.status !== 'available')
|
|
2630
|
+
throw new Error('Canvas execution history unavailable');
|
|
2631
|
+
const scope = {
|
|
2632
|
+
...stateOptions(ctx),
|
|
2633
|
+
baseUrl: ctx.client.baseUrl,
|
|
2634
|
+
ownerId: capabilities.ownerId,
|
|
2635
|
+
};
|
|
2636
|
+
if (subcommand === 'create') {
|
|
2637
|
+
const canvas = await ctx.client.v2.createCanvas({ name: getFlag(ctx.flags, 'name') ?? 'Agent canvas' }, { idempotencyKey: getFlag(ctx.flags, 'operation-id') ?? randomUUID() });
|
|
2638
|
+
await saveCanvasSelection(scope, canvas.id);
|
|
2639
|
+
print(canvas);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
if (!['get', 'use', 'open'].includes(subcommand ?? ''))
|
|
2643
|
+
throw new Error('Use canvas create|list|get|use|open');
|
|
2644
|
+
const id = positionals[2] == null
|
|
2645
|
+
? selectedCanvasId(ctx.flags)
|
|
2646
|
+
: Number(positionals[2]);
|
|
2647
|
+
const canvas = await resolveCanvasSelection({
|
|
2648
|
+
...scope,
|
|
2649
|
+
canvasId: id,
|
|
2650
|
+
create: false,
|
|
2651
|
+
});
|
|
2652
|
+
if (subcommand === 'use')
|
|
2653
|
+
await saveCanvasSelection(scope, canvas.id);
|
|
2654
|
+
if (subcommand === 'open' && stdout.isTTY) {
|
|
2655
|
+
const url = canvas.url;
|
|
2656
|
+
await execFileAsync(process.platform === 'darwin'
|
|
2657
|
+
? 'open'
|
|
2658
|
+
: process.platform === 'win32'
|
|
2659
|
+
? 'rundll32'
|
|
2660
|
+
: 'xdg-open', process.platform === 'win32'
|
|
2661
|
+
? ['url.dll,FileProtocolHandler', url]
|
|
2662
|
+
: [url]);
|
|
2663
|
+
}
|
|
2664
|
+
print(canvas);
|
|
2665
|
+
};
|
|
2666
|
+
const commandLanguage = async (subcommand, ctx) => {
|
|
2667
|
+
if (subcommand !== 'text' && subcommand !== 'vision')
|
|
2668
|
+
throw new Error('Use language text|vision --input-json @<file> --operation-id <uuid>');
|
|
2669
|
+
const input = await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json');
|
|
2670
|
+
const operationId = requireFlag(ctx.flags, 'operation-id');
|
|
2671
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(operationId))
|
|
2672
|
+
throw new Error('--operation-id must be a UUID; reuse the same ID and input after an uncertain response');
|
|
2673
|
+
activeExecution.operationId = operationId;
|
|
2674
|
+
const result = subcommand === 'text'
|
|
2675
|
+
? await ctx.client.v2.languageText(input, {
|
|
2676
|
+
idempotencyKey: operationId,
|
|
2677
|
+
})
|
|
2678
|
+
: await ctx.client.v2.languageVision(input, {
|
|
2679
|
+
idempotencyKey: operationId,
|
|
2680
|
+
});
|
|
2681
|
+
const out = getFlag(ctx.flags, 'out');
|
|
2682
|
+
if (out) {
|
|
2683
|
+
await mkdir(dirname(resolve(out)), { recursive: true });
|
|
2684
|
+
await writeFile(resolve(out), JSON.stringify(result, null, 2) + '\n', {
|
|
2685
|
+
mode: 0o600,
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
print(result);
|
|
2689
|
+
};
|
|
2690
|
+
const commandMoodboard = async (subcommand, positionals, ctx) => {
|
|
2691
|
+
if (subcommand === 'list') {
|
|
2692
|
+
print(await ctx.client.v2.listMoodboards(pageOptions(ctx.flags)));
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
if (subcommand === 'create' || subcommand === 'update') {
|
|
2696
|
+
const input = (await readJsonArgument(requireFlag(ctx.flags, 'input-json'), '--input-json'));
|
|
2697
|
+
if (subcommand === 'update') {
|
|
2698
|
+
print(await ctx.client.v2.updateMoodboard(requirePositional(positionals, 2, 'board-id'), input));
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
const { ownerId } = await ctx.client.v2.getCapabilities();
|
|
2702
|
+
const fingerprint = createHash('sha256')
|
|
2703
|
+
.update(JSON.stringify({ baseUrl: ctx.client.baseUrl, ownerId, input }))
|
|
2704
|
+
.digest('hex');
|
|
2705
|
+
const clientOperationId = getFlag(ctx.flags, 'operation-id') ??
|
|
2706
|
+
`${fingerprint.slice(0, 8)}-${fingerprint.slice(8, 12)}-4${fingerprint.slice(13, 16)}-8${fingerprint.slice(17, 20)}-${fingerprint.slice(20, 32)}`;
|
|
2707
|
+
activeExecution.operationId = clientOperationId;
|
|
2708
|
+
print(await ctx.client.v2.createMoodboard({ ...input, clientOperationId }));
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
const boardId = requirePositional(positionals, 2, 'board-id');
|
|
2712
|
+
if (subcommand === 'get') {
|
|
2713
|
+
print(await ctx.client.v2.getMoodboard(boardId));
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
if (subcommand === 'archive') {
|
|
2717
|
+
print(await ctx.client.v2.archiveMoodboard(boardId));
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (subcommand !== 'analyze')
|
|
2721
|
+
throw new Error('Use moodboard create|list|get|update|archive|analyze');
|
|
2722
|
+
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000);
|
|
2723
|
+
const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
|
|
2724
|
+
const deadline = Date.now() + timeoutMs;
|
|
2725
|
+
let revision = await ctx.client.v2.analyzeMoodboard(boardId, { revisionId: getFlag(ctx.flags, 'revision') }, { signal: AbortSignal.timeout(timeoutMs) });
|
|
2726
|
+
while (hasFlag(ctx.flags, 'wait') && revision.status === 'analyzing') {
|
|
2727
|
+
stderr.write(`[moodboard] ${boardId} revision=${revision.id} analyzing job=${revision.analysisJobId ?? 'pending'}\n`);
|
|
2728
|
+
if (Date.now() >= deadline)
|
|
2729
|
+
throw new Error(`Moodboard analysis timeout; resume with moodboard get ${boardId}`);
|
|
2730
|
+
await delay(Math.min(intervalMs, Math.max(1, deadline - Date.now())));
|
|
2731
|
+
if (Date.now() >= deadline)
|
|
2732
|
+
throw new Error(`Moodboard analysis timeout; resume with moodboard get ${boardId}`);
|
|
2733
|
+
const board = await ctx.client.v2.getMoodboard(boardId, {
|
|
2734
|
+
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())),
|
|
2735
|
+
});
|
|
2736
|
+
if (board.currentRevision?.id !== revision.id)
|
|
2737
|
+
throw new Error('Moodboard revision changed while waiting; inspect the board before generating');
|
|
2738
|
+
revision = board.currentRevision;
|
|
2739
|
+
}
|
|
2740
|
+
print(revision);
|
|
2741
|
+
if (revision.status === 'failed')
|
|
2742
|
+
process.exitCode = 1;
|
|
2743
|
+
};
|
|
2744
|
+
const commandGraph = async (subcommand, ctx) => {
|
|
2745
|
+
if (!['show', 'lineage', 'export'].includes(subcommand ?? ''))
|
|
2746
|
+
throw new Error('Use graph show|lineage|export');
|
|
2747
|
+
const { canvas } = await canvasForRead(ctx);
|
|
2748
|
+
const direction = getFlag(ctx.flags, 'direction') ?? 'both';
|
|
2749
|
+
if (!['ancestors', 'descendants', 'both'].includes(direction))
|
|
2750
|
+
throw new Error('--direction must be ancestors, descendants, or both');
|
|
2751
|
+
const graph = await ctx.client.v2.getCanvasGraph(canvas.id, {
|
|
2752
|
+
artifactId: subcommand === 'lineage'
|
|
2753
|
+
? requireFlag(ctx.flags, 'artifact')
|
|
2754
|
+
: getFlag(ctx.flags, 'artifact'),
|
|
2755
|
+
direction: direction,
|
|
2756
|
+
depth: parsePositiveIntegerFlag(ctx.flags, 'depth', 5),
|
|
2757
|
+
});
|
|
2758
|
+
if (subcommand === 'export' &&
|
|
2759
|
+
graph.truncated &&
|
|
2760
|
+
!hasFlag(ctx.flags, 'allow-truncated'))
|
|
2761
|
+
throw new Error('Graph exceeds export bounds; narrow with --artifact or explicitly use --allow-truncated');
|
|
2762
|
+
const out = getFlag(ctx.flags, 'out');
|
|
2763
|
+
if (subcommand === 'export' && out) {
|
|
2764
|
+
await writeFile(resolve(out), JSON.stringify(graph, null, 2) + '\n', {
|
|
2765
|
+
mode: 0o600,
|
|
2766
|
+
});
|
|
2767
|
+
print({
|
|
2768
|
+
path: resolve(out),
|
|
2769
|
+
revision: graph.revision,
|
|
2770
|
+
truncated: graph.truncated,
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
else
|
|
2774
|
+
print(graph);
|
|
2775
|
+
};
|
|
2776
|
+
const commandEvaluations = async (subcommand, positionals, ctx) => {
|
|
2777
|
+
if (subcommand === 'get') {
|
|
2778
|
+
print(await ctx.client.v2.getEvaluation(requirePositional(positionals, 2, 'evaluation-id')));
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const { canvas } = await canvasForRead(ctx);
|
|
2782
|
+
if (subcommand === 'list') {
|
|
2783
|
+
print(await ctx.client.v2.listEvaluations({
|
|
2784
|
+
...pageOptions(ctx.flags),
|
|
2785
|
+
canvasId: canvas.id,
|
|
2786
|
+
artifactId: getFlag(ctx.flags, 'artifact'),
|
|
2787
|
+
}));
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
if (subcommand !== 'submit')
|
|
2791
|
+
throw new Error('Use evaluations submit|list|get');
|
|
2792
|
+
const reportArg = requireFlag(ctx.flags, 'report');
|
|
2793
|
+
const report = await readJsonArgument(reportArg.startsWith('{') || reportArg.startsWith('@')
|
|
2794
|
+
? reportArg
|
|
2795
|
+
: `@${reportArg}`, '--report');
|
|
2796
|
+
const clientOperationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
|
|
2797
|
+
activeExecution.operationId = clientOperationId;
|
|
2798
|
+
const evaluation = await ctx.client.v2.submitEvaluation({
|
|
2799
|
+
canvasId: canvas.id,
|
|
2800
|
+
clientOperationId,
|
|
2801
|
+
artifactId: requireFlag(ctx.flags, 'artifact'),
|
|
2802
|
+
runId: getFlag(ctx.flags, 'run'),
|
|
2803
|
+
referenceAssetIds: [
|
|
2804
|
+
...new Set([
|
|
2805
|
+
...getFlagValues(ctx.flags, 'reference'),
|
|
2806
|
+
...(Array.isArray(report.referenceAssetIds)
|
|
2807
|
+
? report.referenceAssetIds.filter((id) => typeof id === 'string')
|
|
2808
|
+
: []),
|
|
2809
|
+
]),
|
|
2810
|
+
],
|
|
2811
|
+
evaluator: {
|
|
2812
|
+
name: getFlag(ctx.flags, 'agent') ?? 'external-agent',
|
|
2813
|
+
version: getFlag(ctx.flags, 'evaluator-version'),
|
|
2814
|
+
},
|
|
2815
|
+
report: report,
|
|
2816
|
+
}, { idempotencyKey: clientOperationId });
|
|
2817
|
+
print(evaluation);
|
|
2818
|
+
if (hasFlag(ctx.flags, 'require-pass'))
|
|
2819
|
+
process.exitCode =
|
|
2820
|
+
evaluation.report.verdict === 'pass'
|
|
2821
|
+
? 0
|
|
2822
|
+
: evaluation.report.verdict === 'fail'
|
|
2823
|
+
? 1
|
|
2824
|
+
: 3;
|
|
2825
|
+
};
|
|
2826
|
+
/**
|
|
2827
|
+
* `assethub runs upload <path>` — push a local artifact-graph run folder to
|
|
2828
|
+
* Production Control.
|
|
2829
|
+
*
|
|
2830
|
+
* The wire format is `ag.registry.v1`, identical to what the Cloudflare
|
|
2831
|
+
* ag-registry accepts, so the same folder can go to either destination without
|
|
2832
|
+
* conversion. See src/runsUpload/ for the plan/transport split, and
|
|
2833
|
+
* src/runsUpload/controlEndpoints.ts for every path and size limit.
|
|
2834
|
+
*/
|
|
2835
|
+
const commandRuns = async (subcommand, positionals, ctx) => {
|
|
2836
|
+
if (subcommand === 'list') {
|
|
2837
|
+
const { canvas } = await canvasForRead(ctx);
|
|
2838
|
+
print(await ctx.client.v2.listCanvasRuns(canvas.id, pageOptions(ctx.flags)));
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
if (subcommand === 'get' || subcommand === 'watch') {
|
|
2842
|
+
const id = requirePositional(positionals, 2, 'run-id');
|
|
2843
|
+
const execution = subcommand === 'get'
|
|
2844
|
+
? await ctx.client.v2.getRun(id)
|
|
2845
|
+
: await waitForExecution(ctx.client, id, watchOptions(ctx.flags));
|
|
2846
|
+
print({ execution });
|
|
2847
|
+
process.exitCode = executionExitCode(execution);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (subcommand === 'resume') {
|
|
2851
|
+
const result = await resumeRecorded({
|
|
2852
|
+
...stateOptions(ctx),
|
|
2853
|
+
operationId: requirePositional(positionals, 2, 'operation-id'),
|
|
2854
|
+
});
|
|
2855
|
+
await printRecorded(ctx, result);
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const args = parseRunsUploadArgs(subcommand, positionals, ctx.flags);
|
|
2859
|
+
const folder = await readGraphFolder(resolve(args.folderPath));
|
|
2860
|
+
const plan = await buildRunUploadPlan({
|
|
2861
|
+
folder,
|
|
2862
|
+
graphId: args.graphId,
|
|
2863
|
+
streamId: args.streamId,
|
|
2864
|
+
rev: args.rev,
|
|
2865
|
+
description: args.description,
|
|
2866
|
+
tags: args.tags,
|
|
2867
|
+
});
|
|
2868
|
+
for (const warning of plan.warnings) {
|
|
2869
|
+
stderr.write(`[warn] ${warning}\n`);
|
|
2870
|
+
}
|
|
2871
|
+
const summary = runsUploadSummary(folder.path, plan);
|
|
2872
|
+
// --dry-run stops here on purpose: everything above is the validation a
|
|
2873
|
+
// corrupt run folder fails, so this is how you find out before production
|
|
2874
|
+
// sees it.
|
|
2875
|
+
if (args.dryRun) {
|
|
2876
|
+
print(runsUploadDryRunReport(summary, plan, ctx.auth.baseUrl));
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const result = await uploadRun({
|
|
2880
|
+
plan,
|
|
2881
|
+
baseUrl: ctx.auth.baseUrl,
|
|
2882
|
+
apiKey: ctx.auth.apiKey,
|
|
2883
|
+
skipRegister: args.skipRegister,
|
|
2884
|
+
onProgress: event => {
|
|
2885
|
+
if (event.kind === 'register') {
|
|
2886
|
+
stderr.write(`[upload] register graph=${event.graphId}\n`);
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
if (event.kind === 'blob') {
|
|
2890
|
+
stderr.write(`[upload] blob ${event.index + 1}/${event.total} ${event.status} ` +
|
|
2891
|
+
`${event.blob.name} (${formatBytes(event.blob.size)})\n`);
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
stderr.write(`[upload] snapshot graph=${event.graphId} rev=${event.rev}\n`);
|
|
2895
|
+
},
|
|
2896
|
+
});
|
|
2897
|
+
print({ ...summary, ...result });
|
|
2898
|
+
};
|
|
2899
|
+
/**
|
|
2900
|
+
* Everything `runs upload` decides from argv, split out from the command so the
|
|
2901
|
+
* subcommand routing and flag handling are testable without a filesystem, an
|
|
2902
|
+
* API key, or a network.
|
|
2903
|
+
*/
|
|
2904
|
+
export const parseRunsUploadArgs = (subcommand, positionals, flags) => {
|
|
2905
|
+
if (subcommand !== 'upload') {
|
|
2906
|
+
throw new Error('Unknown runs command. Use "runs upload <path>".');
|
|
2907
|
+
}
|
|
2908
|
+
const folderPath = positionals[2];
|
|
2909
|
+
if (folderPath == null || folderPath.trim().length === 0) {
|
|
2910
|
+
throw new Error('assethub runs upload needs the path to a run folder (the directory holding manifest.json, nodes.jsonl and edges.jsonl).');
|
|
2911
|
+
}
|
|
2912
|
+
return {
|
|
2913
|
+
folderPath,
|
|
2914
|
+
graphId: getFlag(flags, 'graph-id'),
|
|
2915
|
+
streamId: getFlag(flags, 'stream-id'),
|
|
2916
|
+
rev: parseNonNegativeIntegerFlag(flags, 'rev'),
|
|
2917
|
+
description: getFlag(flags, 'description') ?? '',
|
|
2918
|
+
tags: getFlagValues(flags, 'tag'),
|
|
2919
|
+
dryRun: hasFlag(flags, 'dry-run'),
|
|
2920
|
+
skipRegister: hasFlag(flags, 'skip-register'),
|
|
2921
|
+
};
|
|
2922
|
+
};
|
|
2923
|
+
export const runsUploadSummary = (runFolder, plan) => ({
|
|
2924
|
+
runFolder,
|
|
2925
|
+
graphId: plan.graphId,
|
|
2926
|
+
streamId: plan.streamId,
|
|
2927
|
+
rev: plan.rev,
|
|
2928
|
+
graphHash: plan.graphHash,
|
|
2929
|
+
counts: plan.counts,
|
|
2930
|
+
snapshotSize: formatBytes(plan.sizes.pushBytes),
|
|
2931
|
+
blobBytes: formatBytes(plan.counts.blobBytes),
|
|
2932
|
+
});
|
|
2933
|
+
export const runsUploadDryRunReport = (summary, plan, baseUrl) => ({
|
|
2934
|
+
dryRun: true,
|
|
2935
|
+
...summary,
|
|
2936
|
+
warnings: plan.warnings,
|
|
2937
|
+
blobs: plan.blobs.map(blob => ({
|
|
2938
|
+
blobKey: blob.blobKey,
|
|
2939
|
+
name: blob.name,
|
|
2940
|
+
mime: blob.mime,
|
|
2941
|
+
size: blob.size,
|
|
2942
|
+
})),
|
|
2943
|
+
wouldUpload: `${plan.blobs.length} blob(s) then 1 snapshot to ${baseUrl}`,
|
|
2944
|
+
});
|
|
2945
|
+
const parseNonNegativeIntegerFlag = (flags, name) => {
|
|
2946
|
+
const raw = getFlag(flags, name);
|
|
2947
|
+
if (raw == null || raw === 'true') {
|
|
2948
|
+
return undefined;
|
|
2949
|
+
}
|
|
2950
|
+
const parsed = Number(raw);
|
|
2951
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
2952
|
+
throw new Error(`--${name} must be a non-negative integer`);
|
|
2953
|
+
}
|
|
2954
|
+
return parsed;
|
|
2955
|
+
};
|
|
1649
2956
|
const run = async () => {
|
|
1650
2957
|
const parsed = parseArgs(argv.slice(2));
|
|
1651
2958
|
if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
|
|
@@ -1653,12 +2960,96 @@ const run = async () => {
|
|
|
1653
2960
|
return;
|
|
1654
2961
|
}
|
|
1655
2962
|
const [command, subcommand] = parsed.positionals;
|
|
2963
|
+
if (command === 'project') {
|
|
2964
|
+
if (subcommand !== 'ingest')
|
|
2965
|
+
throw new Error('Use project ingest <source-dir> --out-dir <directory>');
|
|
2966
|
+
const result = await ingestProjectSources({
|
|
2967
|
+
sourceDir: requirePositional(parsed.positionals, 2, 'source-dir'),
|
|
2968
|
+
outDir: requireFlag(parsed.flags, 'out-dir'),
|
|
2969
|
+
excludedDirectories: getFlagValues(parsed.flags, 'exclude-dir').map(value => assertFlagHasValue(value, '--exclude-dir <relative-path>')),
|
|
2970
|
+
onProgress: message => {
|
|
2971
|
+
stderr.write(`[ingest] ${message}\n`);
|
|
2972
|
+
},
|
|
2973
|
+
});
|
|
2974
|
+
print(result);
|
|
2975
|
+
if (result.status !== 'complete')
|
|
2976
|
+
process.exitCode = 3;
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
if (command === 'workspace') {
|
|
2980
|
+
if (!hasFlag(parsed.flags, 'internal')) {
|
|
2981
|
+
throw new Error('Workspace operations currently require --internal and the AssetHub monorepo with its existing Infisical environment.');
|
|
2982
|
+
}
|
|
2983
|
+
const root = fileURLToPath(new URL('../../../', import.meta.url));
|
|
2984
|
+
const script = join(root, 'apps/frontend/scripts/assethub-workspace.ts');
|
|
2985
|
+
await readFile(script).catch(() => {
|
|
2986
|
+
throw new Error('Internal workspace, moodboard and context operations require the AssetHub monorepo checkout and its native application dependencies.');
|
|
2987
|
+
});
|
|
2988
|
+
await new Promise((done, reject) => {
|
|
2989
|
+
const child = spawn(process.execPath, [
|
|
2990
|
+
'--import',
|
|
2991
|
+
'tsx',
|
|
2992
|
+
script,
|
|
2993
|
+
...argv.slice(3).filter(arg => arg !== '--internal'),
|
|
2994
|
+
], {
|
|
2995
|
+
cwd: root,
|
|
2996
|
+
env: {
|
|
2997
|
+
...env,
|
|
2998
|
+
TSX_TSCONFIG_PATH: join(root, 'apps/frontend/tsconfig.json'),
|
|
2999
|
+
},
|
|
3000
|
+
stdio: 'inherit',
|
|
3001
|
+
});
|
|
3002
|
+
child.once('error', reject);
|
|
3003
|
+
child.once('exit', code => code === 0
|
|
3004
|
+
? done()
|
|
3005
|
+
: reject(new Error(`Internal workspace operation exited with ${code ?? 'a signal'}`)));
|
|
3006
|
+
});
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
1656
3009
|
if (command === 'auth') {
|
|
1657
3010
|
await commandAuth(subcommand, parsed.flags);
|
|
1658
3011
|
return;
|
|
1659
3012
|
}
|
|
1660
|
-
|
|
3013
|
+
// The experimental demo uses an unpublished workspace development dependency.
|
|
3014
|
+
// Resolve it only for this command so standalone project commands can load.
|
|
3015
|
+
if (command === 'autopilot') {
|
|
3016
|
+
const { commandAutopilot } = await import('./autopilot.js').catch((error) => {
|
|
3017
|
+
if (error instanceof Error &&
|
|
3018
|
+
'code' in error &&
|
|
3019
|
+
error.code === 'ERR_MODULE_NOT_FOUND' &&
|
|
3020
|
+
error.message.includes('@assethub/autopilot-core')) {
|
|
3021
|
+
throw new Error('The experimental autopilot demo requires the AssetHub monorepo with @assethub/autopilot-core installed. Standalone project workflow commands are available without it.');
|
|
3022
|
+
}
|
|
3023
|
+
throw error;
|
|
3024
|
+
});
|
|
3025
|
+
await commandAutopilot(parsed.positionals, parsed.flags);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const ctx = await createContext(parsed.flags, argv.slice(2));
|
|
1661
3029
|
switch (command) {
|
|
3030
|
+
case 'language':
|
|
3031
|
+
await commandLanguage(subcommand, ctx);
|
|
3032
|
+
return;
|
|
3033
|
+
case 'moodboard':
|
|
3034
|
+
await commandMoodboard(subcommand, parsed.positionals, ctx);
|
|
3035
|
+
return;
|
|
3036
|
+
case 'canvas':
|
|
3037
|
+
await commandCanvas(subcommand, parsed.positionals, ctx);
|
|
3038
|
+
return;
|
|
3039
|
+
case 'graph':
|
|
3040
|
+
await commandGraph(subcommand, ctx);
|
|
3041
|
+
return;
|
|
3042
|
+
case 'evaluations':
|
|
3043
|
+
await commandEvaluations(subcommand, parsed.positionals, ctx);
|
|
3044
|
+
return;
|
|
3045
|
+
case 'evaluate':
|
|
3046
|
+
if (subcommand !== 'list')
|
|
3047
|
+
throw new Error('Server evaluators are capability-gated. Use evaluate list, or evaluations submit for external agent reports.');
|
|
3048
|
+
print((await ctx.client.v2.getCapabilities()).evaluators);
|
|
3049
|
+
return;
|
|
3050
|
+
case 'capabilities':
|
|
3051
|
+
print(await ctx.client.v2.getCapabilities());
|
|
3052
|
+
return;
|
|
1662
3053
|
case 'models':
|
|
1663
3054
|
await commandModels(subcommand, parsed.positionals, ctx);
|
|
1664
3055
|
return;
|
|
@@ -1674,12 +3065,21 @@ const run = async () => {
|
|
|
1674
3065
|
case 'mesh':
|
|
1675
3066
|
await commandMesh(subcommand, ctx);
|
|
1676
3067
|
return;
|
|
3068
|
+
case 'rig':
|
|
3069
|
+
await commandRig(subcommand, ctx);
|
|
3070
|
+
return;
|
|
3071
|
+
case 'animate':
|
|
3072
|
+
await commandAnimate(subcommand, ctx);
|
|
3073
|
+
return;
|
|
1677
3074
|
case 'jobs':
|
|
1678
3075
|
await commandJobs(subcommand, parsed.positionals, ctx);
|
|
1679
3076
|
return;
|
|
1680
3077
|
case 'production':
|
|
1681
3078
|
await commandProduction(subcommand, parsed.positionals, ctx);
|
|
1682
3079
|
return;
|
|
3080
|
+
case 'runs':
|
|
3081
|
+
await commandRuns(subcommand, parsed.positionals, ctx);
|
|
3082
|
+
return;
|
|
1683
3083
|
case 'parts':
|
|
1684
3084
|
await commandParts(subcommand, ctx);
|
|
1685
3085
|
return;
|
|
@@ -1687,15 +3087,64 @@ const run = async () => {
|
|
|
1687
3087
|
throw new Error(`Unknown command: ${command}`);
|
|
1688
3088
|
}
|
|
1689
3089
|
};
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
3090
|
+
const isMainModule = (() => {
|
|
3091
|
+
const entry = argv[1];
|
|
3092
|
+
if (entry == null)
|
|
3093
|
+
return false;
|
|
3094
|
+
try {
|
|
3095
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
|
|
1696
3096
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
3097
|
+
catch {
|
|
3098
|
+
return false;
|
|
1699
3099
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
3100
|
+
})();
|
|
3101
|
+
if (isMainModule) {
|
|
3102
|
+
process.once('SIGINT', () => {
|
|
3103
|
+
print({
|
|
3104
|
+
error: {
|
|
3105
|
+
code: 'INTERRUPTED',
|
|
3106
|
+
message: 'Client interrupted; server execution continues',
|
|
3107
|
+
},
|
|
3108
|
+
...activeExecution,
|
|
3109
|
+
});
|
|
3110
|
+
exit(130);
|
|
3111
|
+
});
|
|
3112
|
+
run().catch((error) => {
|
|
3113
|
+
if (error instanceof AssetHubApiError) {
|
|
3114
|
+
stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
|
|
3115
|
+
}
|
|
3116
|
+
else if (error instanceof Error) {
|
|
3117
|
+
stderr.write(`${error.message}\n`);
|
|
3118
|
+
}
|
|
3119
|
+
else {
|
|
3120
|
+
stderr.write(`${String(error)}\n`);
|
|
3121
|
+
}
|
|
3122
|
+
print({
|
|
3123
|
+
error: {
|
|
3124
|
+
code: error instanceof AssetHubApiError
|
|
3125
|
+
? error.code
|
|
3126
|
+
: error instanceof CliExecutionError && error.exitCode === 3
|
|
3127
|
+
? 'WAIT_TIMEOUT'
|
|
3128
|
+
: 'CLI_ERROR',
|
|
3129
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3130
|
+
},
|
|
3131
|
+
runId: error instanceof CliExecutionError
|
|
3132
|
+
? error.runId
|
|
3133
|
+
: activeExecution.runId,
|
|
3134
|
+
operationId: error instanceof CliExecutionError
|
|
3135
|
+
? error.operationId
|
|
3136
|
+
: activeExecution.operationId,
|
|
3137
|
+
execution: error instanceof CliExecutionError
|
|
3138
|
+
? error.execution
|
|
3139
|
+
: activeExecution.execution,
|
|
3140
|
+
});
|
|
3141
|
+
exit(error instanceof CliExecutionError
|
|
3142
|
+
? error.exitCode
|
|
3143
|
+
: error instanceof AssetHubApiError &&
|
|
3144
|
+
error.status >= 500 &&
|
|
3145
|
+
error.code !== 'CANVAS_EXECUTION_UNAVAILABLE' &&
|
|
3146
|
+
error.code !== 'EVALUATOR_UNAVAILABLE'
|
|
3147
|
+
? 1
|
|
3148
|
+
: 2);
|
|
3149
|
+
});
|
|
3150
|
+
}
|