@meshy-ai/cli 0.1.3 → 0.3.0

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 (293) hide show
  1. package/.env.example +8 -2
  2. package/README.md +342 -108
  3. package/dist/client/endpoints/animation-catalog.d.ts +37 -0
  4. package/dist/client/endpoints/animation-catalog.d.ts.map +1 -0
  5. package/dist/client/endpoints/animation-catalog.js +55 -0
  6. package/dist/client/endpoints/animation-catalog.js.map +1 -0
  7. package/dist/client/endpoints/balance.d.ts +5 -0
  8. package/dist/client/endpoints/balance.d.ts.map +1 -1
  9. package/dist/client/endpoints/balance.js +5 -1
  10. package/dist/client/endpoints/balance.js.map +1 -1
  11. package/dist/client/endpoints/base.d.ts +41 -7
  12. package/dist/client/endpoints/base.d.ts.map +1 -1
  13. package/dist/client/endpoints/base.js +61 -40
  14. package/dist/client/endpoints/base.js.map +1 -1
  15. package/dist/client/endpoints/showcases.d.ts +31 -0
  16. package/dist/client/endpoints/showcases.d.ts.map +1 -0
  17. package/dist/client/endpoints/showcases.js +49 -0
  18. package/dist/client/endpoints/showcases.js.map +1 -0
  19. package/dist/client/errors.d.ts +1 -0
  20. package/dist/client/errors.d.ts.map +1 -1
  21. package/dist/client/errors.js +1 -1
  22. package/dist/client/errors.js.map +1 -1
  23. package/dist/client/index.d.ts +47 -38
  24. package/dist/client/index.d.ts.map +1 -1
  25. package/dist/client/index.js +89 -120
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/resource-registry.d.ts +97 -0
  28. package/dist/client/resource-registry.d.ts.map +1 -0
  29. package/dist/client/resource-registry.js +157 -0
  30. package/dist/client/resource-registry.js.map +1 -0
  31. package/dist/client/transport.d.ts +99 -0
  32. package/dist/client/transport.d.ts.map +1 -0
  33. package/dist/client/transport.js +320 -0
  34. package/dist/client/transport.js.map +1 -0
  35. package/dist/client/types.d.ts +28 -221
  36. package/dist/client/types.d.ts.map +1 -1
  37. package/dist/client/types.js +28 -6
  38. package/dist/client/types.js.map +1 -1
  39. package/dist/cmd/animation-catalog.d.ts +26 -0
  40. package/dist/cmd/animation-catalog.d.ts.map +1 -0
  41. package/dist/cmd/animation-catalog.js +80 -0
  42. package/dist/cmd/animation-catalog.js.map +1 -0
  43. package/dist/cmd/api.d.ts +3 -2
  44. package/dist/cmd/api.d.ts.map +1 -1
  45. package/dist/cmd/api.js +22 -8
  46. package/dist/cmd/api.js.map +1 -1
  47. package/dist/cmd/auth.d.ts.map +1 -1
  48. package/dist/cmd/auth.js +5 -0
  49. package/dist/cmd/auth.js.map +1 -1
  50. package/dist/cmd/balance.d.ts.map +1 -1
  51. package/dist/cmd/balance.js +10 -6
  52. package/dist/cmd/balance.js.map +1 -1
  53. package/dist/cmd/creative-lab.d.ts +26 -0
  54. package/dist/cmd/creative-lab.d.ts.map +1 -0
  55. package/dist/cmd/creative-lab.js +256 -0
  56. package/dist/cmd/creative-lab.js.map +1 -0
  57. package/dist/cmd/delete.d.ts.map +1 -1
  58. package/dist/cmd/delete.js +6 -7
  59. package/dist/cmd/delete.js.map +1 -1
  60. package/dist/cmd/doctor.d.ts +23 -0
  61. package/dist/cmd/doctor.d.ts.map +1 -0
  62. package/dist/cmd/doctor.js +61 -0
  63. package/dist/cmd/doctor.js.map +1 -0
  64. package/dist/cmd/download.d.ts +17 -0
  65. package/dist/cmd/download.d.ts.map +1 -0
  66. package/dist/cmd/download.js +436 -0
  67. package/dist/cmd/download.js.map +1 -0
  68. package/dist/cmd/inspect.d.ts +63 -0
  69. package/dist/cmd/inspect.d.ts.map +1 -0
  70. package/dist/cmd/inspect.js +179 -0
  71. package/dist/cmd/inspect.js.map +1 -0
  72. package/dist/cmd/make.d.ts +13 -3
  73. package/dist/cmd/make.d.ts.map +1 -1
  74. package/dist/cmd/make.js +240 -36
  75. package/dist/cmd/make.js.map +1 -1
  76. package/dist/cmd/mesh.d.ts +13 -0
  77. package/dist/cmd/mesh.d.ts.map +1 -0
  78. package/dist/cmd/mesh.js +67 -0
  79. package/dist/cmd/mesh.js.map +1 -0
  80. package/dist/cmd/project.d.ts +16 -0
  81. package/dist/cmd/project.d.ts.map +1 -0
  82. package/dist/cmd/project.js +136 -0
  83. package/dist/cmd/project.js.map +1 -0
  84. package/dist/cmd/resources.d.ts +5 -0
  85. package/dist/cmd/resources.d.ts.map +1 -1
  86. package/dist/cmd/resources.js +38 -10
  87. package/dist/cmd/resources.js.map +1 -1
  88. package/dist/cmd/showcases.d.ts +10 -0
  89. package/dist/cmd/showcases.d.ts.map +1 -0
  90. package/dist/cmd/showcases.js +63 -0
  91. package/dist/cmd/showcases.js.map +1 -0
  92. package/dist/cmd/slicer.d.ts +12 -0
  93. package/dist/cmd/slicer.d.ts.map +1 -0
  94. package/dist/cmd/slicer.js +46 -0
  95. package/dist/cmd/slicer.js.map +1 -0
  96. package/dist/cmd/uv-unwrap.d.ts +16 -0
  97. package/dist/cmd/uv-unwrap.d.ts.map +1 -0
  98. package/dist/cmd/uv-unwrap.js +52 -0
  99. package/dist/cmd/uv-unwrap.js.map +1 -0
  100. package/dist/index.d.ts +13 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +141 -8
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/artifacts.d.ts +97 -0
  105. package/dist/internal/artifacts.d.ts.map +1 -0
  106. package/dist/internal/artifacts.js +280 -0
  107. package/dist/internal/artifacts.js.map +1 -0
  108. package/dist/internal/atomic-file.d.ts +39 -0
  109. package/dist/internal/atomic-file.d.ts.map +1 -0
  110. package/dist/internal/atomic-file.js +160 -0
  111. package/dist/internal/atomic-file.js.map +1 -0
  112. package/dist/internal/command-helpers.d.ts +46 -0
  113. package/dist/internal/command-helpers.d.ts.map +1 -0
  114. package/dist/internal/command-helpers.js +61 -0
  115. package/dist/internal/command-helpers.js.map +1 -0
  116. package/dist/internal/config.d.ts +48 -5
  117. package/dist/internal/config.d.ts.map +1 -1
  118. package/dist/internal/config.js +95 -6
  119. package/dist/internal/config.js.map +1 -1
  120. package/dist/internal/context.d.ts +25 -0
  121. package/dist/internal/context.d.ts.map +1 -0
  122. package/dist/internal/context.js +37 -0
  123. package/dist/internal/context.js.map +1 -0
  124. package/dist/internal/credentials.d.ts +11 -0
  125. package/dist/internal/credentials.d.ts.map +1 -1
  126. package/dist/internal/credentials.js +2 -0
  127. package/dist/internal/credentials.js.map +1 -1
  128. package/dist/internal/device-state.js.map +1 -1
  129. package/dist/internal/device.d.ts.map +1 -1
  130. package/dist/internal/doctor.d.ts +94 -0
  131. package/dist/internal/doctor.d.ts.map +1 -0
  132. package/dist/internal/doctor.js +280 -0
  133. package/dist/internal/doctor.js.map +1 -0
  134. package/dist/internal/download.d.ts +147 -13
  135. package/dist/internal/download.d.ts.map +1 -1
  136. package/dist/internal/download.js +735 -142
  137. package/dist/internal/download.js.map +1 -1
  138. package/dist/internal/env-file.d.ts +29 -0
  139. package/dist/internal/env-file.d.ts.map +1 -0
  140. package/dist/internal/env-file.js +95 -0
  141. package/dist/internal/env-file.js.map +1 -0
  142. package/dist/internal/errors.d.ts +74 -1
  143. package/dist/internal/errors.d.ts.map +1 -1
  144. package/dist/internal/errors.js +173 -1
  145. package/dist/internal/errors.js.map +1 -1
  146. package/dist/internal/file-input.d.ts +52 -12
  147. package/dist/internal/file-input.d.ts.map +1 -1
  148. package/dist/internal/file-input.js +162 -33
  149. package/dist/internal/file-input.js.map +1 -1
  150. package/dist/internal/flags.js.map +1 -1
  151. package/dist/internal/global-options.d.ts +6 -4
  152. package/dist/internal/global-options.d.ts.map +1 -1
  153. package/dist/internal/global-options.js +19 -4
  154. package/dist/internal/global-options.js.map +1 -1
  155. package/dist/internal/headless.js.map +1 -1
  156. package/dist/internal/image-models.d.ts.map +1 -1
  157. package/dist/internal/inspect.d.ts +67 -0
  158. package/dist/internal/inspect.d.ts.map +1 -0
  159. package/dist/internal/inspect.js +111 -0
  160. package/dist/internal/inspect.js.map +1 -0
  161. package/dist/internal/lock.d.ts +19 -0
  162. package/dist/internal/lock.d.ts.map +1 -0
  163. package/dist/internal/lock.js +66 -0
  164. package/dist/internal/lock.js.map +1 -0
  165. package/dist/internal/logger.d.ts.map +1 -1
  166. package/dist/internal/material-links.d.ts +115 -0
  167. package/dist/internal/material-links.d.ts.map +1 -0
  168. package/dist/internal/material-links.js +577 -0
  169. package/dist/internal/material-links.js.map +1 -0
  170. package/dist/internal/obj-transform.d.ts +115 -0
  171. package/dist/internal/obj-transform.d.ts.map +1 -0
  172. package/dist/internal/obj-transform.js +649 -0
  173. package/dist/internal/obj-transform.js.map +1 -0
  174. package/dist/internal/operation-store.d.ts +123 -0
  175. package/dist/internal/operation-store.d.ts.map +1 -0
  176. package/dist/internal/operation-store.js +229 -0
  177. package/dist/internal/operation-store.js.map +1 -0
  178. package/dist/internal/output.d.ts +20 -2
  179. package/dist/internal/output.d.ts.map +1 -1
  180. package/dist/internal/output.js +32 -4
  181. package/dist/internal/output.js.map +1 -1
  182. package/dist/internal/paths.d.ts +89 -0
  183. package/dist/internal/paths.d.ts.map +1 -0
  184. package/dist/internal/paths.js +168 -0
  185. package/dist/internal/paths.js.map +1 -0
  186. package/dist/internal/payload.d.ts +15 -0
  187. package/dist/internal/payload.d.ts.map +1 -1
  188. package/dist/internal/payload.js +50 -3
  189. package/dist/internal/payload.js.map +1 -1
  190. package/dist/internal/poll.d.ts +30 -4
  191. package/dist/internal/poll.d.ts.map +1 -1
  192. package/dist/internal/poll.js +99 -16
  193. package/dist/internal/poll.js.map +1 -1
  194. package/dist/internal/pricing.d.ts.map +1 -1
  195. package/dist/internal/project-store.d.ts +197 -0
  196. package/dist/internal/project-store.d.ts.map +1 -0
  197. package/dist/internal/project-store.js +467 -0
  198. package/dist/internal/project-store.js.map +1 -0
  199. package/dist/internal/result.d.ts +50 -0
  200. package/dist/internal/result.d.ts.map +1 -0
  201. package/dist/internal/result.js +41 -0
  202. package/dist/internal/result.js.map +1 -0
  203. package/dist/internal/runtime.d.ts +35 -5
  204. package/dist/internal/runtime.d.ts.map +1 -1
  205. package/dist/internal/runtime.js +68 -18
  206. package/dist/internal/runtime.js.map +1 -1
  207. package/dist/internal/slicers.d.ts +117 -0
  208. package/dist/internal/slicers.d.ts.map +1 -0
  209. package/dist/internal/slicers.js +314 -0
  210. package/dist/internal/slicers.js.map +1 -0
  211. package/dist/internal/stream.d.ts +58 -0
  212. package/dist/internal/stream.d.ts.map +1 -0
  213. package/dist/internal/stream.js +280 -0
  214. package/dist/internal/stream.js.map +1 -0
  215. package/dist/internal/task-command.d.ts +128 -11
  216. package/dist/internal/task-command.d.ts.map +1 -1
  217. package/dist/internal/task-command.js +991 -111
  218. package/dist/internal/task-command.js.map +1 -1
  219. package/dist/internal/task-view.d.ts +56 -0
  220. package/dist/internal/task-view.d.ts.map +1 -0
  221. package/dist/internal/task-view.js +95 -0
  222. package/dist/internal/task-view.js.map +1 -0
  223. package/dist/internal/update-notifier.d.ts +5 -1
  224. package/dist/internal/update-notifier.d.ts.map +1 -1
  225. package/dist/internal/update-notifier.js +7 -3
  226. package/dist/internal/update-notifier.js.map +1 -1
  227. package/dist/internal/version.js.map +1 -1
  228. package/dist/root.d.ts +10 -0
  229. package/dist/root.d.ts.map +1 -1
  230. package/dist/root.js +75 -10
  231. package/dist/root.js.map +1 -1
  232. package/package.json +11 -10
  233. package/skills/meshy-cli/SKILL.md +140 -59
  234. package/dist/client/endpoints/analyze-printability.d.ts +0 -5
  235. package/dist/client/endpoints/analyze-printability.d.ts.map +0 -1
  236. package/dist/client/endpoints/analyze-printability.js +0 -7
  237. package/dist/client/endpoints/analyze-printability.js.map +0 -1
  238. package/dist/client/endpoints/animate.d.ts +0 -6
  239. package/dist/client/endpoints/animate.d.ts.map +0 -1
  240. package/dist/client/endpoints/animate.js +0 -8
  241. package/dist/client/endpoints/animate.js.map +0 -1
  242. package/dist/client/endpoints/convert.d.ts +0 -5
  243. package/dist/client/endpoints/convert.d.ts.map +0 -1
  244. package/dist/client/endpoints/convert.js +0 -7
  245. package/dist/client/endpoints/convert.js.map +0 -1
  246. package/dist/client/endpoints/image-to-3d.d.ts +0 -5
  247. package/dist/client/endpoints/image-to-3d.d.ts.map +0 -1
  248. package/dist/client/endpoints/image-to-3d.js +0 -7
  249. package/dist/client/endpoints/image-to-3d.js.map +0 -1
  250. package/dist/client/endpoints/image-to-image.d.ts +0 -5
  251. package/dist/client/endpoints/image-to-image.d.ts.map +0 -1
  252. package/dist/client/endpoints/image-to-image.js +0 -7
  253. package/dist/client/endpoints/image-to-image.js.map +0 -1
  254. package/dist/client/endpoints/multi-color-print.d.ts +0 -5
  255. package/dist/client/endpoints/multi-color-print.d.ts.map +0 -1
  256. package/dist/client/endpoints/multi-color-print.js +0 -7
  257. package/dist/client/endpoints/multi-color-print.js.map +0 -1
  258. package/dist/client/endpoints/multi-image-to-3d.d.ts +0 -5
  259. package/dist/client/endpoints/multi-image-to-3d.d.ts.map +0 -1
  260. package/dist/client/endpoints/multi-image-to-3d.js +0 -7
  261. package/dist/client/endpoints/multi-image-to-3d.js.map +0 -1
  262. package/dist/client/endpoints/remesh.d.ts +0 -5
  263. package/dist/client/endpoints/remesh.d.ts.map +0 -1
  264. package/dist/client/endpoints/remesh.js +0 -7
  265. package/dist/client/endpoints/remesh.js.map +0 -1
  266. package/dist/client/endpoints/repair-printability.d.ts +0 -5
  267. package/dist/client/endpoints/repair-printability.d.ts.map +0 -1
  268. package/dist/client/endpoints/repair-printability.js +0 -7
  269. package/dist/client/endpoints/repair-printability.js.map +0 -1
  270. package/dist/client/endpoints/resize.d.ts +0 -5
  271. package/dist/client/endpoints/resize.d.ts.map +0 -1
  272. package/dist/client/endpoints/resize.js +0 -7
  273. package/dist/client/endpoints/resize.js.map +0 -1
  274. package/dist/client/endpoints/retexture.d.ts +0 -5
  275. package/dist/client/endpoints/retexture.d.ts.map +0 -1
  276. package/dist/client/endpoints/retexture.js +0 -7
  277. package/dist/client/endpoints/retexture.js.map +0 -1
  278. package/dist/client/endpoints/rigging.d.ts +0 -6
  279. package/dist/client/endpoints/rigging.d.ts.map +0 -1
  280. package/dist/client/endpoints/rigging.js +0 -8
  281. package/dist/client/endpoints/rigging.js.map +0 -1
  282. package/dist/client/endpoints/text-to-3d.d.ts +0 -6
  283. package/dist/client/endpoints/text-to-3d.d.ts.map +0 -1
  284. package/dist/client/endpoints/text-to-3d.js +0 -8
  285. package/dist/client/endpoints/text-to-3d.js.map +0 -1
  286. package/dist/client/endpoints/text-to-image.d.ts +0 -5
  287. package/dist/client/endpoints/text-to-image.d.ts.map +0 -1
  288. package/dist/client/endpoints/text-to-image.js +0 -7
  289. package/dist/client/endpoints/text-to-image.js.map +0 -1
  290. package/dist/client/endpoints/text-to-motion.d.ts +0 -6
  291. package/dist/client/endpoints/text-to-motion.d.ts.map +0 -1
  292. package/dist/client/endpoints/text-to-motion.js +0 -8
  293. package/dist/client/endpoints/text-to-motion.js.map +0 -1
@@ -1,119 +1,510 @@
1
1
  /**
2
- * Factory that wires the uniform get/list/delete/wait/create subcommands for
3
- * any async-task resource. Keeps the per-resource modules focused on their
4
- * unique create-flag shape.
2
+ * Factory that wires the uniform create/get/list/wait/stream/delete
3
+ * subcommands for any registered task resource. Per-resource modules only
4
+ * contribute their unique `create` flag shape and payload validation.
5
+ *
6
+ * Two output schemas share one execution path:
7
+ * legacy — the 0.2.0 payloads and `-o` status report, with two documented
8
+ * fixes: `get` of a non-terminal task exits 0, and async create
9
+ * never polls.
10
+ * v1 — one `meshy.cli/v1` envelope whose result carries the TaskView,
11
+ * the submission record and the download manifest.
12
+ *
13
+ * Money rules enforced here, independent of schema:
14
+ * - every create is exactly one POST, journaled before it is sent;
15
+ * - a lost or malformed response is `submission_unknown` (exit 10), never a
16
+ * retry and never a "please run it again" hint;
17
+ * - local targets that would fail after the POST (an existing --save-json
18
+ * file, an -o path outside the workspace, a missing --project) are checked
19
+ * before it, so a detectable conflict costs zero requests;
20
+ * - once the server has accepted a task, every later failure — saving JSON,
21
+ * polling, downloading, recording, a signal — still reports that task id,
22
+ * the submission record and the `get`/`wait` commands that resume it;
23
+ * - SIGINT stops waiting/streaming (exit 130) and never deletes anything.
5
24
  */
6
25
  import { Command, Option } from "commander";
7
- import { downloadArtifacts } from "./download.js";
8
- import { resolveImageFields, resolveModelFields } from "./file-input.js";
9
- import { pollUntilTerminal } from "./poll.js";
10
- import { emit } from "./output.js";
11
- import { mergePayload, parseJsonFlag } from "./payload.js";
26
+ import { MeshyApiError } from "../client/errors.js";
27
+ import { requireTaskResource } from "../client/resource-registry.js";
28
+ import { TransportError } from "../client/transport.js";
29
+ import { isTerminalStatus, summarizeTask } from "../client/types.js";
30
+ import { emitResult, openCommand, rejectOutputFlagForV1, saveRawJson } from "./command-helpers.js";
31
+ import { abortSignal, wasInterrupted } from "./context.js";
32
+ import { downloadArtifacts, looksLikeFile } from "./download.js";
33
+ import { classifyError, CliError, UsageError } from "./errors.js";
34
+ import { normalizeMediaPayload } from "./file-input.js";
35
+ import { logger } from "./logger.js";
36
+ import { mergeNestedObjects, mergePayload, parseJsonFlag } from "./payload.js";
37
+ import { emitEnvelope, emitStreamEvent, emit } from "./output.js";
38
+ import { parseTimeoutSeconds, pollUntilTerminal } from "./poll.js";
12
39
  import { printReport } from "./report.js";
40
+ import { errorEnvelope, okEnvelope, warning } from "./result.js";
41
+ import { buildRuntime } from "./runtime.js";
13
42
  import { getUpdateNotice } from "./update-notifier.js";
14
- import { buildRuntime, readGlobalFlags } from "./runtime.js";
15
- import { summarizeTask, TERMINAL_STATUSES } from "../client/types.js";
16
- import { UsageError } from "./errors.js";
17
- import { logger } from "./logger.js";
43
+ import { streamTask } from "./stream.js";
44
+ import { toTaskView } from "./task-view.js";
45
+ import { beginOperation, credentialBinding, credentialFingerprint, newOperationId, operationsRoot, payloadFingerprint, updateOperation, } from "./operation-store.js";
46
+ import { originOf } from "./config.js";
47
+ import { resolveWithinRoot, freezeRoot } from "./paths.js";
48
+ import { assertProjectMetadataPresent, indexRootFor, projectRecordCommand, recordTask, saveTaskSnapshot, stageFromTaskType } from "./project-store.js";
49
+ import { existsSync } from "node:fs";
50
+ import { dirname, join, resolve as resolvePath } from "node:path";
51
+ const TASK_JSON_OPTIONS = (cmd) => cmd
52
+ .option("--save-json <file>", "save the full task JSON as received (never overwrites)")
53
+ .option("--include-raw", "v1: include the untouched task response under result.task.raw")
54
+ .option("--project <dir>", "initialised meshy_output project: save task_<id>.json there and record the task in metadata.json")
55
+ .option("--stage <name>", "stage label for the project record (default: derived from the task type, e.g. preview | refine | build)");
56
+ const NOT_REQUESTED = () => ({ state: "not_requested", files: [], metadata_path: null });
57
+ function taskResult(o) {
58
+ return {
59
+ task: o.task ? toTaskView(o.raw ?? o.task, { descriptor: o.descriptor, includeRaw: o.includeRaw }) : null,
60
+ submission: o.submission,
61
+ downloads: o.downloads ?? NOT_REQUESTED(),
62
+ saved_json: o.savedJson ?? null,
63
+ ...(o.extra ?? {}),
64
+ };
65
+ }
66
+ function nextCommands(descriptor, taskId) {
67
+ const base = `meshy ${descriptor.commandPath.join(" ")}`;
68
+ return {
69
+ get: `${base} get ${taskId} --output-schema v1`,
70
+ wait: `${base} wait ${taskId} --output-schema v1`,
71
+ stream: `${base} stream ${taskId} --format ndjson --output-schema v1`,
72
+ };
73
+ }
74
+ /**
75
+ * Re-throw any error as a CliError whose `result` carries `result` — keeping
76
+ * the original classification (code, HTTP status, hint, recovery, exit code).
77
+ * A CliError's own partial result (files written so far, a failed download
78
+ * manifest) is merged on top, so nothing already known is lost.
79
+ */
80
+ export function wrapWithResult(err, result) {
81
+ if (err instanceof CliError) {
82
+ return new CliError({
83
+ code: err.code,
84
+ message: err.message,
85
+ exitCode: err.exitCode,
86
+ httpStatus: err.httpStatus,
87
+ retryable: err.retryable,
88
+ recovery: err.recovery,
89
+ hint: err.hint,
90
+ details: err.details,
91
+ warnings: err.warnings,
92
+ result: { ...result, ...(err.result ?? {}) },
93
+ cause: err,
94
+ });
95
+ }
96
+ const c = classifyError(err);
97
+ return new CliError({
98
+ code: c.code,
99
+ message: c.message,
100
+ exitCode: c.exitCode,
101
+ httpStatus: c.httpStatus,
102
+ retryable: c.retryable,
103
+ recovery: c.recovery,
104
+ hint: c.hint,
105
+ details: c.details,
106
+ warnings: c.warnings,
107
+ result: { ...result, ...(c.result ?? {}) },
108
+ cause: err,
109
+ });
110
+ }
111
+ /** `wrapWithResult` with the task result shape: the id, submission and next commands always survive. */
112
+ function withTaskContext(err, ctx) {
113
+ const next = nextCommands(ctx.descriptor, ctx.taskId);
114
+ const base = taskResult({
115
+ task: ctx.task ?? null,
116
+ raw: ctx.raw ?? null,
117
+ descriptor: ctx.descriptor,
118
+ includeRaw: Boolean(ctx.includeRaw),
119
+ submission: ctx.submission,
120
+ savedJson: ctx.savedJson ?? null,
121
+ extra: { task_id: ctx.taskId, next, ...(ctx.extra ?? {}) },
122
+ });
123
+ let own = {};
124
+ if (err instanceof CliError && err.result) {
125
+ own = { ...err.result };
126
+ // A bookkeeping error's `task: null` must not erase a task we do know.
127
+ if (own["task"] === null && ctx.task)
128
+ delete own["task"];
129
+ }
130
+ const wrapped = wrapWithResult(err, base);
131
+ return new CliError({
132
+ code: wrapped.code,
133
+ message: wrapped.message,
134
+ exitCode: wrapped.exitCode,
135
+ httpStatus: wrapped.httpStatus,
136
+ retryable: wrapped.retryable,
137
+ recovery: wrapped.recovery,
138
+ hint: wrapped.hint ?? wrapped.recovery?.command ?? next.wait,
139
+ details: wrapped.details,
140
+ warnings: wrapped.warnings,
141
+ result: { ...base, ...own, task_id: ctx.taskId, next },
142
+ cause: err,
143
+ });
144
+ }
145
+ function parentTaskIdFromPayload(payload) {
146
+ if (!payload)
147
+ return null;
148
+ for (const k of ["preview_task_id", "input_task_id", "rig_task_id"]) {
149
+ if (typeof payload[k] === "string" && payload[k])
150
+ return payload[k];
151
+ }
152
+ return null;
153
+ }
154
+ /** Resolve --project: an initialised project directory, inside the workspace when one is set. */
155
+ function resolveProjectDir(projectFlag, workspace, cwd = process.cwd()) {
156
+ const projectDir = resolvePath(cwd, projectFlag);
157
+ if (!existsSync(join(projectDir, "metadata.json"))) {
158
+ throw new CliError({
159
+ code: "local_io",
160
+ message: `--project ${projectFlag} is not an initialised project (no metadata.json); run \`meshy project init\` first`,
161
+ });
162
+ }
163
+ if (workspace)
164
+ resolveWithinRoot(projectDir, workspace, { cwd, label: "--project" });
165
+ return projectDir;
166
+ }
167
+ /**
168
+ * --project: snapshot the task (when a full task is known) and record it.
169
+ * Failures keep the task id in the error result — a bookkeeping problem must
170
+ * never read as "no task was created". The recovery context (task, journal
171
+ * operation, stage, workspace) exists before the project is even looked at, so
172
+ * every failure of this phase — the directory resolving outside the workspace,
173
+ * a metadata.json that vanished or was damaged after the preflight, a lock, a
174
+ * full disk — carries it. Two kinds of failure, two answers: a project that no
175
+ * longer lies inside the write boundary gets no command that would cross it;
176
+ * anything else gets the one `meshy project record …` invocation (with the
177
+ * original `--workspace`) that redoes just the record once the project is
178
+ * restored. Nothing is re-submitted.
179
+ */
180
+ function attachToProject(opts, opened, descriptor, taskId, task, raw, extra, warnings) {
181
+ const projectFlag = opts.project;
182
+ if (!projectFlag)
183
+ return null;
184
+ const projectDir = resolvePath(projectFlag);
185
+ // The boundary was frozen before the first request (the workspace, or the
186
+ // project directory itself when no workspace is given); it is never resolved
187
+ // again from a path that may have been replaced since.
188
+ const root = opened.flags.workspaceRoot ?? opts.__projectRoot ?? freezeRoot(projectDir, { label: "--project" });
189
+ const workspace = opened.flags.workspaceRoot?.given;
190
+ const stage = opts.stage ?? (typeof extra.payload?.["mode"] === "string" ? extra.payload["mode"] : descriptor.creativeLab?.stage ?? stageFromTaskType(task?.type, descriptor.id));
191
+ const input = {
192
+ taskId,
193
+ stage,
194
+ resource: descriptor.id,
195
+ taskType: task?.type ?? null,
196
+ endpoint: descriptor.legacyEndpoint,
197
+ parentTaskId: parentTaskIdFromPayload(extra.payload ?? null),
198
+ status: task?.status ?? null,
199
+ taskJson: null,
200
+ operationId: extra.operationId ?? null,
201
+ files: extra.files ?? [],
202
+ };
203
+ // 1. The location. The frozen boundary must still be the directory it was,
204
+ // and the project must resolve inside it now — a workspace or project
205
+ // replaced by a symlink while the request was in flight, or a project that
206
+ // now resolves elsewhere, is a boundary problem: the task exists and is
207
+ // journaled, but no recovery command may be handed out that writes across
208
+ // that line. What passes is written through its real path.
209
+ let located;
210
+ try {
211
+ located = resolveWithinRoot(projectDir, root, { label: "--project" }).path;
212
+ }
213
+ catch (err) {
214
+ throw projectBoundaryFailure(err, taskId, projectFlag, input);
215
+ }
216
+ // 2. The record itself, including "is this (still) an initialised project".
217
+ try {
218
+ if (opts.__projectInitialised === false && !existsSync(join(located, "metadata.json"))) {
219
+ throw new CliError({ code: "local_io", message: `--project ${projectFlag} is not an initialised project (no metadata.json); run \`meshy project init\` first` });
220
+ }
221
+ assertProjectMetadataPresent(located, projectFlag);
222
+ const snapshot = task && raw ? saveTaskSnapshot(located, taskId, raw) : null;
223
+ input.taskJson = snapshot?.relative ?? null;
224
+ const indexRoot = indexRootFor(located, undefined, opened.flags.workspaceRoot);
225
+ const rec = recordTask(located, input, { root: indexRoot.root, skipIndex: indexRoot.skipIndex });
226
+ if (!rec.index.updated)
227
+ warnings.push(warning("index_dirty", `metadata.json committed but history.json was not updated: ${rec.index.error}; run \`meshy project rebuild-index\``));
228
+ if (rec.migrated_from_legacy)
229
+ warnings.push(warning("metadata_migrated", "legacy metadata.json migrated to schema_version 2 (backup kept beside it)"));
230
+ return { project_dir: projectDir, snapshot: snapshot?.path ?? null, stage, action: rec.action, index: rec.index };
231
+ }
232
+ catch (err) {
233
+ // The task exists and the journal is written; only the project entry is
234
+ // missing. The recovery redoes that one step under the original boundary.
235
+ const command = projectRecordCommand(projectDir, input, { workspace });
236
+ throw new CliError({
237
+ code: err instanceof CliError ? err.code : "local_io",
238
+ message: `task ${taskId} exists${input.operationId ? ` (operation ${input.operationId})` : ""} but recording it in ${projectDir} failed: ${err instanceof Error ? err.message : String(err)}; restore the project, then run: ${command}`,
239
+ httpStatus: err instanceof CliError ? err.httpStatus : null,
240
+ retryable: err instanceof CliError ? err.retryable : false,
241
+ recovery: { action: "record_project", automatic: false, command },
242
+ hint: command,
243
+ details: err instanceof CliError ? err.details : undefined,
244
+ cause: err,
245
+ });
246
+ }
247
+ }
248
+ /**
249
+ * The project directory no longer resolves inside the workspace (or is a
250
+ * symlink). The task is known and journaled; the caller learns that, and that
251
+ * nothing was recorded — but no `meshy project record` command is offered,
252
+ * because the only one that would succeed is one without the boundary.
253
+ */
254
+ function projectBoundaryFailure(err, taskId, projectFlag, input) {
255
+ const reason = err instanceof Error ? err.message : String(err);
256
+ return new CliError({
257
+ code: "local_io",
258
+ message: `task ${taskId} exists${input.operationId ? ` (operation ${input.operationId})` : ""} but --project ${projectFlag} is no longer a target inside the authorised boundary: ${reason}; nothing was recorded (no project lock, snapshot or metadata was written). Restore the project inside the workspace, then record the task with \`meshy project record\` from that workspace`,
259
+ details: { project: projectFlag, task_id: taskId, operation_id: input.operationId ?? null, stage: input.stage, recorded: false },
260
+ cause: err,
261
+ });
262
+ }
263
+ /** --save-json inside the task's context: a full disk or a vanished directory never hides the task id. */
264
+ function saveJsonInContext(opts, opened, raw, ctx) {
265
+ if (!opts.saveJson)
266
+ return null;
267
+ try {
268
+ return saveRawJson(String(opts.saveJson), raw, { workspace: opened.flags.workspaceRoot });
269
+ }
270
+ catch (err) {
271
+ throw withTaskContext(err, ctx);
272
+ }
273
+ }
274
+ function attachInContext(opts, opened, descriptor, taskId, task, raw, extra, warnings, ctx) {
275
+ try {
276
+ return attachToProject(opts, opened, descriptor, taskId, task, raw, extra, warnings);
277
+ }
278
+ catch (err) {
279
+ throw withTaskContext(err, ctx);
280
+ }
281
+ }
282
+ // ---------------------------------------------------------------------------
283
+ // Pre-submission checks: anything local that would fail *after* a billable POST
284
+ // and can be detected now is refused now, with "nothing was submitted".
285
+ // ---------------------------------------------------------------------------
286
+ /** `-o` target: inside the workspace (or its own directory), no symlink leaf, not an existing file. */
287
+ export function preflightOutputPath(output, workspace, cwd = process.cwd()) {
288
+ const abs = resolvePath(cwd, output);
289
+ resolveWithinRoot(abs, workspace ?? dirname(abs), { cwd, label: "--output" });
290
+ if (looksLikeFile(abs) && existsSync(abs)) {
291
+ throw new CliError({
292
+ code: "local_io",
293
+ message: `--output ${output} already exists; choose another path (nothing was submitted)`,
294
+ recovery: { action: "choose_path", automatic: false },
295
+ });
296
+ }
297
+ }
298
+ /** `--save-json` target: inside the workspace (or its own directory), no symlink leaf, not an existing file. */
299
+ export function preflightSaveJsonPath(target, workspace, cwd = process.cwd()) {
300
+ const abs = resolvePath(cwd, target);
301
+ const resolved = resolveWithinRoot(abs, workspace ?? dirname(abs), { cwd, label: "--save-json target" });
302
+ if (existsSync(resolved.path)) {
303
+ throw new CliError({
304
+ code: "local_io",
305
+ message: `--save-json target ${target} already exists; choose another path (nothing was submitted)`,
306
+ recovery: { action: "choose_path", automatic: false },
307
+ });
308
+ }
309
+ }
310
+ /**
311
+ * Freeze the project's write boundary before the first request: with a
312
+ * workspace it is the workspace (already frozen with the flags); without one it
313
+ * is the project directory as it is right now. Whether the project was
314
+ * initialised at this moment is remembered too, so a later "no metadata.json"
315
+ * can say which it was. Nothing is refused here for get/wait/stream — a
316
+ * bookkeeping problem stays a bookkeeping problem after the request (one
317
+ * outcome); create's preflight refuses an uninitialised project before its POST.
318
+ */
319
+ function beginProjectContext(opts, opened) {
320
+ const projectFlag = opts.project;
321
+ if (!projectFlag || opts.__projectRoot)
322
+ return;
323
+ const projectDir = resolvePath(projectFlag);
324
+ opts.__projectInitialised = existsSync(join(projectDir, "metadata.json"));
325
+ opts.__projectRoot = opened.flags.workspaceRoot ?? freezeRoot(projectDir, { label: "--project" });
326
+ }
327
+ function preflightLocalTargets(opts, opened) {
328
+ if (opts.saveJson)
329
+ preflightSaveJsonPath(String(opts.saveJson), opened.flags.workspaceRoot);
330
+ if (opened.flags.output)
331
+ preflightOutputPath(opened.flags.output, opened.flags.workspaceRoot);
332
+ if (opts.project) {
333
+ try {
334
+ resolveProjectDir(String(opts.project), opened.flags.workspaceRoot);
335
+ beginProjectContext(opts, opened);
336
+ }
337
+ catch (err) {
338
+ if (err instanceof CliError) {
339
+ throw new CliError({ code: err.code, message: `${err.message} (nothing was submitted)`, recovery: err.recovery, cause: err });
340
+ }
341
+ throw err;
342
+ }
343
+ }
344
+ }
18
345
  export function buildResourceCommand(spec) {
19
- const cmd = new Command(spec.name).description(spec.description);
346
+ const descriptor = requireTaskResource(spec.name);
347
+ const endpointOf = spec.endpointOf ?? ((client) => client.endpointFor(descriptor));
348
+ const prefix = spec.commandPrefix ?? spec.name;
349
+ const defaultSchema = spec.defaultSchema ?? "legacy";
350
+ const cmd = new Command(spec.commandName ?? spec.name).description(spec.description);
20
351
  // create
21
352
  const createCmd = new Command("create").description(spec.create.description);
22
353
  spec.create.configure(createCmd);
23
- createCmd
24
- .addOption(new Option("--data <json>", "raw JSON payload (or @file.json); merges with flags"))
25
- .addOption(new Option("--async", "return the task_id immediately without polling; query later with `<resource> wait <id>` or `get <id>`").default(false))
354
+ TASK_JSON_OPTIONS(createCmd)
355
+ .addOption(new Option("--data <json>", "raw JSON payload (or @file.json); merges with flags (flags win)"))
356
+ .addOption(new Option("--async", "submit once and return the task id immediately (no polling); query later with `get`, `wait` or `stream`").default(false))
26
357
  .addOption(new Option("--timeout <seconds>", "max seconds to poll in sync mode").default("600"))
358
+ .addOption(new Option("--operation-id <id>", "local journal id for this submission; repeating it returns the recorded outcome instead of submitting again (local record only, not a server idempotency key)"))
27
359
  .action(async (opts, thisCmd) => {
28
- const runtime = await buildRuntime(readGlobalFlags(thisCmd));
29
- const endpoint = spec.endpointOf(runtime.client);
360
+ const opened = openCommand(thisCmd, `${prefix}.create`, defaultSchema);
361
+ const timeoutSeconds = parseTimeoutSeconds(opts.timeout ?? "600");
362
+ const runAsync = Boolean(opts.async);
363
+ const includeRaw = Boolean(opts.includeRaw);
364
+ const runtime = await buildRuntime(opened.flags);
365
+ const endpoint = endpointOf(runtime.client);
366
+ // 1. Build and validate the payload — every local failure happens here, before the journal and the POST.
30
367
  const data = parseJsonFlag(opts.data, "--data");
31
- // Resolve local file paths + preflight URLs for image and 3D-model
32
- // inputs before building the payload — fail fast on missing inputs.
33
- await resolveImageFields(opts);
34
- await resolveModelFields(opts);
35
368
  const flagPayload = spec.create.toPayload(opts);
36
369
  const defaults = spec.create.toDefaults?.(opts) ?? {};
37
- const payload = mergePayload(defaults, data, flagPayload);
38
- logger.debug("create payload", payload);
39
- const taskId = await endpoint.create(payload);
40
- const runAsync = Boolean(opts.async);
41
- const timeoutSeconds = Number(opts.timeout ?? 600);
370
+ const layers = [defaults, data, flagPayload];
371
+ let merged = mergePayload(...layers);
372
+ if (spec.create.nestedObjectKeys && spec.create.nestedObjectKeys.length > 0) {
373
+ merged = mergeNestedObjects(merged, layers, spec.create.nestedObjectKeys);
374
+ }
375
+ spec.create.validatePayload?.(merged, opts);
376
+ const { payload } = await normalizeMediaPayload(merged, descriptor.mediaFields, { signal: abortSignal() });
377
+ logger.debug("create payload", redactForLog(payload));
378
+ // 2. Local targets that would fail after the POST are refused before it.
379
+ preflightLocalTargets(opts, opened);
380
+ // 3. Journal, then exactly one POST.
381
+ const submitted = await submitCreate(runtime, descriptor, endpoint, payload, opts.operationId ?? null);
382
+ const { taskId, raw: createRaw, operationId, warnings } = submitted;
383
+ const submission = { state: "accepted", operation_id: operationId, task_id: taskId, request_id: submitted.requestId };
42
384
  if (runAsync) {
385
+ const ctx = { descriptor, taskId, task: null, raw: null, submission, includeRaw };
386
+ const savedJson = saveJsonInContext(opts, opened, createRaw, ctx);
387
+ const project = attachInContext(opts, opened, descriptor, taskId, null, null, { operationId, payload }, warnings, { ...ctx, savedJson });
388
+ if (opened.schema === "v1") {
389
+ await emitEnvelope(okEnvelope(opened.command, taskResult({
390
+ task: null,
391
+ raw: null,
392
+ descriptor,
393
+ includeRaw: false,
394
+ submission,
395
+ savedJson,
396
+ extra: { task_id: taskId, next: nextCommands(descriptor, taskId), project },
397
+ }), warnings), opened.format);
398
+ return;
399
+ }
43
400
  emit({
44
401
  resource: spec.name,
45
402
  task_id: taskId,
46
403
  status: "PENDING",
47
- hint: `meshy-cli ${spec.name} wait ${taskId}`,
48
- }, { format: runtime.flags.format, file: undefined });
404
+ hint: `meshy-cli ${descriptor.commandPath.join(" ")} wait ${taskId}`,
405
+ operation_id: operationId,
406
+ ...(project ? { project_dir: project.project_dir } : {}),
407
+ }, { format: opened.format, file: undefined });
49
408
  return;
50
409
  }
51
- const started = Date.now();
52
- const { task, timedOut } = await pollUntilTerminal(endpoint, taskId, {
53
- timeoutSeconds,
54
- intervalMs: runtime.config.pollIntervalMs,
55
- });
56
- const elapsed = (Date.now() - started) / 1000;
57
- await emitTerminalOutcome(task, timedOut, elapsed, spec.name, runtime);
410
+ // 4. Sync: poll the id we just recorded.
411
+ await waitAndReport(opened, runtime, descriptor, endpoint, taskId, timeoutSeconds, { ...opts, __payload: payload, __operationId: operationId }, submission, warnings);
58
412
  });
59
413
  cmd.addCommand(createCmd);
60
414
  // get
61
- cmd
62
- .command("get <task-id>")
63
- .description("Retrieve a single task by id")
64
- .action(async (taskId, _opts, thisCmd) => {
65
- const runtime = await buildRuntime(readGlobalFlags(thisCmd));
66
- const task = await spec.endpointOf(runtime.client).retrieve(taskId);
67
- await emitTerminalOutcome(task, false, undefined, spec.name, runtime);
415
+ TASK_JSON_OPTIONS(cmd.command("get <task-id>").description("Retrieve a single task by id (any status is a successful query)")).action(async (taskId, opts, thisCmd) => {
416
+ const opened = openCommand(thisCmd, `${prefix}.get`, defaultSchema);
417
+ beginProjectContext(opts, opened);
418
+ const runtime = await buildRuntime(opened.flags);
419
+ const { task, raw } = await endpointOf(runtime.client).retrieveDetailed(taskId, { signal: abortSignal() });
420
+ const submission = { state: "accepted", operation_id: null };
421
+ const includeRaw = Boolean(opts.includeRaw);
422
+ const warnings = [];
423
+ const ctx = { descriptor, taskId, task, raw, submission, includeRaw };
424
+ const savedJson = saveJsonInContext(opts, opened, raw, ctx);
425
+ const project = attachInContext(opts, opened, descriptor, taskId, task, raw, {}, warnings, { ...ctx, savedJson });
426
+ if (opened.schema === "v1") {
427
+ const downloads = await maybeDownloadV1(opened, descriptor, task, raw, submission, warnings, { savedJson, includeRaw, project });
428
+ await emitEnvelope(okEnvelope(opened.command, taskResult({ task, raw, descriptor, includeRaw, submission, downloads, savedJson, extra: project ? { project } : {} }), warnings), opened.format);
429
+ return;
430
+ }
431
+ // Legacy shape, same rule: a download failure still names the task.
432
+ try {
433
+ await emitLegacyOutcome(task, false, undefined, spec.name, runtime, { query: true });
434
+ }
435
+ catch (err) {
436
+ throw withTaskContext(err, { ...ctx, savedJson, extra: project ? { project } : {} });
437
+ }
68
438
  });
69
439
  // wait
70
- cmd
440
+ TASK_JSON_OPTIONS(cmd
71
441
  .command("wait <task-id>")
72
442
  .description("Poll until the task reaches a terminal status")
73
- .option("--timeout <seconds>", "max seconds to wait", "600")
74
- .action(async (taskId, opts, thisCmd) => {
75
- const runtime = await buildRuntime(readGlobalFlags(thisCmd));
76
- const endpoint = spec.endpointOf(runtime.client);
77
- const timeoutSeconds = Number(opts.timeout ?? 600);
78
- const started = Date.now();
79
- const { task, timedOut } = await pollUntilTerminal(endpoint, taskId, {
80
- timeoutSeconds,
81
- intervalMs: runtime.config.pollIntervalMs,
82
- });
83
- const elapsed = (Date.now() - started) / 1000;
84
- await emitTerminalOutcome(task, timedOut, elapsed, spec.name, runtime);
443
+ .option("--timeout <seconds>", "max seconds to wait (0 = a single query)", "600")).action(async (taskId, opts, thisCmd) => {
444
+ const opened = openCommand(thisCmd, `${prefix}.wait`, defaultSchema);
445
+ beginProjectContext(opts, opened);
446
+ const timeoutSeconds = parseTimeoutSeconds(opts.timeout ?? "600");
447
+ const runtime = await buildRuntime(opened.flags);
448
+ const endpoint = endpointOf(runtime.client);
449
+ await waitAndReport(opened, runtime, descriptor, endpoint, taskId, timeoutSeconds, opts, { state: "accepted", operation_id: null }, []);
450
+ });
451
+ // stream
452
+ TASK_JSON_OPTIONS(cmd
453
+ .command("stream <task-id>")
454
+ .description("Follow the task over Server-Sent Events until it is terminal")
455
+ .option("--timeout <seconds>", "total deadline for the stream", "600")
456
+ .option("--idle-timeout <seconds>", "abort when no bytes arrive for this long (keep-alives count)", "60")).action(async (taskId, opts, thisCmd) => {
457
+ const opened = openCommand(thisCmd, `${prefix}.stream`, defaultSchema);
458
+ beginProjectContext(opts, opened);
459
+ if (!descriptor.supports.stream)
460
+ throw new UsageError(`${spec.name} does not support stream`);
461
+ const timeoutSeconds = parseTimeoutSeconds(opts.timeout ?? "600");
462
+ const idleSeconds = parseTimeoutSeconds(opts.idleTimeout ?? "60", "--idle-timeout");
463
+ if (timeoutSeconds === 0)
464
+ throw new UsageError("--timeout 0 is not meaningful for stream; use `get` for a single query");
465
+ const runtime = await buildRuntime(opened.flags);
466
+ const endpoint = endpointOf(runtime.client);
467
+ await streamAndReport(opened, runtime, descriptor, endpoint, taskId, timeoutSeconds, idleSeconds, opts);
85
468
  });
86
469
  // delete
87
470
  cmd
88
471
  .command("delete <task-id>")
89
472
  .description("Delete a task")
90
473
  .action(async (taskId, _opts, thisCmd) => {
91
- const runtime = await buildRuntime(readGlobalFlags(thisCmd));
92
- await spec.endpointOf(runtime.client).delete(taskId);
93
- emit({ resource: spec.name, task_id: taskId, deleted: true }, {
94
- format: runtime.flags.format,
95
- file: runtime.flags.output,
96
- });
474
+ const opened = openCommand(thisCmd, `${prefix}.delete`, defaultSchema);
475
+ rejectOutputFlagForV1(opened, undefined);
476
+ const runtime = await buildRuntime(opened.flags);
477
+ const raw = await endpointOf(runtime.client).delete(taskId, { signal: abortSignal() });
478
+ await emitResult(opened, { resource: spec.name, task_id: taskId, deleted: true }, { resource: spec.name, endpoint: descriptor.legacyEndpoint, task_id: taskId, deleted: true, response: raw ?? null }, { legacyFile: opened.flags.output });
97
479
  });
98
- // list (optional — some endpoints don't support it)
99
- if (spec.supportsList !== false) {
480
+ // list
481
+ if (spec.supportsList !== false && descriptor.supports.list) {
100
482
  cmd
101
483
  .command("list")
102
484
  .description("List recent tasks")
103
485
  .option("--page <n>", "page number (1-based)", "1")
104
486
  .option("--page-size <n>", "page size", "10")
105
487
  .option("--sort-by <field>", "sort order (e.g. -created_at)", "-created_at")
488
+ .option("--save-json <file>", "save the raw list response (never overwrites)")
489
+ .option("--include-raw", "v1: include each raw task under items[].raw")
106
490
  .action(async (opts, thisCmd) => {
107
- const runtime = await buildRuntime(readGlobalFlags(thisCmd));
108
- const tasks = await spec.endpointOf(runtime.client).list({
109
- page_num: Number(opts.page ?? 1),
110
- page_size: Number(opts.pageSize ?? 10),
111
- sort_by: opts.sortBy ?? "-created_at",
112
- });
113
- emit(tasks.map((t) => summarizeTask(t)), {
114
- format: runtime.flags.format,
115
- file: runtime.flags.output,
116
- });
491
+ const opened = openCommand(thisCmd, `${prefix}.list`, defaultSchema);
492
+ rejectOutputFlagForV1(opened, opts.saveJson);
493
+ const runtime = await buildRuntime(opened.flags);
494
+ const page = { page_num: Number(opts.page ?? 1), page_size: Number(opts.pageSize ?? 10), sort_by: String(opts.sortBy ?? "-created_at") };
495
+ if (!Number.isInteger(page.page_num) || page.page_num < 1)
496
+ throw new UsageError("--page must be a positive integer");
497
+ if (!Number.isInteger(page.page_size) || page.page_size < 1)
498
+ throw new UsageError("--page-size must be a positive integer");
499
+ const { tasks, raw } = await endpointOf(runtime.client).listDetailed(page, { signal: abortSignal() });
500
+ const savedJson = opts.saveJson ? saveRawJson(opts.saveJson, raw, { workspace: opened.flags.workspaceRoot }) : null;
501
+ const rawItems = Array.isArray(raw) ? raw : [];
502
+ await emitResult(opened, tasks.map((t) => summarizeTask(t)), {
503
+ items: tasks.map((t, i) => toTaskView(rawItems[i] ?? t, { descriptor, includeRaw: Boolean(opts.includeRaw) })),
504
+ count: tasks.length,
505
+ page,
506
+ saved_json: savedJson,
507
+ }, { legacyFile: opened.flags.output });
117
508
  });
118
509
  }
119
510
  else {
@@ -126,18 +517,500 @@ export function buildResourceCommand(spec) {
126
517
  }
127
518
  return cmd;
128
519
  }
520
+ /** Strip media payloads before logging. */
521
+ function redactForLog(payload) {
522
+ const out = {};
523
+ for (const [k, v] of Object.entries(payload)) {
524
+ if (typeof v === "string" && v.startsWith("data:"))
525
+ out[k] = `data:<${v.length} chars>`;
526
+ else if (Array.isArray(v))
527
+ out[k] = v.map((x) => (typeof x === "string" && x.startsWith("data:") ? `data:<${x.length} chars>` : x));
528
+ else
529
+ out[k] = v;
530
+ }
531
+ return out;
532
+ }
129
533
  /**
130
- * Decide how to report a task whose status is (expected to be) terminal:
131
- * - with `-o`: download artifacts to the filesystem and print the status
132
- * report. Failed/timed-out tasks print the FAIL form without downloads.
133
- * - without `-o`: emit the familiar JSON/pretty summary to stdout.
534
+ * The credential identity a submission is journaled under: bound to the key
535
+ * digest, the OAuth subject or the OAuth login id. `verified` is false when
536
+ * none of those exists (a pre-login-id OAuth profile): such a credential may
537
+ * start operations but is refused a replay of an existing record.
134
538
  */
135
- export async function emitTerminalOutcome(task, timedOut, elapsedSeconds, resourceName, runtime) {
539
+ export function credentialIdentityFor(runtime, apiOrigin) {
540
+ const parts = {
541
+ source: runtime.config.credentialSource,
542
+ profile: runtime.config.credentialProfile ?? null,
543
+ origin: apiOrigin,
544
+ kind: runtime.config.credentialKind,
545
+ secret: runtime.config.credentialKind === "api_key" ? runtime.config.apiKey : null,
546
+ subject: runtime.config.credentialSubject ?? null,
547
+ loginId: runtime.config.credentialLoginId ?? null,
548
+ };
549
+ return { fingerprint: credentialFingerprint(parts), verified: credentialBinding(parts).verified };
550
+ }
551
+ /**
552
+ * Journal → single POST → journal update. Every failure path leaves a record
553
+ * that says what is known; the unknown state is reported as exit 10 with the
554
+ * operation id and never as a suggestion to submit again. Shared by the
555
+ * resource commands and `make`, so there is exactly one submission state
556
+ * machine.
557
+ */
558
+ export async function submitCreate(runtime, descriptor, endpoint, payload, requestedOperationId, ctx = {}) {
559
+ const root = operationsRoot();
560
+ const operationId = requestedOperationId ?? newOperationId();
561
+ const apiOrigin = originOf(endpoint.transportBaseUrl) ?? endpoint.transportBaseUrl;
562
+ const credential = credentialIdentityFor(runtime, apiOrigin);
563
+ const identity = {
564
+ resource: descriptor.id,
565
+ endpoint: descriptor.legacyEndpoint,
566
+ apiOrigin,
567
+ credentialFingerprint: credential.fingerprint,
568
+ credentialVerified: credential.verified,
569
+ payloadFingerprint: payloadFingerprint(payload),
570
+ };
571
+ const label = ctx.label ?? "the create request";
572
+ const extra = ctx.extraResult ?? {};
573
+ const warnings = [];
574
+ let begin;
575
+ try {
576
+ begin = beginOperation(root, operationId, identity);
577
+ }
578
+ catch (err) {
579
+ if (err instanceof CliError)
580
+ throw err;
581
+ throw new CliError({ code: "local_io", message: `cannot write the operation journal under ${root}: ${err instanceof Error ? err.message : String(err)}; nothing was submitted`, cause: err });
582
+ }
583
+ if (begin.outcome === "existing") {
584
+ return replayExisting(begin.record, descriptor, operationId, extra);
585
+ }
586
+ if (abortSignal().aborted) {
587
+ updateOperation(root, operationId, { state: "not_submitted", error: "interrupted before the request was sent" });
588
+ throw new CliError({ code: "interrupted", message: "interrupted before the request was sent; nothing was submitted", result: { submission: { state: "not_submitted", operation_id: operationId }, task: null, ...extra } });
589
+ }
590
+ let created;
591
+ try {
592
+ created = await endpoint.createDetailed(payload, { signal: abortSignal() });
593
+ }
594
+ catch (err) {
595
+ const submission = classifySubmissionFailure(err);
596
+ try {
597
+ updateOperation(root, operationId, {
598
+ state: submission.state,
599
+ http_status: err instanceof MeshyApiError && err.status ? err.status : null,
600
+ error: err instanceof Error ? err.message : String(err),
601
+ });
602
+ }
603
+ catch (journalErr) {
604
+ warnings.push(warning("journal_write_failed", `operation journal update failed: ${journalErr instanceof Error ? journalErr.message : String(journalErr)}`));
605
+ }
606
+ if (submission.state === "rejected" || submission.state === "not_submitted") {
607
+ // Definite outcomes keep their own classification (validation/auth/network …).
608
+ if (err instanceof CliError)
609
+ throw err;
610
+ const wrapped = err instanceof MeshyApiError ? err : err instanceof Error ? err : new Error(String(err));
611
+ throw attachSubmission(wrapped, { state: submission.state, operation_id: operationId }, warnings);
612
+ }
613
+ const interrupted = wasInterrupted();
614
+ throw new CliError({
615
+ code: interrupted ? "interrupted" : "submission_unknown",
616
+ message: interrupted
617
+ ? `interrupted while ${label} was in flight; the server may or may not have created a task (operation ${operationId})`
618
+ : `${label} was sent but its outcome is unknown (${err instanceof Error ? err.message : String(err)}); the server may or may not have created a task`,
619
+ httpStatus: err instanceof MeshyApiError && err.status ? err.status : null,
620
+ recovery: {
621
+ action: "reconcile",
622
+ automatic: false,
623
+ command: `meshy ${descriptor.commandPath.join(" ")} list --output-schema v1 # then match operation ${operationId} by time/prompt before creating again`,
624
+ },
625
+ result: { submission: { state: "unknown", operation_id: operationId, task_id: null }, task: null, downloads: NOT_REQUESTED(), ...extra },
626
+ warnings,
627
+ cause: err,
628
+ });
629
+ }
630
+ try {
631
+ updateOperation(root, operationId, { state: "accepted", task_id: created.taskId, request_id: created.requestId, http_status: 200 });
632
+ }
633
+ catch (journalErr) {
634
+ // The server has the task; the id must survive this failure — a local write
635
+ // problem is local_io, never an unknown submission.
636
+ throw new CliError({
637
+ code: "local_io",
638
+ message: `task ${created.taskId} was created but the operation journal could not be updated: ${journalErr instanceof Error ? journalErr.message : String(journalErr)}`,
639
+ result: {
640
+ submission: { state: "accepted", operation_id: operationId, task_id: created.taskId, request_id: created.requestId },
641
+ task: null,
642
+ task_id: created.taskId,
643
+ next: nextCommands(descriptor, created.taskId),
644
+ ...extra,
645
+ },
646
+ cause: journalErr,
647
+ });
648
+ }
649
+ return { taskId: created.taskId, raw: created.raw, requestId: created.requestId, operationId, warnings };
650
+ }
651
+ function replayExisting(record, descriptor, operationId, extra) {
652
+ if (record.state === "accepted" && record.task_id) {
653
+ return {
654
+ taskId: record.task_id,
655
+ raw: { result: record.task_id, replayed_from_journal: true },
656
+ requestId: record.request_id,
657
+ operationId,
658
+ warnings: [warning("operation_replayed", `operation ${operationId} was already accepted as task ${record.task_id} on ${record.updated_at}; no new request was sent`)],
659
+ };
660
+ }
661
+ if (record.state === "unknown" || record.state === "started") {
662
+ throw new CliError({
663
+ code: "submission_unknown",
664
+ message: `operation ${operationId} is recorded as '${record.state}' since ${record.updated_at}; reconcile it before submitting again (nothing was sent now)`,
665
+ recovery: { action: "reconcile", automatic: false, command: `meshy ${descriptor.commandPath.join(" ")} list --output-schema v1` },
666
+ result: { submission: { state: "unknown", operation_id: operationId, task_id: record.task_id }, task: null, ...extra },
667
+ });
668
+ }
669
+ throw new CliError({
670
+ code: "operation_conflict",
671
+ message: `operation ${operationId} was already ${record.state} on ${record.updated_at} (${record.error ?? "no detail"}); use a new --operation-id to submit again`,
672
+ result: { submission: { state: record.state, operation_id: operationId, task_id: record.task_id }, task: null, ...extra },
673
+ });
674
+ }
675
+ function classifySubmissionFailure(err) {
676
+ if (err instanceof TransportError) {
677
+ return { state: err.neverSent ? "not_submitted" : "unknown" };
678
+ }
679
+ if (err instanceof MeshyApiError) {
680
+ if (err.status >= 400 && err.status < 500)
681
+ return { state: "rejected" };
682
+ return { state: "unknown" }; // 5xx, malformed 2xx, unknown status
683
+ }
684
+ return { state: "unknown" };
685
+ }
686
+ function attachSubmission(err, submission, warnings) {
687
+ err.submission = submission;
688
+ err.warnings = warnings;
689
+ return err;
690
+ }
691
+ /**
692
+ * v1 `-o`: download a SUCCEEDED task's assets through the legacy layout,
693
+ * confined to the workspace when one is set. A failure keeps the task in the
694
+ * result — the assets are still on the server, the task still exists.
695
+ */
696
+ async function maybeDownloadV1(opened, descriptor, task, raw, submission, warnings, ctx) {
697
+ const output = opened.flags.output;
698
+ if (!output)
699
+ return NOT_REQUESTED();
700
+ if (task.status !== "SUCCEEDED")
701
+ return { state: "not_ready", files: [], metadata_path: null };
702
+ try {
703
+ const { files, metadataPath, materialLinks } = await downloadArtifacts(task, output, descriptor.id, { root: opened.flags.workspaceRoot, signal: abortSignal() });
704
+ if (materialLinks)
705
+ warnings.push(...materialLinks.warnings);
706
+ return {
707
+ state: "completed",
708
+ files: files.map((f) => ({ key: f.key, path: f.path, status: f.status, bytes: f.bytes, sha256: f.sha256, error: f.error })),
709
+ metadata_path: metadataPath,
710
+ material_links: materialLinks,
711
+ };
712
+ }
713
+ catch (err) {
714
+ // Whatever the downloader already committed stays in the manifest, and the
715
+ // failure keeps its own class: an HTTP 503 on the second asset is a network
716
+ // failure with its status, a Ctrl-C is `interrupted` (130) — never a bare local_io.
717
+ const partial = err instanceof CliError && err.result && err.result["downloads"] && typeof err.result["downloads"] === "object"
718
+ ? err.result["downloads"]
719
+ : { state: "failed", files: [], metadata_path: null };
720
+ const interrupted = (err instanceof CliError && err.code === "interrupted") || wasInterrupted();
721
+ const failure = new CliError({
722
+ code: interrupted ? "interrupted" : err instanceof CliError ? err.code : "local_io",
723
+ message: `task ${task.id} is SUCCEEDED but downloading its assets ${interrupted ? "was interrupted" : "failed"}: ${err instanceof Error ? err.message : String(err)}`,
724
+ httpStatus: err instanceof CliError ? err.httpStatus : null,
725
+ retryable: err instanceof CliError ? err.retryable : false,
726
+ recovery: err instanceof CliError && err.recovery ? err.recovery : { action: "download", automatic: false, command: `meshy download --resource ${descriptor.id} --task-id ${task.id} --all --output-dir <dir>` },
727
+ hint: err instanceof CliError ? err.hint : undefined,
728
+ details: err instanceof CliError ? err.details : undefined,
729
+ result: { downloads: partial },
730
+ warnings: [...warnings, ...(err instanceof CliError ? err.warnings : [])],
731
+ cause: err,
732
+ });
733
+ throw withTaskContext(failure, {
734
+ descriptor,
735
+ taskId: task.id,
736
+ task,
737
+ raw,
738
+ submission,
739
+ includeRaw: ctx.includeRaw,
740
+ savedJson: ctx.savedJson,
741
+ extra: ctx.project ? { project: ctx.project } : {},
742
+ });
743
+ }
744
+ }
745
+ async function waitAndReport(opened, runtime, descriptor, endpoint, taskId, timeoutSeconds, opts, submission, warnings) {
746
+ const started = performance.now();
747
+ const includeRaw = Boolean(opts.includeRaw);
748
+ // A holder (not a bare `let`) so the callback's assignment is visible to the
749
+ // catch block without TypeScript narrowing it away.
750
+ const seen = { last: null, polls: 0 };
751
+ let poll;
752
+ try {
753
+ poll = await pollUntilTerminal(endpoint, taskId, {
754
+ timeoutSeconds,
755
+ intervalMs: runtime.config.pollIntervalMs,
756
+ requestTimeoutMs: runtime.config.readTimeoutMs,
757
+ signal: abortSignal(),
758
+ onTick: (task, raw) => {
759
+ seen.last = { task, raw };
760
+ seen.polls += 1;
761
+ if (opened.schema === "v1" && opened.format !== "ndjson") {
762
+ process.stderr.write(`[${descriptor.id}] ${task.status}${typeof task.progress === "number" ? ` ${task.progress}%` : ""}\n`);
763
+ }
764
+ },
765
+ });
766
+ }
767
+ catch (err) {
768
+ if (wasInterrupted() || abortSignal().aborted) {
769
+ throw interruptedError(descriptor, taskId, seen.last, submission, opts, opened);
770
+ }
771
+ // A polling failure (5xx, network, malformed task) is not "no task": the
772
+ // id, the submission and the last status seen travel with the error.
773
+ const elapsed = (performance.now() - started) / 1000;
774
+ throw withTaskContext(err, {
775
+ descriptor,
776
+ taskId,
777
+ task: seen.last?.task ?? null,
778
+ raw: seen.last?.raw ?? null,
779
+ submission,
780
+ includeRaw,
781
+ extra: { wait: { timed_out: false, elapsed_seconds: Number(elapsed.toFixed(2)), polls: seen.polls } },
782
+ });
783
+ }
784
+ const elapsed = (performance.now() - started) / 1000;
785
+ const { task, raw, timedOut, aborted } = poll;
786
+ const waitInfo = { timed_out: timedOut, elapsed_seconds: Number(elapsed.toFixed(2)), polls: poll.polls };
787
+ if (aborted)
788
+ throw interruptedError(descriptor, taskId, task ? { task, raw } : seen.last, submission, opts, opened);
789
+ if (task === null) {
790
+ // The deadline passed before the first response arrived; the task id is all we know — and it is enough.
791
+ if (opened.schema !== "v1") {
792
+ emitLegacyTimeoutWithoutTask(taskId, descriptor.id, runtime);
793
+ return;
794
+ }
795
+ throw new CliError({
796
+ code: "timed_out",
797
+ message: `task ${taskId} did not answer within ${timeoutSeconds}s (no status was received in time); the server keeps running it`,
798
+ recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait },
799
+ result: taskResult({ task: null, raw: null, descriptor, includeRaw, submission, extra: { task_id: taskId, wait: waitInfo, next: nextCommands(descriptor, taskId) } }),
800
+ warnings,
801
+ });
802
+ }
803
+ const ctx = { descriptor, taskId, task, raw, submission, includeRaw, extra: { wait: waitInfo } };
804
+ const savedJson = saveJsonInContext(opts, opened, raw, ctx);
805
+ const project = attachInContext(opts, opened, descriptor, taskId, task, raw, { operationId: opts.__operationId ?? submission.operation_id ?? null, payload: opts.__payload ?? null }, warnings, { ...ctx, savedJson });
806
+ const projectExtra = project ? { project } : {};
807
+ if (opened.schema !== "v1") {
808
+ // The legacy reporter downloads too; whatever fails there — an asset host
809
+ // 503, a sidecar that cannot be published, Ctrl-C — the error still
810
+ // carries the accepted task id, the real submission and the resume command.
811
+ try {
812
+ await emitLegacyOutcome(task, timedOut, elapsed, descriptor.id, runtime, { query: false });
813
+ }
814
+ catch (err) {
815
+ throw withTaskContext(err, { ...ctx, savedJson, extra: { wait: waitInfo, ...projectExtra } });
816
+ }
817
+ return;
818
+ }
819
+ if (timedOut) {
820
+ throw new CliError({
821
+ code: "timed_out",
822
+ message: `task ${taskId} did not reach a terminal status within ${timeoutSeconds}s (last status: ${task.status}); the server keeps running it`,
823
+ recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait },
824
+ result: taskResult({ task, raw, descriptor, includeRaw, submission, savedJson, extra: { task_id: taskId, wait: waitInfo, next: nextCommands(descriptor, taskId), ...projectExtra } }),
825
+ warnings,
826
+ });
827
+ }
828
+ if (task.status !== "SUCCEEDED") {
829
+ throw new CliError({
830
+ code: "task_failed",
831
+ message: task.task_error?.message ? `task ${taskId} ${task.status}: ${task.task_error.message}` : `task ${taskId} ended as ${task.status}`,
832
+ result: taskResult({ task, raw, descriptor, includeRaw, submission, savedJson, extra: { task_id: taskId, wait: waitInfo, ...projectExtra } }),
833
+ warnings,
834
+ });
835
+ }
836
+ const downloads = await maybeDownloadV1(opened, descriptor, task, raw, submission, warnings, { savedJson, includeRaw, project });
837
+ await emitEnvelope(okEnvelope(opened.command, taskResult({ task, raw, descriptor, includeRaw, submission, downloads, savedJson, extra: { wait: waitInfo, ...projectExtra } }), warnings), opened.format);
838
+ }
839
+ /** Legacy shape for a wait that timed out before any status arrived: still the task id, still exit 8. */
840
+ function emitLegacyTimeoutWithoutTask(taskId, resourceName, runtime) {
841
+ if (runtime.flags.output) {
842
+ const report = { status: "FAIL", taskId, type: resourceName, timedOut: true };
843
+ const notice = getUpdateNotice();
844
+ if (notice)
845
+ report._notice = notice;
846
+ printReport(report);
847
+ }
848
+ else {
849
+ emit({ resource: resourceName, id: taskId, status: null, timed_out: true }, { format: runtime.flags.format });
850
+ }
851
+ process.exitCode = 8;
852
+ }
853
+ function interruptedError(descriptor, taskId, last, submission, opts, opened, savedJson = null) {
854
+ let saved = savedJson;
855
+ if (!saved && opts.saveJson && last) {
856
+ try {
857
+ saved = saveRawJson(opts.saveJson, last.raw, { workspace: opened.flags.workspaceRoot });
858
+ }
859
+ catch {
860
+ saved = null;
861
+ }
862
+ }
863
+ return new CliError({
864
+ code: "interrupted",
865
+ message: `interrupted while waiting for task ${taskId}; the server keeps running it — resume with \`${nextCommands(descriptor, taskId).wait}\``,
866
+ recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait },
867
+ result: taskResult({ task: last?.task ?? null, raw: last?.raw ?? null, descriptor, includeRaw: Boolean(opts.includeRaw), submission, savedJson: saved, extra: { task_id: taskId, next: nextCommands(descriptor, taskId) } }),
868
+ });
869
+ }
870
+ async function streamAndReport(opened, runtime, descriptor, endpoint, taskId, timeoutSeconds, idleSeconds, opts) {
871
+ const includeRaw = Boolean(opts.includeRaw);
872
+ const ndjson = opened.format === "ndjson";
873
+ let sequence = 0;
874
+ const submission = { state: "accepted", operation_id: null };
875
+ const warnings = [];
876
+ const outcome = await streamTask(endpoint, taskId, {
877
+ timeoutMs: timeoutSeconds * 1000,
878
+ idleTimeoutMs: idleSeconds * 1000,
879
+ signal: abortSignal(),
880
+ onUnknownEvent: (ev) => {
881
+ warnings.push(warning("unknown_sse_event", `ignored SSE event '${ev.event}'`));
882
+ },
883
+ onTask: async (task, raw) => {
884
+ if (opened.schema === "v1" && ndjson) {
885
+ sequence += 1;
886
+ const event = {
887
+ ...okEnvelope(opened.command, taskResult({ task, raw, descriptor, includeRaw, submission })),
888
+ event: "task",
889
+ sequence,
890
+ };
891
+ await emitStreamEvent(event);
892
+ }
893
+ else if (opened.schema === "v1") {
894
+ process.stderr.write(`[${descriptor.id}] ${task.status}${typeof task.progress === "number" ? ` ${task.progress}%` : ""}\n`);
895
+ }
896
+ },
897
+ });
898
+ const streamInfo = { events: outcome.events, ended: outcome.reason, elapsed_seconds: Number((outcome.elapsedMs / 1000).toFixed(2)) };
899
+ const ctx = { descriptor, taskId, task: outcome.task, raw: outcome.raw, submission, includeRaw, extra: { stream: streamInfo, task_id: taskId } };
900
+ // Once the stream has started, every later step — saving JSON, recording the
901
+ // project, downloading — is part of the same terminal outcome: one `outcome`
902
+ // event (ndjson) or one envelope (json/pretty), never a bare error after it.
903
+ let savedJson = null;
904
+ let project = null;
905
+ let bookkeepingError = null;
906
+ try {
907
+ savedJson = opts.saveJson && outcome.raw ? saveJsonInContext(opts, opened, outcome.raw, ctx) : null;
908
+ project = outcome.task ? attachInContext(opts, opened, descriptor, taskId, outcome.task, outcome.raw, {}, warnings, { ...ctx, savedJson }) : null;
909
+ }
910
+ catch (err) {
911
+ bookkeepingError = err instanceof CliError ? err : withTaskContext(err, ctx);
912
+ }
913
+ const projectExtra = project ? { project } : {};
914
+ let finalError = null;
915
+ const resultWith = (downloads) => taskResult({ task: outcome.task, raw: outcome.raw, descriptor, includeRaw, submission, downloads, savedJson, extra: { stream: streamInfo, task_id: taskId, next: nextCommands(descriptor, taskId), ...projectExtra } });
916
+ let result = resultWith();
917
+ switch (outcome.reason) {
918
+ case "terminal":
919
+ if (outcome.task && outcome.task.status !== "SUCCEEDED") {
920
+ finalError = new CliError({
921
+ code: "task_failed",
922
+ message: outcome.task.task_error?.message ? `task ${taskId} ${outcome.task.status}: ${outcome.task.task_error.message}` : `task ${taskId} ended as ${outcome.task.status}`,
923
+ result,
924
+ warnings,
925
+ });
926
+ }
927
+ break;
928
+ case "timeout":
929
+ finalError = new CliError({ code: "timed_out", message: `stream deadline of ${timeoutSeconds}s reached (last status: ${outcome.task?.status ?? "none"}); the server keeps running the task`, recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait }, result, warnings });
930
+ break;
931
+ case "idle_timeout":
932
+ finalError = new CliError({ code: "network", message: `no bytes received for ${idleSeconds}s on the stream (last status: ${outcome.task?.status ?? "none"})`, recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait }, result, warnings });
933
+ break;
934
+ case "disconnected":
935
+ finalError = new CliError({ code: "network", message: outcome.error?.message ?? "stream disconnected before a terminal status", recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait }, result, warnings });
936
+ break;
937
+ case "interrupted":
938
+ finalError = new CliError({ code: "interrupted", message: `interrupted while streaming task ${taskId}; the server keeps running it`, recovery: { action: "wait", automatic: false, command: nextCommands(descriptor, taskId).wait }, result, warnings });
939
+ break;
940
+ case "error":
941
+ case "protocol":
942
+ finalError = wrapStreamError(outcome.error, result, warnings);
943
+ break;
944
+ }
945
+ if (bookkeepingError) {
946
+ if (finalError) {
947
+ // The stream's own failure is the outcome; the bookkeeping failure rides along as a warning.
948
+ finalError.warnings.push(warning("bookkeeping_failed", bookkeepingError.message));
949
+ }
950
+ else {
951
+ finalError = bookkeepingError;
952
+ }
953
+ }
954
+ if (opened.schema !== "v1") {
955
+ // Legacy has no stream shape to preserve: reuse the terminal summary path (which honours -o itself).
956
+ if (finalError)
957
+ throw finalError;
958
+ if (outcome.task) {
959
+ try {
960
+ await emitLegacyOutcome(outcome.task, false, outcome.elapsedMs / 1000, descriptor.id, runtime, { query: false });
961
+ }
962
+ catch (err) {
963
+ throw withTaskContext(err, { ...ctx, savedJson, extra: { stream: streamInfo, ...projectExtra } });
964
+ }
965
+ }
966
+ return;
967
+ }
968
+ // The terminal outcome includes the requested download whatever the output
969
+ // format: `-o` means "put the assets on disk", not "only when printing JSON".
970
+ if (!finalError && outcome.task && outcome.task.status === "SUCCEEDED") {
971
+ try {
972
+ const downloads = await maybeDownloadV1(opened, descriptor, outcome.task, outcome.raw, submission, warnings, { savedJson, includeRaw, project });
973
+ result = resultWith(downloads);
974
+ }
975
+ catch (err) {
976
+ finalError = err instanceof CliError ? err : withTaskContext(err, ctx);
977
+ }
978
+ }
979
+ if (ndjson) {
980
+ sequence += 1;
981
+ const body = finalError ? errorEnvelope(opened.command, finalError).envelope : okEnvelope(opened.command, result, warnings);
982
+ const event = { ...body, event: "outcome", sequence };
983
+ await emitStreamEvent(event);
984
+ // The outcome line already carries the error; exit with its code without a second envelope.
985
+ if (finalError)
986
+ process.exitCode = finalError.exitCode;
987
+ return;
988
+ }
989
+ if (finalError)
990
+ throw finalError;
991
+ await emitEnvelope(okEnvelope(opened.command, result, warnings), opened.format);
992
+ }
993
+ function wrapStreamError(err, result, warnings) {
994
+ if (err instanceof CliError) {
995
+ return new CliError({ code: err.code, message: err.message, httpStatus: err.httpStatus, recovery: err.recovery, result, warnings: [...err.warnings, ...warnings], cause: err });
996
+ }
997
+ if (err instanceof MeshyApiError) {
998
+ const code = err.code === "server" ? "server" : err.code;
999
+ return new CliError({ code, message: err.message, httpStatus: err.status || null, result, warnings, cause: err });
1000
+ }
1001
+ return new CliError({ code: "protocol", message: "stream ended abnormally", result, warnings });
1002
+ }
1003
+ /**
1004
+ * Legacy reporter. `query: true` is `get`: a valid task is a successful query
1005
+ * whatever its status (0.2.0 exited 1 for PENDING/IN_PROGRESS — fixed); a
1006
+ * terminal FAILED/CANCELED still exits 1 for compatibility.
1007
+ */
1008
+ async function emitLegacyOutcome(task, timedOut, elapsedSeconds, resourceName, runtime, mode) {
136
1009
  const output = runtime.flags.output;
137
1010
  const succeeded = !timedOut && task.status === "SUCCEEDED";
138
1011
  if (output) {
139
1012
  if (succeeded) {
140
- const { savedFiles, metadataPath } = await downloadArtifacts(task, output, resourceName);
1013
+ const { savedFiles, metadataPath } = await downloadArtifacts(task, output, resourceName, { root: runtime.flags.workspaceRoot, signal: abortSignal() });
141
1014
  const successReport = {
142
1015
  status: "SUCCESS",
143
1016
  taskId: task.id,
@@ -149,41 +1022,48 @@ export async function emitTerminalOutcome(task, timedOut, elapsedSeconds, resour
149
1022
  if (successNotice)
150
1023
  successReport._notice = successNotice;
151
1024
  printReport(successReport);
152
- }
153
- else {
154
- const report = {
155
- status: "FAIL",
156
- taskId: task.id,
157
- type: task.type || resourceName,
158
- };
159
- if (timedOut)
160
- report.timedOut = true;
161
- else if (task.task_error?.message)
162
- report.error = task.task_error.message;
163
- else if (task.status)
164
- report.error = `task status: ${task.status}`;
165
- const failNotice = getUpdateNotice();
166
- if (failNotice)
167
- report._notice = failNotice;
168
- printReport(report);
169
- process.exitCode = timedOut ? 8 : 1;
170
- }
171
- if (succeeded)
172
1025
  return;
173
- }
174
- else {
175
- // No -o: keep the JSON/pretty summary for machine consumption.
176
- emit({
177
- resource: resourceName,
178
- ...summarizeTask(task, elapsedSeconds),
179
- ...(timedOut ? { timed_out: true } : {}),
180
- }, { format: runtime.flags.format });
181
- }
182
- if (!output) {
1026
+ }
1027
+ const report = {
1028
+ status: "FAIL",
1029
+ taskId: task.id,
1030
+ type: task.type || resourceName,
1031
+ };
1032
+ if (timedOut)
1033
+ report.timedOut = true;
1034
+ else if (task.task_error?.message)
1035
+ report.error = task.task_error.message;
1036
+ else if (task.status)
1037
+ report.error = `task status: ${task.status}`;
1038
+ const failNotice = getUpdateNotice();
1039
+ if (failNotice)
1040
+ report._notice = failNotice;
1041
+ printReport(report);
183
1042
  if (timedOut)
184
1043
  process.exitCode = 8;
185
- else if (!TERMINAL_STATUSES.has(task.status) || task.status !== "SUCCEEDED")
1044
+ else if (mode.query && !isTerminalStatus(task.status))
1045
+ process.exitCode = 0;
1046
+ else
186
1047
  process.exitCode = 1;
1048
+ return;
187
1049
  }
1050
+ emit({
1051
+ resource: resourceName,
1052
+ ...summarizeTask(task, elapsedSeconds),
1053
+ ...(timedOut ? { timed_out: true } : {}),
1054
+ }, { format: runtime.flags.format });
1055
+ if (timedOut)
1056
+ process.exitCode = 8;
1057
+ else if (task.status === "SUCCEEDED")
1058
+ process.exitCode = 0;
1059
+ else if (!isTerminalStatus(task.status))
1060
+ process.exitCode = mode.query ? 0 : 1;
1061
+ else
1062
+ process.exitCode = 1;
1063
+ }
1064
+ /** Kept for make.ts (legacy path). */
1065
+ export async function emitTerminalOutcome(task, timedOut, elapsedSeconds, resourceName, runtime) {
1066
+ await emitLegacyOutcome(task, timedOut, elapsedSeconds, resourceName, runtime, { query: false });
188
1067
  }
1068
+ export { taskResult as buildTaskResult, nextCommands as taskNextCommands };
189
1069
  //# sourceMappingURL=task-command.js.map