@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.
Files changed (47) hide show
  1. package/README.md +289 -5
  2. package/dist/canvas.d.ts +27 -0
  3. package/dist/canvas.d.ts.map +1 -0
  4. package/dist/canvas.js +82 -0
  5. package/dist/canvasAssetImport.d.ts +45 -0
  6. package/dist/canvasAssetImport.d.ts.map +1 -0
  7. package/dist/canvasAssetImport.js +154 -0
  8. package/dist/canvasComparison.d.ts +21 -0
  9. package/dist/canvasComparison.d.ts.map +1 -0
  10. package/dist/canvasComparison.js +58 -0
  11. package/dist/execution.d.ts +175 -0
  12. package/dist/execution.d.ts.map +1 -0
  13. package/dist/execution.js +234 -0
  14. package/dist/index.d.ts +94 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1567 -189
  17. package/dist/projectSources.d.ts +65 -0
  18. package/dist/projectSources.d.ts.map +1 -0
  19. package/dist/projectSources.js +369 -0
  20. package/dist/projectSourcesWorkbook.d.ts +3 -0
  21. package/dist/projectSourcesWorkbook.d.ts.map +1 -0
  22. package/dist/projectSourcesWorkbook.js +143 -0
  23. package/dist/runsUpload/artifactGraphModel.d.ts +48 -0
  24. package/dist/runsUpload/artifactGraphModel.d.ts.map +1 -0
  25. package/dist/runsUpload/artifactGraphModel.js +127 -0
  26. package/dist/runsUpload/blobRefs.d.ts +13 -0
  27. package/dist/runsUpload/blobRefs.d.ts.map +1 -0
  28. package/dist/runsUpload/blobRefs.js +73 -0
  29. package/dist/runsUpload/buildRunUpload.d.ts +63 -0
  30. package/dist/runsUpload/buildRunUpload.d.ts.map +1 -0
  31. package/dist/runsUpload/buildRunUpload.js +428 -0
  32. package/dist/runsUpload/canonicalJson.d.ts +5 -0
  33. package/dist/runsUpload/canonicalJson.d.ts.map +1 -0
  34. package/dist/runsUpload/canonicalJson.js +108 -0
  35. package/dist/runsUpload/controlEndpoints.d.ts +63 -0
  36. package/dist/runsUpload/controlEndpoints.d.ts.map +1 -0
  37. package/dist/runsUpload/controlEndpoints.js +64 -0
  38. package/dist/runsUpload/graphFolder.d.ts +25 -0
  39. package/dist/runsUpload/graphFolder.d.ts.map +1 -0
  40. package/dist/runsUpload/graphFolder.js +147 -0
  41. package/dist/runsUpload/runUploadError.d.ts +23 -0
  42. package/dist/runsUpload/runUploadError.d.ts.map +1 -0
  43. package/dist/runsUpload/runUploadError.js +24 -0
  44. package/dist/runsUpload/uploadRun.d.ts +43 -0
  45. package/dist/runsUpload/uploadRun.d.ts.map +1 -0
  46. package/dist/runsUpload/uploadRun.js +217 -0
  47. package/package.json +11 -5
package/dist/index.js CHANGED
@@ -1,32 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from 'node:child_process';
3
- import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
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
- const defaultImageModelId = 'imageGen.nanoBanana.pro.openrouter';
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 image generate --prompt <text> [--file <path> | --source-url <url> | --stdin | --stdin-base64 | --stdin-data-uri | --stdin-json | --source-json <json|@file|@-> | --data-uri <uri> | --clipboard] [--file-name <name>] [--content-type <type>] [--model-id <id>] [--batch-size <n>] [--wait] [--download --out-dir <dir>] [--api-version v1|v2]
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).map(([key, entry]) => [
271
+ return Object.fromEntries(Object.entries(value)
272
+ .filter(([key]) => key !== 'publicAccessToken')
273
+ .map(([key, entry]) => [
215
274
  key,
216
- key === 'agentVersion' && typeof entry === 'string'
217
- ? publicPartExtractorName(entry)
218
- : sanitizeProductionOutput(entry),
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 ? profile : (fallbackProfileName ?? profile),
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.replace(/\s+/g, '').replace(/-/g, '+').replace(/_/g, '/');
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') ?? getFlag(flags, 'image-url') ?? getFlag(flags, '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 fileRefValue = value.fileRef ?? value.file_ref ?? value.imageBlobLocation ?? value.image_blob_location ?? value.blobLocation ?? value.blob_location;
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, ['sourceUrl', 'source_url', 'url', 'publicUrl', 'public_url']);
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: { kind: 'file', path: assertFlagHasValue(filePath, '--file <path>') },
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') ?? defaultFileNameForMediaType(fileMediaType),
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({ descriptor, client, mediaType: fileMediaType });
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') ?? defaultFileNameForMediaType(fileMediaType),
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({ descriptor, client, mediaType: fileMediaType });
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') ?? defaultFileNameForMediaType(fileMediaType),
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') ?? defaultFileNameForMediaType(fileMediaType), fileMediaType, getContentTypeFlag(flags)),
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
- if (source.resourceId != null) {
913
- return { imageAssetId: source.resourceId };
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.resourceId != null) {
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
- return { imageBlobLocation: source.fileRef };
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
- return { fileRef: source.imageBlobLocation };
1049
+ if (source.imageBlobLocation)
1050
+ return { fileRef: source.imageBlobLocation };
1051
+ throw new Error(missingSourceMessage);
940
1052
  };
941
- const productionAnalyzeTerminalStatuses = new Set(['ready', 'failed']);
1053
+ const productionAnalyzeTerminalStatuses = new Set([
1054
+ 'ready',
1055
+ 'completed',
1056
+ 'failed',
1057
+ ]);
942
1058
  const productionExecuteTerminalStatuses = new Set(['completed', 'failed']);
943
- const pollProductionStatus = async ({ ctx, orderId, terminalStatuses, }) => {
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', 900000);
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 orderStatus = status.order.status;
950
- stderr.write(`[poll] order=${orderId} status=${orderStatus}\n`);
951
- if (terminalStatuses.has(orderStatus)) {
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(`Polling timeout: order=${orderId} did not reach terminal state within ${timeoutMs}ms`);
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 requireSource({
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
- const source = await resolveSource({
1112
- flags: ctx.flags,
1113
- client: ctx.client,
1114
- fileMediaType: 'image',
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
- const apiVersion = getFlag(ctx.flags, 'api-version') ?? 'v1';
1117
- if (apiVersion !== 'v1' && apiVersion !== 'v2') {
1118
- throw new Error('--api-version must be "v1" or "v2"');
1119
- }
1120
- const commonInput = {
1121
- prompt: requireFlag(ctx.flags, 'prompt'),
1122
- modelId: getFlag(ctx.flags, 'model-id') ?? defaultImageModelId,
1123
- batchSize: parsePositiveIntegerFlag(ctx.flags, 'batch-size', 1),
1124
- negativePrompt: getFlag(ctx.flags, 'negative-prompt'),
1125
- seed: getFlag(ctx.flags, 'seed') == null
1126
- ? undefined
1127
- : parsePositiveIntegerFlag(ctx.flags, 'seed', 1),
1128
- steps: getFlag(ctx.flags, 'steps') == null
1129
- ? undefined
1130
- : parsePositiveIntegerFlag(ctx.flags, 'steps', 1),
1131
- cfgScale: getFlag(ctx.flags, 'cfg-scale') == null
1132
- ? undefined
1133
- : Number(getFlag(ctx.flags, 'cfg-scale')),
1134
- };
1135
- const queued = apiVersion === 'v1'
1136
- ? await ctx.client.v1.generateImage({
1137
- ...commonInput,
1138
- ...sourceToV1ImageInput(source),
1139
- })
1140
- : await ctx.client.v2.generateImage({
1141
- ...commonInput,
1142
- source,
1143
- });
1144
- const job = await pollIfRequested({ ctx, jobId: queued.jobId, apiVersion });
1145
- print(await withOptionalDownloads(ctx.flags, { queued, job, apiVersion }));
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 queued = await ctx.client.v2.generateMesh({
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: 'image',
1511
+ fileMediaType: 'mesh',
1154
1512
  }),
1155
- modelId: getFlag(ctx.flags, 'model-id') ?? defaultMeshModelId,
1513
+ modelId: requireFlag(ctx.flags, 'model-id'),
1156
1514
  name: getFlag(ctx.flags, 'name'),
1157
- faceLimit: getFlag(ctx.flags, 'face-limit') == null
1158
- ? undefined
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
- if (subcommand === 'retopo') {
1167
- const queued = await ctx.client.v2.retopo({
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
- throw new Error('Unknown mesh command. Use "mesh generate" or "mesh retopo".');
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' && typeof record.bucket === 'string' && typeof record.path === 'string') {
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' && typeof record.signedUrl === 'string') {
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(['resourceId', 'resource_id', 'assetId', 'asset_id', 'imageAssetId']));
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 || typeof mission !== 'object' || !('tasks' in mission)) {
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 analyzed = await ctx.client.v1.analyzeProduction({
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
- const status = hasFlag(ctx.flags, 'wait')
1485
- ? await pollProductionStatus({
1486
- ctx,
1487
- orderId: analyzed.orderId,
1488
- terminalStatuses: productionAnalyzeTerminalStatuses,
1489
- })
1490
- : undefined;
1491
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ analyzed, status })));
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 executed = await ctx.client.v1.executeProduction({
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 pollProductionStatus({
1535
- ctx,
1536
- orderId,
1537
- terminalStatuses: productionExecuteTerminalStatuses,
1538
- })
2231
+ ? await watchProductionRun({ ctx, orderId })
1539
2232
  : undefined;
1540
- print(await withOptionalDownloads(ctx.flags, sanitizeProductionOutput({ executed, status })));
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
- throw new Error('Unknown production command. Use "production analyze", "production status", or "production execute".');
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 ctx.client.v1.analyzeProduction({
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
- orderId = result.orderId;
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
- executed = await ctx.client.v1.executeProduction({
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 && sourceInputCount(ctx.flags) > 0) {
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
- print(await withOptionalDownloads(ctx.flags, await runPartSplit()));
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 && getFlagValues(ctx.flags, 'task-id').length > 0) {
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 direct = await runPartSplit(inputSource);
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 ctx.client.v2.generateImage({
1660
- source: sourceFromProductionAnalyzeSource(inputSource),
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 job = await ctx.client.v2.pollJob(queued.jobId, {
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 error;
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(error),
2532
+ error: errorToOutput(failure),
1697
2533
  };
1698
2534
  }
1699
2535
  }
1700
- print(await withOptionalDownloads(ctx.flags, {
1701
- direct,
1702
- preprocessed,
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
- const ctx = await createContext(parsed.flags);
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
- run().catch((error) => {
1750
- if (error instanceof AssetHubApiError) {
1751
- stderr.write(`AssetHub API error [${error.status}] ${error.code}: ${error.message}\n`);
1752
- }
1753
- else if (error instanceof Error) {
1754
- stderr.write(`${error.message}\n`);
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
- else {
1757
- stderr.write(`${String(error)}\n`);
3085
+ catch {
3086
+ return false;
1758
3087
  }
1759
- exit(1);
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
+ }