@assethub/cli 0.1.6 → 0.1.8
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 +289 -5
- 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 +1567 -189
- 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 -5
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
|
}
|
|
@@ -500,7 +584,9 @@ const ensureFileNameExtension = (fileName, mediaType, mimeType) => {
|
|
|
500
584
|
};
|
|
501
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.';
|
|
502
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.';
|
|
503
|
-
const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ??
|
|
587
|
+
const getSourceUrlFlag = (flags) => getFlag(flags, 'source-url') ??
|
|
588
|
+
getFlag(flags, 'image-url') ??
|
|
589
|
+
getFlag(flags, 'url');
|
|
504
590
|
const assertFlagHasValue = (value, flagDescription) => {
|
|
505
591
|
if (value === 'true' || value.trim() === '') {
|
|
506
592
|
throw new Error(`Missing required flag value: ${flagDescription}`);
|
|
@@ -625,7 +711,15 @@ const sourceDescriptorFromJson = (value, options) => {
|
|
|
625
711
|
if (resourceId != null) {
|
|
626
712
|
return { kind: 'source', source: { resourceId } };
|
|
627
713
|
}
|
|
628
|
-
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;
|
|
629
723
|
if (isBlobLocation(fileRefValue)) {
|
|
630
724
|
return { kind: 'source', source: { fileRef: fileRefValue } };
|
|
631
725
|
}
|
|
@@ -652,7 +746,13 @@ const sourceDescriptorFromJson = (value, options) => {
|
|
|
652
746
|
flagName: '--source-json',
|
|
653
747
|
});
|
|
654
748
|
}
|
|
655
|
-
const url = getFirstString(value, [
|
|
749
|
+
const url = getFirstString(value, [
|
|
750
|
+
'sourceUrl',
|
|
751
|
+
'source_url',
|
|
752
|
+
'url',
|
|
753
|
+
'publicUrl',
|
|
754
|
+
'public_url',
|
|
755
|
+
]);
|
|
656
756
|
if (url != null) {
|
|
657
757
|
const source = sourceFromString(url);
|
|
658
758
|
if (source != null) {
|
|
@@ -750,13 +850,11 @@ const readClipboardImage = async () => {
|
|
|
750
850
|
await unlink(filePath).catch(() => undefined);
|
|
751
851
|
}
|
|
752
852
|
};
|
|
753
|
-
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
|
|
853
|
+
const uploadSourceDescriptor = async ({ descriptor, client, mediaType, preferUploadId, }) => {
|
|
754
854
|
if (descriptor.kind === 'source') {
|
|
755
855
|
return descriptor.source;
|
|
756
856
|
}
|
|
757
|
-
const fileName = descriptor.kind === 'file'
|
|
758
|
-
? basename(descriptor.path)
|
|
759
|
-
: descriptor.fileName;
|
|
857
|
+
const fileName = descriptor.kind === 'file' ? basename(descriptor.path) : descriptor.fileName;
|
|
760
858
|
const fileNameWithExtension = ensureFileNameExtension(fileName, mediaType, descriptor.contentType);
|
|
761
859
|
const contentType = resolveUploadContentType(fileNameWithExtension, descriptor.contentType);
|
|
762
860
|
const file = descriptor.kind === 'file'
|
|
@@ -769,9 +867,14 @@ const uploadSourceDescriptor = async ({ descriptor, client, mediaType, }) => {
|
|
|
769
867
|
fileName: fileNameWithExtension,
|
|
770
868
|
mediaType,
|
|
771
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
|
+
}
|
|
772
875
|
return { fileRef: upload.fileRef };
|
|
773
876
|
};
|
|
774
|
-
const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
877
|
+
const resolveSource = async ({ flags, client, fileMediaType, preferUploadId, }) => {
|
|
775
878
|
const providedCount = sourceInputCount(flags);
|
|
776
879
|
if (providedCount === 0) {
|
|
777
880
|
return undefined;
|
|
@@ -800,9 +903,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
800
903
|
const filePath = getFlag(flags, 'file');
|
|
801
904
|
if (filePath != null) {
|
|
802
905
|
return await uploadSourceDescriptor({
|
|
803
|
-
descriptor: {
|
|
906
|
+
descriptor: {
|
|
907
|
+
kind: 'file',
|
|
908
|
+
path: assertFlagHasValue(filePath, '--file <path>'),
|
|
909
|
+
},
|
|
804
910
|
client,
|
|
805
911
|
mediaType: fileMediaType,
|
|
912
|
+
preferUploadId,
|
|
806
913
|
});
|
|
807
914
|
}
|
|
808
915
|
const dataUri = getFlag(flags, 'data-uri');
|
|
@@ -811,35 +918,49 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
811
918
|
descriptor: descriptorFromDataUri(assertFlagHasValue(dataUri, '--data-uri <uri>'), fileMediaType, getFlag(flags, 'file-name') ?? 'data-uri', '--data-uri'),
|
|
812
919
|
client,
|
|
813
920
|
mediaType: fileMediaType,
|
|
921
|
+
preferUploadId,
|
|
814
922
|
});
|
|
815
923
|
}
|
|
816
924
|
if (getFlag(flags, 'source-json') != null) {
|
|
817
925
|
const parsed = parseJsonValue(await readSourceJsonFlag(flags), '--source-json');
|
|
818
926
|
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
819
927
|
mediaType: fileMediaType,
|
|
820
|
-
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
928
|
+
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
929
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
821
930
|
});
|
|
822
931
|
if (descriptor == null) {
|
|
823
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.');
|
|
824
933
|
}
|
|
825
|
-
return await uploadSourceDescriptor({
|
|
934
|
+
return await uploadSourceDescriptor({
|
|
935
|
+
descriptor,
|
|
936
|
+
client,
|
|
937
|
+
mediaType: fileMediaType,
|
|
938
|
+
preferUploadId,
|
|
939
|
+
});
|
|
826
940
|
}
|
|
827
941
|
if (hasFlag(flags, 'stdin-json')) {
|
|
828
942
|
const parsed = parseJsonValue(await readStdin(), '--stdin-json');
|
|
829
943
|
const descriptor = sourceDescriptorFromJson(parsed, {
|
|
830
944
|
mediaType: fileMediaType,
|
|
831
|
-
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
945
|
+
fallbackFileName: getFlag(flags, 'file-name') ??
|
|
946
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
832
947
|
});
|
|
833
948
|
if (descriptor == null) {
|
|
834
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.');
|
|
835
950
|
}
|
|
836
|
-
return await uploadSourceDescriptor({
|
|
951
|
+
return await uploadSourceDescriptor({
|
|
952
|
+
descriptor,
|
|
953
|
+
client,
|
|
954
|
+
mediaType: fileMediaType,
|
|
955
|
+
preferUploadId,
|
|
956
|
+
});
|
|
837
957
|
}
|
|
838
958
|
if (hasFlag(flags, 'stdin-data-uri')) {
|
|
839
959
|
return await uploadSourceDescriptor({
|
|
840
960
|
descriptor: descriptorFromDataUri(await readStdin(), fileMediaType, getFlag(flags, 'file-name') ?? 'stdin-data-uri', '--stdin-data-uri'),
|
|
841
961
|
client,
|
|
842
962
|
mediaType: fileMediaType,
|
|
963
|
+
preferUploadId,
|
|
843
964
|
});
|
|
844
965
|
}
|
|
845
966
|
if (hasFlag(flags, 'stdin-base64')) {
|
|
@@ -847,12 +968,14 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
847
968
|
descriptor: descriptorFromBase64({
|
|
848
969
|
value: await readStdin(),
|
|
849
970
|
contentType: getContentTypeFlag(flags),
|
|
850
|
-
fileName: getFlag(flags, 'file-name') ??
|
|
971
|
+
fileName: getFlag(flags, 'file-name') ??
|
|
972
|
+
defaultFileNameForMediaType(fileMediaType),
|
|
851
973
|
mediaType: fileMediaType,
|
|
852
974
|
flagName: '--stdin-base64',
|
|
853
975
|
}),
|
|
854
976
|
client,
|
|
855
977
|
mediaType: fileMediaType,
|
|
978
|
+
preferUploadId,
|
|
856
979
|
});
|
|
857
980
|
}
|
|
858
981
|
if (hasFlag(flags, 'clipboard')) {
|
|
@@ -868,6 +991,7 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
868
991
|
},
|
|
869
992
|
client,
|
|
870
993
|
mediaType: fileMediaType,
|
|
994
|
+
preferUploadId,
|
|
871
995
|
});
|
|
872
996
|
}
|
|
873
997
|
if (hasFlag(flags, 'stdin')) {
|
|
@@ -875,11 +999,13 @@ const resolveSource = async ({ flags, client, fileMediaType, }) => {
|
|
|
875
999
|
descriptor: {
|
|
876
1000
|
kind: 'blob',
|
|
877
1001
|
blob: await readStdinBlob(getContentTypeFlag(flags)),
|
|
878
|
-
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ??
|
|
1002
|
+
fileName: ensureFileNameExtension(getFlag(flags, 'file-name') ??
|
|
1003
|
+
defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
|
|
879
1004
|
contentType: getContentTypeFlag(flags),
|
|
880
1005
|
},
|
|
881
1006
|
client,
|
|
882
1007
|
mediaType: fileMediaType,
|
|
1008
|
+
preferUploadId,
|
|
883
1009
|
});
|
|
884
1010
|
}
|
|
885
1011
|
return undefined;
|
|
@@ -891,69 +1017,126 @@ const requireSource = async (options) => {
|
|
|
891
1017
|
}
|
|
892
1018
|
return source;
|
|
893
1019
|
};
|
|
894
|
-
const sourceToV1ImageInput = (source) => {
|
|
895
|
-
if (source == null) {
|
|
896
|
-
return {};
|
|
897
|
-
}
|
|
898
|
-
if ('url' in source) {
|
|
899
|
-
return { imageUrl: source.url };
|
|
900
|
-
}
|
|
901
|
-
if ('fileRef' in source) {
|
|
902
|
-
return { imageBlobLocation: source.fileRef };
|
|
903
|
-
}
|
|
904
|
-
throw new Error('v1 image generation does not accept source-resource-id. Use --source-url, --file, or --api-version v2.');
|
|
905
|
-
};
|
|
906
1020
|
const resolveProductionAnalyzeSource = async (ctx) => {
|
|
907
|
-
const source = await requireSource({
|
|
1021
|
+
const source = await recordedSource(ctx, await requireSource({
|
|
908
1022
|
flags: ctx.flags,
|
|
909
1023
|
client: ctx.client,
|
|
910
1024
|
fileMediaType: 'image',
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
if (source.url != null) {
|
|
916
|
-
return { imageUrl: source.url };
|
|
917
|
-
}
|
|
918
|
-
if (source.fileRef == null) {
|
|
919
|
-
throw new Error(missingSourceMessage);
|
|
920
|
-
}
|
|
921
|
-
return { imageBlobLocation: source.fileRef };
|
|
1025
|
+
preferUploadId: true,
|
|
1026
|
+
}));
|
|
1027
|
+
return productionAnalyzeSourceFromSource(source);
|
|
922
1028
|
};
|
|
923
1029
|
const productionAnalyzeSourceFromSource = (source) => {
|
|
924
|
-
if (source.
|
|
1030
|
+
if (source.uploadId)
|
|
1031
|
+
return { uploadId: source.uploadId };
|
|
1032
|
+
if (source.resourceId)
|
|
925
1033
|
return { imageAssetId: source.resourceId };
|
|
926
|
-
|
|
927
|
-
if (source.url != null) {
|
|
1034
|
+
if (source.url)
|
|
928
1035
|
return { imageUrl: source.url };
|
|
929
|
-
|
|
930
|
-
|
|
1036
|
+
if (source.fileRef)
|
|
1037
|
+
return { imageBlobLocation: source.fileRef };
|
|
1038
|
+
throw new Error(missingSourceMessage);
|
|
931
1039
|
};
|
|
932
1040
|
const sourceFromProductionAnalyzeSource = (source) => {
|
|
1041
|
+
if (source.uploadId)
|
|
1042
|
+
return { uploadId: source.uploadId };
|
|
933
1043
|
if (source.imageAssetId != null) {
|
|
934
1044
|
return { resourceId: source.imageAssetId };
|
|
935
1045
|
}
|
|
936
1046
|
if (source.imageUrl != null) {
|
|
937
1047
|
return { url: source.imageUrl };
|
|
938
1048
|
}
|
|
939
|
-
|
|
1049
|
+
if (source.imageBlobLocation)
|
|
1050
|
+
return { fileRef: source.imageBlobLocation };
|
|
1051
|
+
throw new Error(missingSourceMessage);
|
|
940
1052
|
};
|
|
941
|
-
const productionAnalyzeTerminalStatuses = new Set([
|
|
1053
|
+
const productionAnalyzeTerminalStatuses = new Set([
|
|
1054
|
+
'ready',
|
|
1055
|
+
'completed',
|
|
1056
|
+
'failed',
|
|
1057
|
+
]);
|
|
942
1058
|
const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
|
|
943
|
-
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, }) => {
|
|
944
1099
|
const intervalMs = parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000);
|
|
945
|
-
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms',
|
|
1100
|
+
const timeoutMs = parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 1800000);
|
|
946
1101
|
const startedAt = Date.now();
|
|
1102
|
+
let lastPhase;
|
|
947
1103
|
while (Date.now() - startedAt <= timeoutMs) {
|
|
948
1104
|
const status = await ctx.client.v1.getProductionStatus(orderId);
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
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)) {
|
|
952
1111
|
return status;
|
|
953
1112
|
}
|
|
954
1113
|
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
955
1114
|
}
|
|
956
|
-
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();
|
|
957
1140
|
};
|
|
958
1141
|
const pollIfRequested = async ({ ctx, jobId, apiVersion = 'v2', }) => {
|
|
959
1142
|
if (!hasFlag(ctx.flags, 'wait')) {
|
|
@@ -982,11 +1165,18 @@ const commandSource = async (subcommand, ctx) => {
|
|
|
982
1165
|
throw new Error('Unknown source command. Use "source create".');
|
|
983
1166
|
}
|
|
984
1167
|
const mediaType = parseMediaType(requireFlag(ctx.flags, 'media-type'));
|
|
985
|
-
const source = await
|
|
1168
|
+
const source = await resolveSource({
|
|
986
1169
|
flags: ctx.flags,
|
|
987
1170
|
client: ctx.client,
|
|
988
1171
|
fileMediaType: mediaType,
|
|
1172
|
+
preferUploadId: true,
|
|
989
1173
|
});
|
|
1174
|
+
if (!source)
|
|
1175
|
+
throw new Error(missingSourceMessage);
|
|
1176
|
+
if (source.uploadId != null) {
|
|
1177
|
+
print({ source, uploadId: source.uploadId, mediaType });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
990
1180
|
if (source.url != null) {
|
|
991
1181
|
print({ source, url: source.url, mediaType });
|
|
992
1182
|
return;
|
|
@@ -1104,82 +1294,349 @@ const commandFiles = async (subcommand, positionals, ctx) => {
|
|
|
1104
1294
|
}
|
|
1105
1295
|
throw new Error('Unknown files command. Use "files import" or "files upload".');
|
|
1106
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
|
+
};
|
|
1107
1399
|
const commandImage = async (subcommand, ctx) => {
|
|
1108
|
-
if (subcommand !== 'generate')
|
|
1400
|
+
if (subcommand !== 'generate')
|
|
1109
1401
|
throw new Error('Unknown image command. Use "image generate".');
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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',
|
|
1115
1437
|
});
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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 }));
|
|
1146
1468
|
};
|
|
1147
1469
|
const commandMesh = async (subcommand, ctx) => {
|
|
1148
1470
|
if (subcommand === 'generate') {
|
|
1149
|
-
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({
|
|
1150
1508
|
source: await requireSource({
|
|
1151
1509
|
flags: ctx.flags,
|
|
1152
1510
|
client: ctx.client,
|
|
1153
|
-
fileMediaType: '
|
|
1511
|
+
fileMediaType: 'mesh',
|
|
1154
1512
|
}),
|
|
1155
|
-
modelId:
|
|
1513
|
+
modelId: requireFlag(ctx.flags, 'model-id'),
|
|
1156
1514
|
name: getFlag(ctx.flags, 'name'),
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
: parsePositiveIntegerFlag(ctx.flags, 'face-limit', 1),
|
|
1160
|
-
isLowPoly: parseBooleanFlag(ctx.flags, 'low-poly'),
|
|
1515
|
+
faceLevel: getFlag(ctx.flags, 'face-level'),
|
|
1516
|
+
polygonType: getFlag(ctx.flags, 'polygon-type'),
|
|
1161
1517
|
});
|
|
1162
1518
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1163
1519
|
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1164
1520
|
return;
|
|
1165
1521
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
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({
|
|
1168
1598
|
source: await requireSource({
|
|
1169
1599
|
flags: ctx.flags,
|
|
1170
1600
|
client: ctx.client,
|
|
1171
1601
|
fileMediaType: 'mesh',
|
|
1172
1602
|
}),
|
|
1173
|
-
modelId: requireFlag(ctx.flags, 'model-id'),
|
|
1174
1603
|
name: getFlag(ctx.flags, 'name'),
|
|
1175
|
-
faceLevel: getFlag(ctx.flags, 'face-level'),
|
|
1176
|
-
polygonType: getFlag(ctx.flags, 'polygon-type'),
|
|
1177
1604
|
});
|
|
1178
1605
|
const job = await pollIfRequested({ ctx, jobId: queued.jobId });
|
|
1179
1606
|
print(await withOptionalDownloads(ctx.flags, { queued, job }));
|
|
1180
1607
|
return;
|
|
1181
1608
|
}
|
|
1182
|
-
|
|
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".');
|
|
1183
1640
|
};
|
|
1184
1641
|
const commandJobs = async (subcommand, positionals, ctx) => {
|
|
1185
1642
|
const jobId = requirePositional(positionals, 2, 'job-id');
|
|
@@ -1370,14 +1827,17 @@ const firstBlobLocation = (value) => {
|
|
|
1370
1827
|
return undefined;
|
|
1371
1828
|
}
|
|
1372
1829
|
const record = value;
|
|
1373
|
-
if (record.type === 'supabase' &&
|
|
1830
|
+
if (record.type === 'supabase' &&
|
|
1831
|
+
typeof record.bucket === 'string' &&
|
|
1832
|
+
typeof record.path === 'string') {
|
|
1374
1833
|
return {
|
|
1375
1834
|
type: 'supabase',
|
|
1376
1835
|
bucket: record.bucket,
|
|
1377
1836
|
path: record.path,
|
|
1378
1837
|
};
|
|
1379
1838
|
}
|
|
1380
|
-
if (record.type === 'signedSupabase' &&
|
|
1839
|
+
if (record.type === 'signedSupabase' &&
|
|
1840
|
+
typeof record.signedUrl === 'string') {
|
|
1381
1841
|
return {
|
|
1382
1842
|
type: 'signedSupabase',
|
|
1383
1843
|
signedUrl: record.signedUrl,
|
|
@@ -1401,7 +1861,13 @@ const imageSourceFromCompletedJob = (job) => {
|
|
|
1401
1861
|
if (job.resourceId != null && job.resourceId.trim() !== '') {
|
|
1402
1862
|
return { imageAssetId: job.resourceId };
|
|
1403
1863
|
}
|
|
1404
|
-
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
|
+
]));
|
|
1405
1871
|
if (resourceId != null) {
|
|
1406
1872
|
return { imageAssetId: resourceId };
|
|
1407
1873
|
}
|
|
@@ -1416,6 +1882,14 @@ const imageSourceFromCompletedJob = (job) => {
|
|
|
1416
1882
|
throw new Error('Preprocessing completed, but the image job did not include an image source for part splitting.');
|
|
1417
1883
|
};
|
|
1418
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
|
+
};
|
|
1419
1893
|
if (error instanceof AssetHubApiError) {
|
|
1420
1894
|
return {
|
|
1421
1895
|
name: error.name,
|
|
@@ -1440,7 +1914,9 @@ const readyTaskIdsFromStatus = (status) => {
|
|
|
1440
1914
|
return [];
|
|
1441
1915
|
}
|
|
1442
1916
|
return missions.flatMap(mission => {
|
|
1443
|
-
if (mission == null ||
|
|
1917
|
+
if (mission == null ||
|
|
1918
|
+
typeof mission !== 'object' ||
|
|
1919
|
+
!('tasks' in mission)) {
|
|
1444
1920
|
return [];
|
|
1445
1921
|
}
|
|
1446
1922
|
const tasks = mission.tasks;
|
|
@@ -1474,21 +1950,203 @@ const singleMissionIdFromStatus = (status) => {
|
|
|
1474
1950
|
}
|
|
1475
1951
|
return missionId;
|
|
1476
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
|
+
};
|
|
1477
2107
|
const commandProduction = async (subcommand, positionals, ctx) => {
|
|
2108
|
+
if (subcommand === 'agents') {
|
|
2109
|
+
print(sanitizeProductionOutput(await ctx.client.v1.getProductionAgents()));
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
1478
2112
|
if (subcommand === 'analyze') {
|
|
1479
|
-
const
|
|
2113
|
+
const session = await productionSession(ctx, 'production.analyze');
|
|
2114
|
+
const analyzed = await executeRecorded(session, 'production.analyze', {
|
|
1480
2115
|
...(await resolveProductionAnalyzeSource(ctx)),
|
|
1481
2116
|
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1482
2117
|
name: getFlag(ctx.flags, 'name'),
|
|
1483
|
-
});
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
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);
|
|
1492
2150
|
return;
|
|
1493
2151
|
}
|
|
1494
2152
|
if (subcommand === 'status') {
|
|
@@ -1523,41 +2181,182 @@ const commandProduction = async (subcommand, positionals, ctx) => {
|
|
|
1523
2181
|
const agentVersion = partExtractorFlag == null
|
|
1524
2182
|
? await resolvePartExtractorFromMission(ctx, orderId, missionId)
|
|
1525
2183
|
: resolvePartExtractorApiValue(partExtractorFlag);
|
|
1526
|
-
const
|
|
2184
|
+
const session = await productionSession(ctx, 'production.execute');
|
|
2185
|
+
const executed = await executeRecorded(session, 'production.execute', {
|
|
1527
2186
|
orderId,
|
|
1528
2187
|
missionId,
|
|
1529
2188
|
confirmedTaskIds,
|
|
1530
2189
|
agentVersion,
|
|
1531
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 } : {}),
|
|
1532
2229
|
});
|
|
1533
2230
|
const status = hasFlag(ctx.flags, 'wait')
|
|
1534
|
-
? await
|
|
1535
|
-
ctx,
|
|
1536
|
-
orderId,
|
|
1537
|
-
terminalStatuses: productionExecuteTerminalStatuses,
|
|
1538
|
-
})
|
|
2231
|
+
? await watchProductionRun({ ctx, orderId })
|
|
1539
2232
|
: undefined;
|
|
1540
|
-
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));
|
|
1541
2239
|
return;
|
|
1542
2240
|
}
|
|
1543
|
-
|
|
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
|
+
});
|
|
1544
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;
|
|
1545
2295
|
const commandParts = async (subcommand, ctx) => {
|
|
1546
|
-
const runPartSplit = async (source) => {
|
|
2296
|
+
const runPartSplit = async (source, operationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID()) => {
|
|
1547
2297
|
let orderId = source == null ? getFlag(ctx.flags, 'order-id') : undefined;
|
|
1548
2298
|
if (orderId != null) {
|
|
1549
2299
|
orderId = assertFlagHasValue(orderId, '--order-id <id>');
|
|
1550
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;
|
|
1551
2315
|
let analyzed;
|
|
1552
2316
|
let status;
|
|
1553
2317
|
if (orderId == null) {
|
|
1554
|
-
const result = await
|
|
2318
|
+
const result = await executeRecorded(session, 'production.analyze', {
|
|
1555
2319
|
...(source ?? (await resolveProductionAnalyzeSource(ctx))),
|
|
1556
2320
|
agentVersion: resolvePartExtractorForAnalyze(ctx.flags),
|
|
1557
2321
|
name: getFlag(ctx.flags, 'name'),
|
|
1558
|
-
});
|
|
2322
|
+
}, { ...executionOptions(ctx.flags), operationId });
|
|
1559
2323
|
analyzed = result;
|
|
1560
|
-
|
|
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
|
+
}
|
|
1561
2360
|
if (hasFlag(ctx.flags, 'wait') || hasFlag(ctx.flags, 'all-ready')) {
|
|
1562
2361
|
status = await pollProductionStatus({
|
|
1563
2362
|
ctx,
|
|
@@ -1600,13 +2399,21 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1600
2399
|
partExtractionMode !== 'high_quality') {
|
|
1601
2400
|
throw new Error('--part-extraction-mode must be "fast" or "high_quality"');
|
|
1602
2401
|
}
|
|
1603
|
-
|
|
2402
|
+
const result = await executeRecorded(session, 'production.execute', {
|
|
1604
2403
|
orderId,
|
|
1605
2404
|
missionId,
|
|
1606
2405
|
confirmedTaskIds: taskIds,
|
|
1607
2406
|
agentVersion,
|
|
1608
2407
|
partExtractionMode,
|
|
2408
|
+
}, {
|
|
2409
|
+
...executionOptions(ctx.flags),
|
|
2410
|
+
operationId: execution
|
|
2411
|
+
? childOperationId(operationId, 'execute')
|
|
2412
|
+
: operationId,
|
|
2413
|
+
parentRunId: execution?.runId ?? executionOptions(ctx.flags).parentRunId,
|
|
1609
2414
|
});
|
|
2415
|
+
executed = result;
|
|
2416
|
+
execution = result.execution;
|
|
1610
2417
|
if (hasFlag(ctx.flags, 'wait')) {
|
|
1611
2418
|
executionStatus = await pollProductionStatus({
|
|
1612
2419
|
ctx,
|
|
@@ -1615,7 +2422,14 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1615
2422
|
});
|
|
1616
2423
|
}
|
|
1617
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);
|
|
1618
2431
|
return sanitizeProductionOutput({
|
|
2432
|
+
execution,
|
|
1619
2433
|
analyzed,
|
|
1620
2434
|
status,
|
|
1621
2435
|
executed,
|
|
@@ -1623,10 +2437,13 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1623
2437
|
});
|
|
1624
2438
|
};
|
|
1625
2439
|
if (subcommand === 'split') {
|
|
1626
|
-
if (getFlag(ctx.flags, 'order-id') != null &&
|
|
2440
|
+
if (getFlag(ctx.flags, 'order-id') != null &&
|
|
2441
|
+
sourceInputCount(ctx.flags) > 0) {
|
|
1627
2442
|
throw new Error('Pass either --order-id or one image source, not both.');
|
|
1628
2443
|
}
|
|
1629
|
-
|
|
2444
|
+
const result = await runPartSplit();
|
|
2445
|
+
const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
|
|
2446
|
+
print({ ...result, ...(downloads ? { downloads } : {}) });
|
|
1630
2447
|
return;
|
|
1631
2448
|
}
|
|
1632
2449
|
if (subcommand === 'compare') {
|
|
@@ -1637,7 +2454,8 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1637
2454
|
if (preprocessPrompt === 'true') {
|
|
1638
2455
|
throw new Error('Missing required flag value: --preprocess-prompt <text>');
|
|
1639
2456
|
}
|
|
1640
|
-
if (preprocessPrompt != null &&
|
|
2457
|
+
if (preprocessPrompt != null &&
|
|
2458
|
+
getFlagValues(ctx.flags, 'task-id').length > 0) {
|
|
1641
2459
|
throw new Error('parts compare preprocessing creates a second order. Use --all-ready instead of --task-id.');
|
|
1642
2460
|
}
|
|
1643
2461
|
if (preprocessPrompt != null && getFlag(ctx.flags, 'mission-id') != null) {
|
|
@@ -1647,17 +2465,22 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1647
2465
|
if (preprocessModelId === 'true') {
|
|
1648
2466
|
throw new Error('Missing required flag value: --preprocess-model-id <id>');
|
|
1649
2467
|
}
|
|
2468
|
+
const compareSession = await productionSession(ctx, 'production.analyze');
|
|
2469
|
+
const compareOperationId = getFlag(ctx.flags, 'operation-id') ?? randomUUID();
|
|
1650
2470
|
const inputSource = productionAnalyzeSourceFromSource(await requireSource({
|
|
1651
2471
|
flags: ctx.flags,
|
|
1652
2472
|
client: ctx.client,
|
|
1653
2473
|
fileMediaType: 'image',
|
|
2474
|
+
preferUploadId: true,
|
|
1654
2475
|
}));
|
|
1655
|
-
const
|
|
2476
|
+
const durableInput = productionAnalyzeSourceFromSource((await recordedSource(ctx, sourceFromProductionAnalyzeSource(inputSource))));
|
|
2477
|
+
const direct = await runPartSplit(durableInput, compareOperationId);
|
|
1656
2478
|
let preprocessed = null;
|
|
1657
2479
|
if (preprocessPrompt != null && preprocessPrompt !== 'true') {
|
|
2480
|
+
const preprocessOperationId = childOperationId(compareOperationId, 'preprocess');
|
|
1658
2481
|
try {
|
|
1659
|
-
const queued = await
|
|
1660
|
-
source: sourceFromProductionAnalyzeSource(
|
|
2482
|
+
const queued = await executeRecorded(compareSession, 'image.generate', {
|
|
2483
|
+
source: sourceFromProductionAnalyzeSource(durableInput),
|
|
1661
2484
|
prompt: preprocessPrompt,
|
|
1662
2485
|
modelId: preprocessModelId ?? defaultImageModelId,
|
|
1663
2486
|
batchSize: 1,
|
|
@@ -1665,8 +2488,15 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1665
2488
|
seed: getFlag(ctx.flags, 'preprocess-seed') == null
|
|
1666
2489
|
? undefined
|
|
1667
2490
|
: parsePositiveIntegerFlag(ctx.flags, 'preprocess-seed', 1),
|
|
2491
|
+
}, {
|
|
2492
|
+
...executionOptions(ctx.flags),
|
|
2493
|
+
operationId: preprocessOperationId,
|
|
1668
2494
|
});
|
|
1669
|
-
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, {
|
|
1670
2500
|
intervalMs: parsePositiveIntegerFlag(ctx.flags, 'interval-ms', 5000),
|
|
1671
2501
|
timeoutMs: parsePositiveIntegerFlag(ctx.flags, 'timeout-ms', 900000),
|
|
1672
2502
|
onPoll: current => {
|
|
@@ -1684,27 +2514,445 @@ const commandParts = async (subcommand, ctx) => {
|
|
|
1684
2514
|
job,
|
|
1685
2515
|
source: generatedSource,
|
|
1686
2516
|
},
|
|
1687
|
-
split: await runPartSplit(generatedSource),
|
|
2517
|
+
split: await runPartSplit(generatedSource, childOperationId(compareOperationId, 'preprocessed-split')),
|
|
1688
2518
|
};
|
|
1689
2519
|
}
|
|
1690
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);
|
|
1691
2525
|
if (hasFlag(ctx.flags, 'fail-on-preprocess-error')) {
|
|
1692
|
-
throw
|
|
2526
|
+
throw failure;
|
|
1693
2527
|
}
|
|
2528
|
+
process.exitCode =
|
|
2529
|
+
failure instanceof CliExecutionError ? failure.exitCode : 1;
|
|
1694
2530
|
preprocessed = {
|
|
1695
2531
|
ok: false,
|
|
1696
|
-
error: errorToOutput(
|
|
2532
|
+
error: errorToOutput(failure),
|
|
1697
2533
|
};
|
|
1698
2534
|
}
|
|
1699
2535
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}));
|
|
2536
|
+
const result = { direct, preprocessed };
|
|
2537
|
+
const downloads = await maybeDownloadUrls(ctx.flags, recordedOutputs(result));
|
|
2538
|
+
print({ ...result, ...(downloads ? { downloads } : {}) });
|
|
1704
2539
|
return;
|
|
1705
2540
|
}
|
|
1706
2541
|
throw new Error('Unknown parts command. Use "parts split" or "parts compare".');
|
|
1707
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
|
+
};
|
|
1708
2956
|
const run = async () => {
|
|
1709
2957
|
const parsed = parseArgs(argv.slice(2));
|
|
1710
2958
|
if (hasFlag(parsed.flags, 'help') || parsed.positionals.length === 0) {
|
|
@@ -1712,12 +2960,84 @@ const run = async () => {
|
|
|
1712
2960
|
return;
|
|
1713
2961
|
}
|
|
1714
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
|
+
}
|
|
1715
3009
|
if (command === 'auth') {
|
|
1716
3010
|
await commandAuth(subcommand, parsed.flags);
|
|
1717
3011
|
return;
|
|
1718
3012
|
}
|
|
1719
|
-
|
|
3013
|
+
if (command === 'autopilot') {
|
|
3014
|
+
throw new Error('The experimental autopilot demo requires the AssetHub monorepo with @assethub/autopilot-core installed. Standalone project workflow commands are available without it.');
|
|
3015
|
+
}
|
|
3016
|
+
const ctx = await createContext(parsed.flags, argv.slice(2));
|
|
1720
3017
|
switch (command) {
|
|
3018
|
+
case 'language':
|
|
3019
|
+
await commandLanguage(subcommand, ctx);
|
|
3020
|
+
return;
|
|
3021
|
+
case 'moodboard':
|
|
3022
|
+
await commandMoodboard(subcommand, parsed.positionals, ctx);
|
|
3023
|
+
return;
|
|
3024
|
+
case 'canvas':
|
|
3025
|
+
await commandCanvas(subcommand, parsed.positionals, ctx);
|
|
3026
|
+
return;
|
|
3027
|
+
case 'graph':
|
|
3028
|
+
await commandGraph(subcommand, ctx);
|
|
3029
|
+
return;
|
|
3030
|
+
case 'evaluations':
|
|
3031
|
+
await commandEvaluations(subcommand, parsed.positionals, ctx);
|
|
3032
|
+
return;
|
|
3033
|
+
case 'evaluate':
|
|
3034
|
+
if (subcommand !== 'list')
|
|
3035
|
+
throw new Error('Server evaluators are capability-gated. Use evaluate list, or evaluations submit for external agent reports.');
|
|
3036
|
+
print((await ctx.client.v2.getCapabilities()).evaluators);
|
|
3037
|
+
return;
|
|
3038
|
+
case 'capabilities':
|
|
3039
|
+
print(await ctx.client.v2.getCapabilities());
|
|
3040
|
+
return;
|
|
1721
3041
|
case 'models':
|
|
1722
3042
|
await commandModels(subcommand, parsed.positionals, ctx);
|
|
1723
3043
|
return;
|
|
@@ -1733,12 +3053,21 @@ const run = async () => {
|
|
|
1733
3053
|
case 'mesh':
|
|
1734
3054
|
await commandMesh(subcommand, ctx);
|
|
1735
3055
|
return;
|
|
3056
|
+
case 'rig':
|
|
3057
|
+
await commandRig(subcommand, ctx);
|
|
3058
|
+
return;
|
|
3059
|
+
case 'animate':
|
|
3060
|
+
await commandAnimate(subcommand, ctx);
|
|
3061
|
+
return;
|
|
1736
3062
|
case 'jobs':
|
|
1737
3063
|
await commandJobs(subcommand, parsed.positionals, ctx);
|
|
1738
3064
|
return;
|
|
1739
3065
|
case 'production':
|
|
1740
3066
|
await commandProduction(subcommand, parsed.positionals, ctx);
|
|
1741
3067
|
return;
|
|
3068
|
+
case 'runs':
|
|
3069
|
+
await commandRuns(subcommand, parsed.positionals, ctx);
|
|
3070
|
+
return;
|
|
1742
3071
|
case 'parts':
|
|
1743
3072
|
await commandParts(subcommand, ctx);
|
|
1744
3073
|
return;
|
|
@@ -1746,15 +3075,64 @@ const run = async () => {
|
|
|
1746
3075
|
throw new Error(`Unknown command: ${command}`);
|
|
1747
3076
|
}
|
|
1748
3077
|
};
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
3078
|
+
const isMainModule = (() => {
|
|
3079
|
+
const entry = argv[1];
|
|
3080
|
+
if (entry == null)
|
|
3081
|
+
return false;
|
|
3082
|
+
try {
|
|
3083
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
|
|
1755
3084
|
}
|
|
1756
|
-
|
|
1757
|
-
|
|
3085
|
+
catch {
|
|
3086
|
+
return false;
|
|
1758
3087
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
3088
|
+
})();
|
|
3089
|
+
if (isMainModule) {
|
|
3090
|
+
process.once('SIGINT', () => {
|
|
3091
|
+
print({
|
|
3092
|
+
error: {
|
|
3093
|
+
code: 'INTERRUPTED',
|
|
3094
|
+
message: 'Client interrupted; server execution continues',
|
|
3095
|
+
},
|
|
3096
|
+
...activeExecution,
|
|
3097
|
+
});
|
|
3098
|
+
exit(130);
|
|
3099
|
+
});
|
|
3100
|
+
run().catch((error) => {
|
|
3101
|
+
if (error instanceof AssetHubApiError) {
|
|
3102
|
+
stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
|
|
3103
|
+
}
|
|
3104
|
+
else if (error instanceof Error) {
|
|
3105
|
+
stderr.write(`${error.message}\n`);
|
|
3106
|
+
}
|
|
3107
|
+
else {
|
|
3108
|
+
stderr.write(`${String(error)}\n`);
|
|
3109
|
+
}
|
|
3110
|
+
print({
|
|
3111
|
+
error: {
|
|
3112
|
+
code: error instanceof AssetHubApiError
|
|
3113
|
+
? error.code
|
|
3114
|
+
: error instanceof CliExecutionError && error.exitCode === 3
|
|
3115
|
+
? 'WAIT_TIMEOUT'
|
|
3116
|
+
: 'CLI_ERROR',
|
|
3117
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3118
|
+
},
|
|
3119
|
+
runId: error instanceof CliExecutionError
|
|
3120
|
+
? error.runId
|
|
3121
|
+
: activeExecution.runId,
|
|
3122
|
+
operationId: error instanceof CliExecutionError
|
|
3123
|
+
? error.operationId
|
|
3124
|
+
: activeExecution.operationId,
|
|
3125
|
+
execution: error instanceof CliExecutionError
|
|
3126
|
+
? error.execution
|
|
3127
|
+
: activeExecution.execution,
|
|
3128
|
+
});
|
|
3129
|
+
exit(error instanceof CliExecutionError
|
|
3130
|
+
? error.exitCode
|
|
3131
|
+
: error instanceof AssetHubApiError &&
|
|
3132
|
+
error.status >= 500 &&
|
|
3133
|
+
error.code !== 'CANVAS_EXECUTION_UNAVAILABLE' &&
|
|
3134
|
+
error.code !== 'EVALUATOR_UNAVAILABLE'
|
|
3135
|
+
? 1
|
|
3136
|
+
: 2);
|
|
3137
|
+
});
|
|
3138
|
+
}
|