@12ui/design 0.2.26 → 0.2.28

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 (361) hide show
  1. package/README.md +72 -87
  2. package/SKILL.md +5 -5
  3. package/dist/branch-client.d.ts +7 -0
  4. package/dist/branch-client.d.ts.map +1 -1
  5. package/dist/branch-client.js +1 -0
  6. package/dist/branch-client.js.map +1 -1
  7. package/dist/branch-completion.d.ts +8 -0
  8. package/dist/branch-completion.d.ts.map +1 -1
  9. package/dist/branch-completion.js +84 -16
  10. package/dist/branch-completion.js.map +1 -1
  11. package/dist/branch-execution.d.ts +8 -0
  12. package/dist/branch-execution.d.ts.map +1 -1
  13. package/dist/branch-execution.js +62 -11
  14. package/dist/branch-execution.js.map +1 -1
  15. package/dist/branch-identity.d.ts +10 -4
  16. package/dist/branch-identity.d.ts.map +1 -1
  17. package/dist/branch-identity.js +9 -6
  18. package/dist/branch-identity.js.map +1 -1
  19. package/dist/branch-page-conversion.d.ts +14 -8
  20. package/dist/branch-page-conversion.d.ts.map +1 -1
  21. package/dist/branch-page-conversion.js +32 -15
  22. package/dist/branch-page-conversion.js.map +1 -1
  23. package/dist/branch-page-packaging.d.ts +31 -0
  24. package/dist/branch-page-packaging.d.ts.map +1 -0
  25. package/dist/branch-page-packaging.js +86 -0
  26. package/dist/branch-page-packaging.js.map +1 -0
  27. package/dist/branch-plan.d.ts +44 -0
  28. package/dist/branch-plan.d.ts.map +1 -1
  29. package/dist/branch-plan.js +89 -18
  30. package/dist/branch-plan.js.map +1 -1
  31. package/dist/branch-progress.d.ts +22 -0
  32. package/dist/branch-progress.d.ts.map +1 -1
  33. package/dist/branch-progress.js +46 -8
  34. package/dist/branch-progress.js.map +1 -1
  35. package/dist/branch-run-record.d.ts +36 -1
  36. package/dist/branch-run-record.d.ts.map +1 -1
  37. package/dist/branch-run-record.js.map +1 -1
  38. package/dist/branch-status.d.ts +2 -0
  39. package/dist/branch-status.d.ts.map +1 -1
  40. package/dist/branch-status.js +60 -15
  41. package/dist/branch-status.js.map +1 -1
  42. package/dist/branch-winner.d.ts +13 -15
  43. package/dist/branch-winner.d.ts.map +1 -1
  44. package/dist/branch-winner.js +25 -77
  45. package/dist/branch-winner.js.map +1 -1
  46. package/dist/cli-auth-command.d.ts.map +1 -1
  47. package/dist/cli-auth-command.js +5 -0
  48. package/dist/cli-auth-command.js.map +1 -1
  49. package/dist/cli-auth-onboarding.d.ts +12 -0
  50. package/dist/cli-auth-onboarding.d.ts.map +1 -0
  51. package/dist/cli-auth-onboarding.js +21 -0
  52. package/dist/cli-auth-onboarding.js.map +1 -0
  53. package/dist/cli-branch-command.d.ts +14 -19
  54. package/dist/cli-branch-command.d.ts.map +1 -1
  55. package/dist/cli-branch-command.js +207 -137
  56. package/dist/cli-branch-command.js.map +1 -1
  57. package/dist/cli-capabilities.d.ts +10 -51
  58. package/dist/cli-capabilities.d.ts.map +1 -1
  59. package/dist/cli-capabilities.js +13 -26
  60. package/dist/cli-capabilities.js.map +1 -1
  61. package/dist/cli-cli-command.d.ts +3 -0
  62. package/dist/cli-cli-command.d.ts.map +1 -1
  63. package/dist/cli-cli-command.js +15 -3
  64. package/dist/cli-cli-command.js.map +1 -1
  65. package/dist/cli-conversion-derivation.d.ts +30 -0
  66. package/dist/cli-conversion-derivation.d.ts.map +1 -0
  67. package/dist/{cli-export-command.js → cli-conversion-derivation.js} +178 -110
  68. package/dist/cli-conversion-derivation.js.map +1 -0
  69. package/dist/cli-conversion-output-directory.d.ts +5 -0
  70. package/dist/cli-conversion-output-directory.d.ts.map +1 -0
  71. package/dist/{cli-export-output-directory.js → cli-conversion-output-directory.js} +10 -10
  72. package/dist/cli-conversion-output-directory.js.map +1 -0
  73. package/dist/cli-convert-command.d.ts +9 -0
  74. package/dist/cli-convert-command.d.ts.map +1 -1
  75. package/dist/cli-convert-command.js +528 -158
  76. package/dist/cli-convert-command.js.map +1 -1
  77. package/dist/cli-corpus-command.d.ts.map +1 -1
  78. package/dist/cli-corpus-command.js +77 -10
  79. package/dist/cli-corpus-command.js.map +1 -1
  80. package/dist/cli-corpus-reference-downloads.d.ts +5 -0
  81. package/dist/cli-corpus-reference-downloads.d.ts.map +1 -1
  82. package/dist/cli-corpus-reference-downloads.js +18 -7
  83. package/dist/cli-corpus-reference-downloads.js.map +1 -1
  84. package/dist/cli-corpus-run.d.ts +9 -0
  85. package/dist/cli-corpus-run.d.ts.map +1 -1
  86. package/dist/cli-corpus-run.js +18 -0
  87. package/dist/cli-corpus-run.js.map +1 -1
  88. package/dist/cli-detach.d.ts +12 -4
  89. package/dist/cli-detach.d.ts.map +1 -1
  90. package/dist/cli-detach.js +49 -17
  91. package/dist/cli-detach.js.map +1 -1
  92. package/dist/cli-draft-command.d.ts +44 -61
  93. package/dist/cli-draft-command.d.ts.map +1 -1
  94. package/dist/cli-draft-command.js +307 -180
  95. package/dist/cli-draft-command.js.map +1 -1
  96. package/dist/cli-help/conversion.d.ts +30 -0
  97. package/dist/cli-help/conversion.d.ts.map +1 -0
  98. package/dist/cli-help/conversion.js +55 -0
  99. package/dist/cli-help/conversion.js.map +1 -0
  100. package/dist/cli-help/design.d.ts +74 -0
  101. package/dist/cli-help/design.d.ts.map +1 -0
  102. package/dist/cli-help/design.js +131 -0
  103. package/dist/cli-help/design.js.map +1 -0
  104. package/dist/cli-help/index.d.ts +224 -0
  105. package/dist/cli-help/index.d.ts.map +1 -0
  106. package/dist/cli-help/index.js +32 -0
  107. package/dist/cli-help/index.js.map +1 -0
  108. package/dist/cli-help/meta.d.ts +24 -0
  109. package/dist/cli-help/meta.d.ts.map +1 -0
  110. package/dist/cli-help/meta.js +30 -0
  111. package/dist/cli-help/meta.js.map +1 -0
  112. package/dist/cli-help/render.d.ts +4 -0
  113. package/dist/cli-help/render.d.ts.map +1 -0
  114. package/dist/cli-help/render.js +28 -0
  115. package/dist/cli-help/render.js.map +1 -0
  116. package/dist/cli-help/setup.d.ts +93 -0
  117. package/dist/cli-help/setup.d.ts.map +1 -0
  118. package/dist/cli-help/setup.js +142 -0
  119. package/dist/cli-help/setup.js.map +1 -0
  120. package/dist/cli-help/types.d.ts +10 -0
  121. package/dist/cli-help/types.d.ts.map +1 -0
  122. package/dist/cli-help/types.js +2 -0
  123. package/dist/cli-help/types.js.map +1 -0
  124. package/dist/cli-next-command.d.ts +11 -8
  125. package/dist/cli-next-command.d.ts.map +1 -1
  126. package/dist/cli-next-command.js +288 -88
  127. package/dist/cli-next-command.js.map +1 -1
  128. package/dist/cli-output-paths.d.ts +2 -23
  129. package/dist/cli-output-paths.d.ts.map +1 -1
  130. package/dist/cli-output-paths.js +2 -36
  131. package/dist/cli-output-paths.js.map +1 -1
  132. package/dist/cli-package-command.d.ts +1 -0
  133. package/dist/cli-package-command.d.ts.map +1 -1
  134. package/dist/cli-package-command.js +107 -34
  135. package/dist/cli-package-command.js.map +1 -1
  136. package/dist/cli-pending-auth.d.ts +20 -0
  137. package/dist/cli-pending-auth.d.ts.map +1 -0
  138. package/dist/cli-pending-auth.js +160 -0
  139. package/dist/cli-pending-auth.js.map +1 -0
  140. package/dist/cli-progress.d.ts +256 -75
  141. package/dist/cli-progress.d.ts.map +1 -1
  142. package/dist/cli-progress.js +425 -98
  143. package/dist/cli-progress.js.map +1 -1
  144. package/dist/cli-run.d.ts +2 -0
  145. package/dist/cli-run.d.ts.map +1 -1
  146. package/dist/cli-run.js +56 -75
  147. package/dist/cli-run.js.map +1 -1
  148. package/dist/cli-skill-command.d.ts +3 -0
  149. package/dist/cli-skill-command.d.ts.map +1 -1
  150. package/dist/cli-skill-command.js +26 -3
  151. package/dist/cli-skill-command.js.map +1 -1
  152. package/dist/cli-usage.d.ts +17 -8
  153. package/dist/cli-usage.d.ts.map +1 -1
  154. package/dist/cli-usage.js +55 -40
  155. package/dist/cli-usage.js.map +1 -1
  156. package/dist/config.d.ts +1 -0
  157. package/dist/config.d.ts.map +1 -1
  158. package/dist/config.js +8 -2
  159. package/dist/config.js.map +1 -1
  160. package/dist/conversion-client.d.ts +4 -0
  161. package/dist/conversion-client.d.ts.map +1 -1
  162. package/dist/conversion-client.js +23 -3
  163. package/dist/conversion-client.js.map +1 -1
  164. package/dist/conversion-outputs.d.ts +11 -0
  165. package/dist/conversion-outputs.d.ts.map +1 -1
  166. package/dist/conversion-outputs.js +13 -1
  167. package/dist/conversion-outputs.js.map +1 -1
  168. package/dist/conversion-receipt.d.ts +1 -1
  169. package/dist/conversion-receipt.js +1 -1
  170. package/dist/conversion-recovery.d.ts +20 -0
  171. package/dist/conversion-recovery.d.ts.map +1 -0
  172. package/dist/conversion-recovery.js +61 -0
  173. package/dist/conversion-recovery.js.map +1 -0
  174. package/dist/conversion-status-contract.js +1 -1
  175. package/dist/conversion-status-contract.js.map +1 -1
  176. package/dist/credential-setup.d.ts +9 -1
  177. package/dist/credential-setup.d.ts.map +1 -1
  178. package/dist/credential-setup.js +13 -1
  179. package/dist/credential-setup.js.map +1 -1
  180. package/dist/draft-adoption.d.ts +9 -3
  181. package/dist/draft-adoption.d.ts.map +1 -1
  182. package/dist/draft-adoption.js +26 -41
  183. package/dist/draft-adoption.js.map +1 -1
  184. package/dist/draft-identity.d.ts +9 -12
  185. package/dist/draft-identity.d.ts.map +1 -1
  186. package/dist/draft-identity.js +0 -0
  187. package/dist/draft-identity.js.map +1 -1
  188. package/dist/draft-run.d.ts +9 -19
  189. package/dist/draft-run.d.ts.map +1 -1
  190. package/dist/draft-run.js +67 -95
  191. package/dist/draft-run.js.map +1 -1
  192. package/dist/draft-workspace.d.ts +26 -18
  193. package/dist/draft-workspace.d.ts.map +1 -1
  194. package/dist/draft-workspace.js +16 -5
  195. package/dist/draft-workspace.js.map +1 -1
  196. package/dist/hosted-draft-identity.d.ts +20 -0
  197. package/dist/hosted-draft-identity.d.ts.map +1 -0
  198. package/dist/hosted-draft-identity.js +30 -0
  199. package/dist/hosted-draft-identity.js.map +1 -0
  200. package/dist/hosted-draft-run.d.ts +60 -0
  201. package/dist/hosted-draft-run.d.ts.map +1 -0
  202. package/dist/hosted-draft-run.js +97 -0
  203. package/dist/hosted-draft-run.js.map +1 -0
  204. package/dist/legacy-codex-cleanup.d.ts +13 -0
  205. package/dist/legacy-codex-cleanup.d.ts.map +1 -1
  206. package/dist/legacy-codex-cleanup.js +83 -0
  207. package/dist/legacy-codex-cleanup.js.map +1 -1
  208. package/dist/legacy-skill-catalog.d.ts +2 -2
  209. package/dist/legacy-skill-catalog.d.ts.map +1 -1
  210. package/dist/legacy-skill-catalog.js +76 -5
  211. package/dist/legacy-skill-catalog.js.map +1 -1
  212. package/dist/login-auth.d.ts +38 -0
  213. package/dist/login-auth.d.ts.map +1 -1
  214. package/dist/login-auth.js +67 -16
  215. package/dist/login-auth.js.map +1 -1
  216. package/dist/mcp-tool-definitions.js +1 -1
  217. package/dist/mcp-tool-definitions.js.map +1 -1
  218. package/dist/next-target.d.ts +1 -4
  219. package/dist/next-target.d.ts.map +1 -1
  220. package/dist/next-target.js +5 -9
  221. package/dist/next-target.js.map +1 -1
  222. package/dist/next-wait-lease.d.ts +17 -0
  223. package/dist/next-wait-lease.d.ts.map +1 -0
  224. package/dist/next-wait-lease.js +92 -0
  225. package/dist/next-wait-lease.js.map +1 -0
  226. package/dist/operation-poll-policy.d.ts +89 -0
  227. package/dist/operation-poll-policy.d.ts.map +1 -0
  228. package/dist/operation-poll-policy.js +99 -0
  229. package/dist/operation-poll-policy.js.map +1 -0
  230. package/dist/package-client.d.ts +30 -0
  231. package/dist/package-client.d.ts.map +1 -1
  232. package/dist/package-client.js +48 -9
  233. package/dist/package-client.js.map +1 -1
  234. package/dist/package-submission.d.ts +17 -0
  235. package/dist/package-submission.d.ts.map +1 -1
  236. package/dist/package-submission.js +43 -2
  237. package/dist/package-submission.js.map +1 -1
  238. package/dist/pending-auth-store.d.ts +15 -0
  239. package/dist/pending-auth-store.d.ts.map +1 -0
  240. package/dist/pending-auth-store.js +90 -0
  241. package/dist/pending-auth-store.js.map +1 -0
  242. package/dist/responsive-quality.d.ts +2 -2
  243. package/dist/responsive-quality.js +2 -2
  244. package/dist/run-journal-commands.d.ts.map +1 -1
  245. package/dist/run-journal-commands.js +30 -78
  246. package/dist/run-journal-commands.js.map +1 -1
  247. package/dist/run-journal-in-flight.d.ts +2 -2
  248. package/dist/run-journal-in-flight.d.ts.map +1 -1
  249. package/dist/run-journal-in-flight.js +0 -0
  250. package/dist/run-journal-in-flight.js.map +1 -1
  251. package/dist/skill-installer.d.ts +2 -2
  252. package/dist/skill-installer.d.ts.map +1 -1
  253. package/dist/skill-installer.js +2 -2
  254. package/dist/skill-installer.js.map +1 -1
  255. package/open-design.json +3 -3
  256. package/package.json +1 -1
  257. package/skills/12ui-design/SKILL.md +62 -0
  258. package/skills/12ui-design/agents/openai.yaml +6 -0
  259. package/skills/12ui-design/inspire.md +11 -0
  260. package/dist/cli-create-command.d.ts +0 -59
  261. package/dist/cli-create-command.d.ts.map +0 -1
  262. package/dist/cli-create-command.js +0 -354
  263. package/dist/cli-create-command.js.map +0 -1
  264. package/dist/cli-export-command.d.ts +0 -26
  265. package/dist/cli-export-command.d.ts.map +0 -1
  266. package/dist/cli-export-command.js.map +0 -1
  267. package/dist/cli-export-output-directory.d.ts +0 -5
  268. package/dist/cli-export-output-directory.d.ts.map +0 -1
  269. package/dist/cli-export-output-directory.js.map +0 -1
  270. package/dist/cli-image-batch-command.d.ts +0 -29
  271. package/dist/cli-image-batch-command.d.ts.map +0 -1
  272. package/dist/cli-image-batch-command.js +0 -236
  273. package/dist/cli-image-batch-command.js.map +0 -1
  274. package/dist/cli-image-batch-materialize.d.ts +0 -27
  275. package/dist/cli-image-batch-materialize.d.ts.map +0 -1
  276. package/dist/cli-image-batch-materialize.js +0 -41
  277. package/dist/cli-image-batch-materialize.js.map +0 -1
  278. package/dist/cli-image-batch-resume.d.ts +0 -36
  279. package/dist/cli-image-batch-resume.d.ts.map +0 -1
  280. package/dist/cli-image-batch-resume.js +0 -176
  281. package/dist/cli-image-batch-resume.js.map +0 -1
  282. package/dist/cli-image-command.d.ts +0 -10
  283. package/dist/cli-image-command.d.ts.map +0 -1
  284. package/dist/cli-image-command.js +0 -106
  285. package/dist/cli-image-command.js.map +0 -1
  286. package/dist/cli-select-command.d.ts +0 -16
  287. package/dist/cli-select-command.d.ts.map +0 -1
  288. package/dist/cli-select-command.js +0 -60
  289. package/dist/cli-select-command.js.map +0 -1
  290. package/dist/create-identity.d.ts +0 -25
  291. package/dist/create-identity.d.ts.map +0 -1
  292. package/dist/create-identity.js +0 -0
  293. package/dist/create-identity.js.map +0 -1
  294. package/dist/draft-prompts.d.ts +0 -35
  295. package/dist/draft-prompts.d.ts.map +0 -1
  296. package/dist/draft-prompts.js +0 -51
  297. package/dist/draft-prompts.js.map +0 -1
  298. package/dist/image-batch-status.d.ts +0 -42
  299. package/dist/image-batch-status.d.ts.map +0 -1
  300. package/dist/image-batch-status.js +0 -143
  301. package/dist/image-batch-status.js.map +0 -1
  302. package/dist/image-generation-batch-attempt.d.ts +0 -28
  303. package/dist/image-generation-batch-attempt.d.ts.map +0 -1
  304. package/dist/image-generation-batch-attempt.js +0 -48
  305. package/dist/image-generation-batch-attempt.js.map +0 -1
  306. package/dist/image-generation-batch-manifest.d.ts +0 -25
  307. package/dist/image-generation-batch-manifest.d.ts.map +0 -1
  308. package/dist/image-generation-batch-manifest.js +0 -97
  309. package/dist/image-generation-batch-manifest.js.map +0 -1
  310. package/dist/image-generation-completion.d.ts +0 -43
  311. package/dist/image-generation-completion.d.ts.map +0 -1
  312. package/dist/image-generation-completion.js +0 -54
  313. package/dist/image-generation-completion.js.map +0 -1
  314. package/dist/image-generation-error.d.ts +0 -3
  315. package/dist/image-generation-error.d.ts.map +0 -1
  316. package/dist/image-generation-error.js +0 -30
  317. package/dist/image-generation-error.js.map +0 -1
  318. package/dist/image-generation-evidence.d.ts +0 -43
  319. package/dist/image-generation-evidence.d.ts.map +0 -1
  320. package/dist/image-generation-evidence.js +0 -28
  321. package/dist/image-generation-evidence.js.map +0 -1
  322. package/dist/image-generation-fs.d.ts +0 -3
  323. package/dist/image-generation-fs.d.ts.map +0 -1
  324. package/dist/image-generation-fs.js +0 -23
  325. package/dist/image-generation-fs.js.map +0 -1
  326. package/dist/image-generation-output.d.ts +0 -3
  327. package/dist/image-generation-output.d.ts.map +0 -1
  328. package/dist/image-generation-output.js +0 -51
  329. package/dist/image-generation-output.js.map +0 -1
  330. package/dist/image-generation-request.d.ts +0 -43
  331. package/dist/image-generation-request.d.ts.map +0 -1
  332. package/dist/image-generation-request.js +0 -52
  333. package/dist/image-generation-request.js.map +0 -1
  334. package/dist/image-generation-resume.d.ts +0 -49
  335. package/dist/image-generation-resume.d.ts.map +0 -1
  336. package/dist/image-generation-resume.js +0 -96
  337. package/dist/image-generation-resume.js.map +0 -1
  338. package/dist/image-generation-run.d.ts +0 -46
  339. package/dist/image-generation-run.d.ts.map +0 -1
  340. package/dist/image-generation-run.js +0 -158
  341. package/dist/image-generation-run.js.map +0 -1
  342. package/dist/image-generation-staging.d.ts +0 -25
  343. package/dist/image-generation-staging.d.ts.map +0 -1
  344. package/dist/image-generation-staging.js +0 -80
  345. package/dist/image-generation-staging.js.map +0 -1
  346. package/dist/image-generation-usage.d.ts +0 -75
  347. package/dist/image-generation-usage.d.ts.map +0 -1
  348. package/dist/image-generation-usage.js +0 -219
  349. package/dist/image-generation-usage.js.map +0 -1
  350. package/dist/image-provider-preflight.d.ts +0 -21
  351. package/dist/image-provider-preflight.d.ts.map +0 -1
  352. package/dist/image-provider-preflight.js +0 -184
  353. package/dist/image-provider-preflight.js.map +0 -1
  354. package/dist/selection-record.d.ts +0 -97
  355. package/dist/selection-record.d.ts.map +0 -1
  356. package/dist/selection-record.js +0 -138
  357. package/dist/selection-record.js.map +0 -1
  358. package/skills/design/SKILL.md +0 -116
  359. package/skills/design/agents/openai.yaml +0 -8
  360. package/skills/design/assets/large-logo.svg +0 -3
  361. package/skills/design/assets/small-400px.png +0 -0
@@ -3,19 +3,21 @@ import { readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { materializeConversionArtifacts } from './artifact-materializer.js';
5
5
  import { booleanFlag, stringFlag } from './cli-arguments.js';
6
- import { prepareOutputFile, prepareOutputTarget } from './cli-output-paths.js';
7
- import { CONVERSION_DURATION_HINTS, createProgressReporter, durationHint, nextActionLine, } from './cli-progress.js';
8
- import { refuseUsage, usage } from './cli-usage.js';
6
+ import { prepareOutputDirectory, prepareOutputFile } from './cli-output-paths.js';
7
+ import { runConversionDerivation } from './cli-conversion-derivation.js';
8
+ import { CONVERSION_DURATION_HINTS, createLongRunningProgress, } from './cli-progress.js';
9
+ import { refuseUsage, usage, usageRefusal } from './cli-usage.js';
9
10
  import { readConversionConfig } from './config.js';
10
11
  import { TwelveUiConversionClient, } from './conversion-client.js';
11
12
  import { isConversionModel } from './conversion-models.js';
12
- import { isConversionOutput } from './conversion-outputs.js';
13
+ import { isCliConversionOutput, } from './conversion-outputs.js';
13
14
  import { recordConvertedImageReceipt } from './conversion-receipt.js';
15
+ import { readConversionRecovery, writeConversionRecovery, } from './conversion-recovery.js';
14
16
  import { isPendingConversionStatus, readConversionStatus, wrongConversionOutputRefusal, } from './conversion-status-contract.js';
15
17
  import { normalizeOptionalIdempotencyKey } from './idempotency-key.js';
16
18
  import { classifyLocalConversionInput, createInputForLocalFile, defaultConversionOutputPath, } from './local-conversion-input.js';
17
19
  import { normalizeImageOutputWidth, parseImageOutputWidth, } from './image-output-width.js';
18
- import { isProjectConversionOutput, parseConversionOutputProfile, } from './project-profiles.js';
20
+ import { resolveNextTarget } from './next-target.js';
19
21
  import { isResponsiveConversionQuality, RESPONSIVE_CONVERSION_QUALITIES, } from './responsive-quality.js';
20
22
  import { describeInFlightRecord, readInFlightRunJournal, } from './run-journal-in-flight.js';
21
23
  /**
@@ -32,27 +34,6 @@ export const parseTimeoutMs = (value) => {
32
34
  }
33
35
  return Math.floor(timeoutMs);
34
36
  };
35
- const parseProjectProfileFlag = async (output, value) => {
36
- if (!isProjectConversionOutput(output)) {
37
- if (value !== undefined)
38
- throw new Error('--profile is only valid for project outputs');
39
- return undefined;
40
- }
41
- if (!value?.trim())
42
- throw new Error(`--profile is required for ${output}`);
43
- const trimmed = value.trim();
44
- const source = trimmed.startsWith('{')
45
- ? trimmed
46
- : await readFile(path.resolve(trimmed.startsWith('@') ? trimmed.slice(1) : trimmed), 'utf8');
47
- let parsed;
48
- try {
49
- parsed = JSON.parse(source);
50
- }
51
- catch (error) {
52
- throw new Error(`--profile must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
53
- }
54
- return parseConversionOutputProfile(output, parsed);
55
- };
56
37
  const downloadCompletedConversion = async (args) => {
57
38
  const downloaded = await args.client.downloadResult(args.operation.id, args.output);
58
39
  await writeFile(args.outFile, downloaded.bytes);
@@ -61,9 +42,47 @@ const downloadCompletedConversion = async (args) => {
61
42
  id: args.operation.id,
62
43
  output: args.output,
63
44
  outFile: args.outFile,
64
- artifactManifest,
45
+ // The artifact inventory lives in the on-disk manifest the materializer
46
+ // just wrote; the payload references it instead of inlining the array.
47
+ // The board's fused capture spent 243 of 257 lines printing the same 14
48
+ // artifacts twice — the manifest file already held every byte of it.
49
+ artifactManifest: {
50
+ conversionId: artifactManifest.conversionId,
51
+ operationExpiresAt: artifactManifest.operationExpiresAt,
52
+ manifestFile: artifactManifest.manifestFile,
53
+ artifactCount: artifactManifest.artifacts.length,
54
+ },
65
55
  };
66
56
  };
57
+ const reportConversion = (reporter, operation, detail) => {
58
+ reporter.update({
59
+ // A succeeded child can still be followed by local materialization or by
60
+ // another member of a fused request. Keep the overall line honest: the
61
+ // reporter owns terminality, while a child success means collection is
62
+ // beginning rather than `running | stage succeeded`.
63
+ stage: operation.status === 'succeeded'
64
+ ? 'collecting'
65
+ : operation.progress?.stage ?? operation.status,
66
+ // The raw percent is deliberately NOT forwarded (owner, 2026-08-21):
67
+ // it is the measured stuck metric. The service's per-lane counters are
68
+ // the advancing signal behind every stale percent (`extracting layers
69
+ // 2%` held 45s on the board while the lanes moved); render them
70
+ // rather than discarding them.
71
+ ...(operation.status !== 'succeeded' && operation.progress?.active?.length
72
+ ? { lanes: operation.progress.active }
73
+ : {}),
74
+ ...(operation.status !== 'succeeded' && operation.progress?.milestone
75
+ ? { milestone: operation.progress.milestone }
76
+ : {}),
77
+ ...(operation.status !== 'succeeded' && operation.progress?.stage_since
78
+ ? { stageSince: operation.progress.stage_since }
79
+ : {}),
80
+ ...(operation.status !== 'succeeded' && operation.progress?.attempt !== undefined
81
+ ? { attempt: operation.progress.attempt }
82
+ : {}),
83
+ ...(detail ? { detail } : {}),
84
+ });
85
+ };
67
86
  /**
68
87
  * A LayerDoc landed on disk and no page was written from it.
69
88
  *
@@ -78,6 +97,20 @@ const downloadCompletedConversion = async (args) => {
78
97
  export const layerDocPageAction = (outFile) => (`The document is written and no page was rendered from it. To render one:`
79
98
  + ` \`12ui convert ${outFile} --output html_fixed\`. A LayerDoc on disk is valid input for`
80
99
  + ' every output, so this derives the page rather than converting the image again.');
100
+ export const layerDocStateAction = (args) => {
101
+ const linked = args.linkedExport;
102
+ if (!linked)
103
+ return layerDocPageAction(args.outFile);
104
+ if (linked.status === 'succeeded') {
105
+ return `Responsive page already exported${linked.outFile ? `: ${linked.outFile}` : ''}`
106
+ + ` (export ${linked.id}).`;
107
+ }
108
+ if (isPendingConversionStatus(linked.status)) {
109
+ return `Responsive page export ${linked.id} is ${linked.status};`
110
+ + ' the original purchase is still working.';
111
+ }
112
+ return `Responsive page export ${linked.id} is ${linked.status}; no additional conversion was bought.`;
113
+ };
81
114
  /**
82
115
  * A conversion of these exact bytes is already running in this workspace.
83
116
  *
@@ -105,30 +138,142 @@ export const secondConversionNotice = (args) => ('These exact bytes are already
105
138
  + ` Read it from any shell with \`12ui next ${args.workspace}\` — that costs nothing and does not`
106
139
  + ' wait on this command. Continuing starts a SECOND, independently paid conversion of the same'
107
140
  + ' image.');
108
- /** The result JSON, plus the page action whenever a document stands alone. */
109
- const conversionResultJson = (result) => JSON.stringify({
141
+ /** The result JSON, with guidance derived from the linked operation's real state. */
142
+ const conversionResultJson = (result, linkedExport) => JSON.stringify({
110
143
  ...result,
111
- ...(result.output === 'layerdoc' ? { nextAction: layerDocPageAction(result.outFile) } : {}),
144
+ ...(result.output === 'layerdoc'
145
+ ? { nextAction: layerDocStateAction({ outFile: result.outFile, ...(linkedExport ? { linkedExport } : {}) }) }
146
+ : {}),
112
147
  }, null, 2);
113
- const writeCompletedConversion = async (args) => {
114
- console.log(conversionResultJson(await downloadCompletedConversion(args)));
148
+ /**
149
+ * stdout is the machine surface: exactly one JSON document, nothing else.
150
+ * Human summaries (paths, per-format states) live in the stderr terminal
151
+ * block, so an agent can parse stdout without stripping trailing prose.
152
+ */
153
+ const writeResultWithSummary = (result) => {
154
+ console.log(JSON.stringify(result, null, 2));
155
+ };
156
+ const outputPathForRecovery = async (artifact, overrideDirectory) => prepareOutputFile(overrideDirectory
157
+ ? path.join(overrideDirectory, path.basename(artifact.outFile))
158
+ : artifact.outFile);
159
+ const completedOperation = async (args) => {
160
+ const current = await args.client.getConversion(args.id);
161
+ args.onProgress?.(current);
162
+ return current.status === 'succeeded'
163
+ ? current
164
+ : args.client.waitForConversion(current.id, {
165
+ expiresAt: current.expires_at,
166
+ timeoutMs: args.timeoutMs,
167
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
168
+ });
169
+ };
170
+ const recoverOriginalPurchase = async (args) => {
171
+ const overrideDirectory = args.outDir
172
+ ? await prepareOutputDirectory(args.outDir)
173
+ : undefined;
174
+ const conversionOutFile = await outputPathForRecovery(args.record.conversion, overrideDirectory);
175
+ const linkedOutFile = args.record.linkedExport
176
+ ? await outputPathForRecovery(args.record.linkedExport, overrideDirectory)
177
+ : undefined;
178
+ const conversion = await completedOperation({
179
+ client: args.client,
180
+ id: args.record.conversion.conversionId,
181
+ ...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs }),
182
+ onProgress: (operation) => reportConversion(args.progress, operation, `recovering conversion ${args.record.conversion.conversionId}`),
183
+ });
184
+ if (conversion.output !== args.record.conversion.output) {
185
+ throw new Error(`Recovery record expects ${args.record.conversion.output}, but conversion ${conversion.id}`
186
+ + ` produces ${conversion.output}.`);
187
+ }
188
+ const converted = await downloadCompletedConversion({
189
+ client: args.client,
190
+ operation: conversion,
191
+ output: args.record.conversion.output,
192
+ outFile: conversionOutFile,
193
+ });
194
+ let linkedResult;
195
+ let linkedOperation;
196
+ if (args.record.linkedExport && linkedOutFile) {
197
+ linkedOperation = await completedOperation({
198
+ client: args.client,
199
+ id: args.record.linkedExport.conversionId,
200
+ ...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs }),
201
+ onProgress: (operation) => reportConversion(args.progress, operation, `recovering linked export ${args.record.linkedExport.conversionId}`),
202
+ });
203
+ if (linkedOperation.output !== args.record.linkedExport.output) {
204
+ throw new Error(`Recovery record expects ${args.record.linkedExport.output}, but linked conversion`
205
+ + ` ${linkedOperation.id} produces ${linkedOperation.output}.`);
206
+ }
207
+ linkedResult = await downloadCompletedConversion({
208
+ client: args.client,
209
+ operation: linkedOperation,
210
+ output: args.record.linkedExport.output,
211
+ outFile: linkedOutFile,
212
+ });
213
+ }
214
+ const artifacts = [conversionOutFile, ...(linkedOutFile ? [linkedOutFile] : [])];
215
+ args.journal.note({ runId: conversion.id, artifacts, terminal: true });
216
+ const nextAction = conversion.output === 'layerdoc'
217
+ ? layerDocStateAction({
218
+ outFile: conversionOutFile,
219
+ ...(linkedOperation
220
+ ? {
221
+ linkedExport: {
222
+ id: linkedOperation.id,
223
+ status: linkedOperation.status,
224
+ ...(linkedOutFile ? { outFile: linkedOutFile } : {}),
225
+ },
226
+ }
227
+ : {}),
228
+ })
229
+ : undefined;
230
+ args.progress.finish('succeeded', {
231
+ artifacts: [
232
+ {
233
+ name: args.record.conversion.output,
234
+ state: 'recovered free',
235
+ path: conversionOutFile,
236
+ },
237
+ ...(linkedOutFile
238
+ ? [{ name: 'responsive html', state: 'recovered free', path: linkedOutFile }]
239
+ : []),
240
+ ],
241
+ spend: 'nothing new — the original purchase was collected',
242
+ });
243
+ writeResultWithSummary({
244
+ ...converted,
245
+ ...(nextAction ? { nextAction } : {}),
246
+ ...(linkedResult ? { export: { ...linkedResult, status: 'succeeded' } } : {}),
247
+ });
115
248
  };
116
249
  export const runConversionCommand = async (command, flags, journal) => {
117
250
  const commandStartedAt = Date.now();
118
251
  const verb = command[0];
119
- // `12ui convert <image> --export html` is the whole command for a single
120
- // screen. The export is derived from a LayerDoc conversion, so asking for the
121
- // output as well is asking the caller to restate the only value it can have.
122
- const outputValue = stringFlag(flags, 'output')
123
- ?? (stringFlag(flags, 'export') !== undefined ? 'layerdoc' : undefined);
124
- const output = isConversionOutput(outputValue) ? outputValue : refuseUsage({
125
- reason: outputValue === undefined
126
- ? `${verb === 'resume' ? 'A resume collects' : 'A conversion produces'} one named format,`
127
- + ' and --output was not given.'
128
- : `\`${outputValue}\` is not one of the outputs this CLI produces.`,
129
- code: 'cli_usage',
130
- verb: [verb === 'resume' ? 'resume' : 'convert'],
131
- });
252
+ if (verb === 'convert') {
253
+ const retiredFlags = [
254
+ 'export', 'export-quality', 'export-out', 'out', 'profile',
255
+ 'web-profile', 'app-profile', 'detach',
256
+ ].filter((flag) => flags[flag] !== undefined);
257
+ if (retiredFlags.length > 0) {
258
+ throw new Error(`${retiredFlags.map((flag) => `--${flag}`).join(', ')} retired from 12ui convert.`
259
+ + ' Use --output to name the result and --out-dir to name its directory.');
260
+ }
261
+ const target = command[1] ? await resolveNextTarget(command[1]) : null;
262
+ if (target?.kind === 'conversion') {
263
+ for (const flag of ['model', 'width']) {
264
+ if (flags[flag] !== undefined) {
265
+ throw new Error(`--${flag} applies to image input, not a conversion id`);
266
+ }
267
+ }
268
+ const derived = await runConversionDerivation(target.conversionId, flags);
269
+ journal.note({
270
+ runId: target.conversionId,
271
+ artifacts: [derived.manifestPath, ...derived.results.map((result) => result.outFile)],
272
+ purchased: derived.results.map((result) => result.conversionId),
273
+ });
274
+ return;
275
+ }
276
+ }
132
277
  const timeoutMs = parseTimeoutMs(stringFlag(flags, 'timeout-ms'));
133
278
  // Credentials resolve only after the invocation has proven usable: a usage
134
279
  // refusal must never depend on having an API key. The deploy gate caught the
@@ -138,58 +283,189 @@ export const runConversionCommand = async (command, flags, journal) => {
138
283
  apiKey: stringFlag(flags, 'api-key'),
139
284
  }));
140
285
  if (verb === 'resume') {
141
- if (stringFlag(flags, 'profile') !== undefined
142
- || stringFlag(flags, 'model') !== undefined
286
+ if (stringFlag(flags, 'model') !== undefined
143
287
  || stringFlag(flags, 'width') !== undefined
144
288
  || stringFlag(flags, 'responsive-quality') !== undefined
145
289
  || booleanFlag(flags, 'detach')) {
146
- throw new Error('--profile, --model, --width, --responsive-quality, and --detach are not accepted by resume');
290
+ throw new Error('--model, --width, --responsive-quality, and --detach are not accepted by resume');
147
291
  }
148
292
  const conversionId = command[1]?.trim();
149
- const outFile = stringFlag(flags, 'out');
150
293
  if (!conversionId) {
151
294
  refuseUsage({
152
- reason: 'A resume collects one conversion already bought, and no conversion id was given.'
295
+ reason: 'A resume collects one purchase already bought, and no conversion id was given.'
153
296
  + ' Every command that starts one prints it.',
154
297
  code: 'cli_usage',
155
298
  verb: ['resume'],
156
299
  });
157
300
  }
158
- // File-only on purpose: a resume writes exactly one artifact, so there is
159
- // nothing here to derive a name from the way `convert --out <dir>` does.
160
- const resolvedOutFile = await prepareOutputFile(outFile || refuseUsage({
161
- reason: 'A resume writes the collected result to a file, and --out was not given.',
162
- code: 'cli_usage',
163
- verb: ['resume'],
164
- }));
165
301
  const client = conversionClient();
166
- const current = await client.getConversion(conversionId);
167
- if (current.output !== output) {
302
+ const outputValue = stringFlag(flags, 'output');
303
+ const outFile = stringFlag(flags, 'out');
304
+ if (outputValue === undefined && outFile === undefined) {
305
+ const recovery = await readConversionRecovery(conversionId);
306
+ if (!recovery) {
307
+ throw usageRefusal({
308
+ reason: `No local recovery record was found for ${conversionId}. Bare resume works for`
309
+ + ' purchases started by this CLI; use the power form below for an older or foreign id.',
310
+ code: 'resume_recovery_missing',
311
+ verb: ['resume'],
312
+ });
313
+ }
314
+ const progress = createLongRunningProgress({
315
+ label: `resume ${conversionId}`,
316
+ typicalSeconds: recovery.linkedExport
317
+ ? CONVERSION_DURATION_HINTS.fused
318
+ : CONVERSION_DURATION_HINTS.convert,
319
+ quiet: booleanFlag(flags, 'quiet'),
320
+ });
321
+ // A resume collects work already bought. Settled work arrives in
322
+ // seconds, so no expectation is quoted (the board's bare resume quoted
323
+ // ~5m for a 4s collection); only work still running server-side waits.
324
+ // The done paths honor --out-dir: the v2 board caught the brief quoting
325
+ // the record's original directory while the artifacts landed elsewhere.
326
+ const overrideDir = stringFlag(flags, 'out-dir');
327
+ const shownPath = (file) => (overrideDir
328
+ ? path.join(path.resolve(overrideDir), path.basename(file))
329
+ : file);
330
+ progress.brief({
331
+ request: `resume ${conversionId}: collect the original purchase`,
332
+ free: 'everything already settled server-side is recovered without buying again',
333
+ done: recovery.linkedExport
334
+ ? `${shownPath(recovery.conversion.outFile)} + ${shownPath(recovery.linkedExport.outFile)}`
335
+ : shownPath(recovery.conversion.outFile),
336
+ blocking: true,
337
+ });
338
+ progress.update({ stage: 'prepare', detail: 'reading the original purchase' });
339
+ try {
340
+ await recoverOriginalPurchase({
341
+ client,
342
+ record: recovery,
343
+ journal,
344
+ progress,
345
+ ...(stringFlag(flags, 'out-dir') ? { outDir: stringFlag(flags, 'out-dir') } : {}),
346
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
347
+ });
348
+ }
349
+ catch (error) {
350
+ progress.finish('stopped', {
351
+ detail: error instanceof Error ? error.message : String(error),
352
+ next: `re-run 12ui resume ${conversionId}; collection is free until the purchase expires`,
353
+ });
354
+ throw error;
355
+ }
356
+ return;
357
+ }
358
+ if (outputValue === undefined || outFile === undefined) {
359
+ throw usageRefusal({
360
+ reason: 'The optional power form needs both --output and --out; omit both to recover the'
361
+ + ' original purchase and filenames.',
362
+ code: 'resume_power_form_incomplete',
363
+ verb: ['resume'],
364
+ });
365
+ }
366
+ if (stringFlag(flags, 'out-dir') !== undefined) {
367
+ throw new Error('--out-dir overrides original filenames only on bare resume; power resume uses --out');
368
+ }
369
+ const requestedOutput = isCliConversionOutput(outputValue)
370
+ ? outputValue
371
+ : refuseUsage({
372
+ reason: `\`${outputValue}\` is not one of the outputs this CLI produces.`,
373
+ code: 'cli_usage',
374
+ verb: ['resume'],
375
+ });
376
+ const resolvedOutFile = await prepareOutputFile(outFile);
377
+ const progress = createLongRunningProgress({
378
+ label: `resume ${conversionId}`,
379
+ typicalSeconds: requestedOutput === 'html'
380
+ ? CONVERSION_DURATION_HINTS.export
381
+ : CONVERSION_DURATION_HINTS.convert,
382
+ quiet: booleanFlag(flags, 'quiet'),
383
+ });
384
+ progress.brief({
385
+ request: `resume ${conversionId}: collect ${requestedOutput}`,
386
+ free: 'a settled purchase is recovered without buying again',
387
+ done: resolvedOutFile,
388
+ blocking: true,
389
+ });
390
+ progress.update({ stage: 'prepare', detail: 'reading the existing purchase' });
391
+ const current = await client.getConversion(conversionId).catch((error) => {
392
+ progress.finish('stopped', {
393
+ detail: error instanceof Error ? error.message : String(error),
394
+ });
395
+ throw error;
396
+ });
397
+ if (current.output !== requestedOutput) {
398
+ progress.finish('stopped', {
399
+ detail: `purchase produces ${current.output}, not ${requestedOutput}`,
400
+ });
168
401
  // The refusal is written from an object that carries the id the caller
169
402
  // actually wants, so it names both rather than only the unusable one.
170
403
  throw new Error(wrongConversionOutputRefusal({
171
404
  operation: current,
172
- requested: output,
405
+ requested: requestedOutput,
173
406
  collect: (id, kind) => `12ui resume ${id} --output ${kind} --out <file>`,
174
407
  }));
175
408
  }
176
- const completed = current.status === 'succeeded'
177
- ? current
178
- : await client.waitForConversion(current.id, {
179
- expiresAt: current.expires_at,
180
- timeoutMs,
409
+ reportConversion(progress, current, `collecting ${requestedOutput}`);
410
+ let completed;
411
+ try {
412
+ completed = current.status === 'succeeded'
413
+ ? current
414
+ : await client.waitForConversion(current.id, {
415
+ expiresAt: current.expires_at,
416
+ timeoutMs,
417
+ onProgress: (operation) => reportConversion(progress, operation, `collecting ${requestedOutput}`),
418
+ });
419
+ }
420
+ catch (error) {
421
+ progress.finish('stopped', {
422
+ detail: error instanceof Error ? error.message : String(error),
423
+ next: `re-run 12ui resume ${conversionId} --output ${requestedOutput} --out ${resolvedOutFile};`
424
+ + ' collection is free until the purchase expires',
181
425
  });
182
- await writeCompletedConversion({
426
+ throw error;
427
+ }
428
+ const downloaded = await downloadCompletedConversion({
183
429
  client,
184
430
  operation: completed,
185
- output,
431
+ output: requestedOutput,
186
432
  outFile: resolvedOutFile,
187
433
  });
188
434
  journal.note({ runId: completed.id, artifacts: [resolvedOutFile] });
435
+ let linkedExport;
436
+ if (completed.output === 'layerdoc' && completed.export?.conversion_id) {
437
+ const linked = await client.getConversion(completed.export.conversion_id);
438
+ linkedExport = { id: linked.id, status: linked.status };
439
+ }
440
+ progress.finish('succeeded', {
441
+ artifacts: [{ name: requestedOutput, state: 'recovered free', path: resolvedOutFile }],
442
+ spend: 'nothing new — the existing purchase was collected',
443
+ });
444
+ writeResultWithSummary({
445
+ ...downloaded,
446
+ ...(downloaded.output === 'layerdoc'
447
+ ? {
448
+ nextAction: layerDocStateAction({
449
+ outFile: downloaded.outFile,
450
+ ...(linkedExport ? { linkedExport } : {}),
451
+ }),
452
+ }
453
+ : {}),
454
+ });
189
455
  return;
190
456
  }
191
457
  if (verb !== 'convert')
192
458
  usage();
459
+ const outputValue = stringFlag(flags, 'output');
460
+ const requestedOutput = isCliConversionOutput(outputValue)
461
+ ? outputValue
462
+ : refuseUsage({
463
+ reason: outputValue === undefined
464
+ ? 'A conversion produces one named format, and --output was not given.'
465
+ : `\`${outputValue}\` is not one of the outputs this CLI produces.`,
466
+ code: 'cli_usage',
467
+ verb: ['convert'],
468
+ });
193
469
  if (!command[1]) {
194
470
  refuseUsage({
195
471
  reason: 'A conversion starts from one image or one LayerDoc on disk, and no input was named.',
@@ -211,9 +487,10 @@ export const runConversionCommand = async (command, flags, journal) => {
211
487
  if (responsiveValue !== undefined && !isResponsiveConversionQuality(responsiveValue)) {
212
488
  throw new Error(`--responsive-quality must be one of: ${RESPONSIVE_CONVERSION_QUALITIES.join(', ')}`);
213
489
  }
214
- const profile = await parseProjectProfileFlag(output, stringFlag(flags, 'profile'));
215
490
  const inputPath = path.resolve(command[1]);
216
491
  const localInput = classifyLocalConversionInput(inputPath);
492
+ const fusedHtml = localInput.kind === 'image' && requestedOutput === 'html';
493
+ const output = fusedHtml ? 'layerdoc' : requestedOutput;
217
494
  const outputWidth = parseImageOutputWidth(stringFlag(flags, 'width'));
218
495
  if (outputWidth !== undefined && localInput.kind === 'layerdoc') {
219
496
  throw new Error('--width is only valid for image input');
@@ -230,7 +507,6 @@ export const runConversionCommand = async (command, flags, journal) => {
230
507
  bytes: inputBytes,
231
508
  filePath: inputPath,
232
509
  output,
233
- ...(profile ? { profile } : {}),
234
510
  });
235
511
  const idempotencyKey = normalizeOptionalIdempotencyKey(stringFlag(flags, 'idempotency-key'), '--idempotency-key');
236
512
  if (input.layerdoc !== undefined && model)
@@ -238,67 +514,48 @@ export const runConversionCommand = async (command, flags, journal) => {
238
514
  if (input.layerdoc !== undefined && responsiveValue) {
239
515
  throw new Error('--responsive-quality with LayerDoc input requires the SDK source-conversion workflow');
240
516
  }
241
- if (responsiveValue && output !== 'html') {
517
+ if (responsiveValue && requestedOutput !== 'html') {
242
518
  throw new Error('--responsive-quality is only valid with --output html');
243
519
  }
244
- // --export asks for the responsive HTML export to run ALONGSIDE the
245
- // conversion in one blocking command, instead of the create-then-export
246
- // choreography that costs the two runs back to back.
247
- const exportValue = stringFlag(flags, 'export');
248
- if (exportValue !== undefined && exportValue !== 'html') {
249
- throw new Error('--export must be html');
250
- }
251
- const exportQualityValue = stringFlag(flags, 'export-quality');
252
- if (exportQualityValue !== undefined && !isResponsiveConversionQuality(exportQualityValue)) {
253
- throw new Error(`--export-quality must be one of: ${RESPONSIVE_CONVERSION_QUALITIES.join(', ')}`);
254
- }
255
- if (exportQualityValue !== undefined && exportValue === undefined) {
256
- throw new Error('--export-quality requires --export html');
257
- }
258
- const exportOutValue = stringFlag(flags, 'export-out');
259
- if (exportOutValue !== undefined && exportValue === undefined) {
260
- throw new Error('--export-out requires --export html');
261
- }
262
- if (exportValue !== undefined) {
263
- if (input.layerdoc !== undefined)
264
- throw new Error('--export requires image input');
265
- if (output !== 'layerdoc')
266
- throw new Error('--export requires --output layerdoc');
267
- }
268
- const exportHtml = exportValue === 'html'
269
- ? { ...(exportQualityValue ? { responsiveQuality: exportQualityValue } : {}) }
520
+ // Owner-facing `--output html` maps to the existing v1 fused wire shape.
521
+ // The API remains output=layerdoc + exportHtml; only the CLI vocabulary moves.
522
+ const exportHtml = fusedHtml
523
+ ? { ...(responsiveValue ? { responsiveQuality: responsiveValue } : {}) }
270
524
  : undefined;
271
525
  const conversionInput = input.layerdoc !== undefined
272
526
  ? input
273
527
  : {
274
528
  ...input,
275
529
  ...(model ? { model } : {}),
276
- ...(responsiveValue ? { responsiveQuality: responsiveValue } : {}),
530
+ ...(!fusedHtml && responsiveValue ? { responsiveQuality: responsiveValue } : {}),
277
531
  ...(exportHtml ? { exportHtml } : {}),
278
532
  };
279
- const detach = booleanFlag(flags, 'detach');
280
533
  const quiet = booleanFlag(flags, 'quiet');
281
534
  // Both destinations are made writable BEFORE the conversion is bought. The
282
535
  // paid call used to move first, so a missing parent directory lost the
283
536
  // settled document, its export, and the journal line that recorded the
284
537
  // purchase. See cli-output-paths.ts.
285
538
  //
286
- // `--out` accepts a directory, and when it names one the export lands beside
287
- // the document under a derived name. That is the whole point: one fused call
288
- // then carries ONE output path instead of two that disagreed on their type.
289
- const outValue = stringFlag(flags, 'out');
290
- const outFile = outValue
291
- ? await prepareOutputTarget(outValue, () => path.basename(defaultConversionOutputPath(inputPath, output)))
292
- : await prepareOutputFile(defaultConversionOutputPath(inputPath, output));
539
+ // Convert has one directory spelling. Filenames are always derived from the
540
+ // input, so fused output cannot split a file flag and a directory flag again.
541
+ const outDirectory = stringFlag(flags, 'out-dir')
542
+ ? await prepareOutputDirectory(stringFlag(flags, 'out-dir'))
543
+ : path.dirname(inputPath);
544
+ const outFile = await prepareOutputFile(path.join(outDirectory, path.basename(defaultConversionOutputPath(inputPath, output))));
293
545
  const exportOutFile = exportHtml
294
- ? await prepareOutputFile(exportOutValue
295
- ?? path.join(path.dirname(outFile), path.basename(defaultConversionOutputPath(inputPath, 'html'))))
546
+ ? await prepareOutputFile(path.join(outDirectory, path.basename(defaultConversionOutputPath(inputPath, 'html'))))
296
547
  : null;
297
548
  // Everything below happens BEFORE the paid dispatch, in the workspace the
298
549
  // artifacts land in — which is where this command's journal lives, so a
299
550
  // second shell reading it is reading the same file.
300
551
  const workspace = path.dirname(outFile);
301
- const typicalSeconds = CONVERSION_DURATION_HINTS[exportHtml ? 'fused' : 'convert'];
552
+ // A fused run stays on the fused median even at --model fast: its wall is
553
+ // anchor-dominated, not extraction-dominated (measured 1m53-5m6 fast).
554
+ const typicalSeconds = exportHtml
555
+ ? CONVERSION_DURATION_HINTS.fused
556
+ : model === 'fast'
557
+ ? CONVERSION_DURATION_HINTS.convertFast
558
+ : CONVERSION_DURATION_HINTS.convert;
302
559
  const sourceSha256 = createHash('sha256').update(inputBytes).digest('hex');
303
560
  journal.note({
304
561
  sourceSha256,
@@ -321,52 +578,75 @@ export const runConversionCommand = async (command, flags, journal) => {
321
578
  elapsedSeconds: (Date.now() - commandStartedAt) / 1000,
322
579
  typicalSeconds,
323
580
  });
324
- const client = conversionClient();
325
- const created = await client.createConversion({
326
- ...conversionInput,
327
- ...(idempotencyKey ? { idempotencyKey } : {}),
328
- waitSeconds: detach ? 0 : 20,
581
+ const progress = createLongRunningProgress({
582
+ label: exportHtml ? 'convert (image + responsive HTML)' : 'convert',
583
+ typicalSeconds,
584
+ quiet,
585
+ heartbeat,
586
+ });
587
+ progress.brief({
588
+ request: `convert ${path.basename(inputPath)} -> ${exportHtml
589
+ ? 'layerdoc + responsive html (one fused purchase)'
590
+ : requestedOutput}`,
591
+ paid: input.layerdoc !== undefined
592
+ ? 'one derived conversion from the LayerDoc on disk'
593
+ : `1 ${model ?? 'standard'} conversion${exportHtml ? ' + 1 responsive export' : ''}`,
594
+ done: exportOutFile ? `${outFile} + ${exportOutFile}` : outFile,
595
+ expectation: {
596
+ typicalSeconds,
597
+ basis: `measured median, ${model ?? 'standard'} model${exportHtml ? ', fused' : ''}`,
598
+ },
599
+ blocking: true,
329
600
  });
601
+ progress.update({ stage: 'dispatching' });
602
+ const client = conversionClient();
603
+ let created;
604
+ try {
605
+ created = await client.createConversion({
606
+ ...conversionInput,
607
+ ...(idempotencyKey ? { idempotencyKey } : {}),
608
+ waitSeconds: 20,
609
+ });
610
+ }
611
+ catch (error) {
612
+ progress.finish('failed', {
613
+ detail: `the paid dispatch itself failed: ${error instanceof Error ? error.message : String(error)}`,
614
+ });
615
+ throw error;
616
+ }
330
617
  journal.note({
331
618
  runId: created.id,
332
619
  purchased: [created.id, ...(created.export?.conversion_id ? [created.export.conversion_id] : [])],
333
620
  });
334
- if (detach) {
335
- const resumeCommand = `12ui resume ${created.id} --output ${output} --out <file>`;
336
- console.log(JSON.stringify({
337
- id: created.id,
338
- status: created.status,
339
- output: created.output,
340
- ...(created.export ? { export: created.export } : {}),
341
- expiresAt: created.expires_at,
342
- // A detached operation is the one case with nothing streaming, so the
343
- // response has to say how long it takes and how to collect it.
344
- nextAction: nextActionLine({
345
- what: 'A conversion',
346
- typicalSeconds: CONVERSION_DURATION_HINTS[created.export ? 'fused' : 'convert'],
347
- statusCommand: resumeCommand,
348
- waitHint: 'which blocks until it finishes and writes the result',
349
- }),
350
- }, null, 2));
351
- return;
352
- }
353
621
  const exportOperationId = created.export?.conversion_id ?? null;
354
- if (!quiet) {
355
- process.stderr.write(`12ui convert ${created.id}${exportOperationId ? ` (+ export ${exportOperationId})` : ''}`
356
- + ` — ${durationHint(exportOperationId ? 'fused' : 'convert')}\n`
357
- + `${nextActionLine({
358
- what: 'A conversion',
359
- typicalSeconds: CONVERSION_DURATION_HINTS[exportOperationId ? 'fused' : 'convert'],
360
- statusCommand: `12ui resume ${created.id} --output ${output} --out <file>`,
361
- waitHint: 'or leave this command running, which is what --wait already does by default',
362
- })}\n`);
363
- }
364
- const reporter = createProgressReporter({
365
- label: 'convert',
366
- quiet,
367
- typicalSeconds: CONVERSION_DURATION_HINTS[exportOperationId ? 'fused' : 'convert'],
368
- heartbeat,
622
+ const recovery = {
623
+ version: 1,
624
+ createdAt: created.created_at,
625
+ expiresAt: created.expires_at,
626
+ conversion: { conversionId: created.id, output, outFile },
627
+ ...(exportOperationId && exportOutFile
628
+ ? {
629
+ linkedExport: {
630
+ conversionId: exportOperationId,
631
+ output: 'html',
632
+ outFile: exportOutFile,
633
+ },
634
+ }
635
+ : {}),
636
+ };
637
+ await writeConversionRecovery(recovery).catch((error) => {
638
+ if (!quiet) {
639
+ process.stderr.write(`Could not record bare-resume recovery for ${created.id}:`
640
+ + ` ${error instanceof Error ? error.message : String(error)}.`
641
+ + ` If interrupted, use \`12ui resume ${created.id} --output ${output} --out ${outFile}\`.\n`);
642
+ }
369
643
  });
644
+ // Identity is said once, here, and again in the terminal block — never on
645
+ // ticks. The board's fused capture repeated both ids on 11 of 13 lines.
646
+ progress.note(`claimed conversion ${created.id}${exportOperationId
647
+ ? `; its linked responsive export is ${exportOperationId}`
648
+ : ''}`);
649
+ reportConversion(progress, created);
370
650
  let operation;
371
651
  try {
372
652
  operation = created.status === 'succeeded'
@@ -374,14 +654,26 @@ export const runConversionCommand = async (command, flags, journal) => {
374
654
  : await client.waitForConversion(created.id, {
375
655
  expiresAt: created.expires_at,
376
656
  timeoutMs,
377
- onProgress: reporter.onProgress,
657
+ onProgress: (current) => { reportConversion(progress, current); },
378
658
  });
379
659
  }
380
660
  catch (error) {
381
661
  const detail = error instanceof Error ? error.message : String(error);
662
+ progress.finish('stopped', {
663
+ detail: `conversion ${created.id} did not settle: ${detail}`,
664
+ ...(exportOperationId && exportOutFile
665
+ ? {
666
+ artifacts: [
667
+ { name: output, state: 'not collected' },
668
+ { name: 'responsive html', state: 'unknown — the linked export was not read' },
669
+ ],
670
+ next: `12ui resume ${created.id} recovers both halves without re-buying`,
671
+ }
672
+ : { next: `12ui resume ${created.id} recovers the purchase without re-buying` }),
673
+ });
382
674
  throw new Error(`Conversion ${created.id} failed: ${detail}`, { cause: error });
383
675
  }
384
- reporter.done(operation);
676
+ reportConversion(progress, operation);
385
677
  // The proof that pairs these exact bytes with the conversion just bought, so
386
678
  // a later `12ui branch execute` from the same image attaches it instead of
387
679
  // buying viewport 1 of its source page a second time. It goes to the per-user
@@ -399,6 +691,20 @@ export const runConversionCommand = async (command, flags, journal) => {
399
691
  journal.note({ artifacts: [outFile] });
400
692
  if (!exportOperationId || !exportOutFile) {
401
693
  // No export ran, so a `layerdoc` result here IS a document with no page.
694
+ progress.finish('succeeded', {
695
+ artifacts: [{
696
+ name: output,
697
+ state: 'succeeded',
698
+ path: outFile,
699
+ ...(result.artifactManifest.artifactCount > 0
700
+ ? { note: `${result.artifactManifest.artifactCount} assets, manifest ${result.artifactManifest.manifestFile}` }
701
+ : {}),
702
+ }],
703
+ spend: input.layerdoc !== undefined
704
+ ? 'one derived conversion purchased'
705
+ : `1 ${model ?? 'standard'} conversion purchased`,
706
+ next: `free replay: 12ui resume ${created.id}`,
707
+ });
402
708
  console.log(conversionResultJson(result));
403
709
  return;
404
710
  }
@@ -406,21 +712,28 @@ export const runConversionCommand = async (command, flags, journal) => {
406
712
  // running after the budget must not take it back: the two halves are
407
713
  // separate operations and the conversion is the authoritative answer for its
408
714
  // own half.
409
- const exportReporter = createProgressReporter({
410
- label: 'export',
411
- quiet,
715
+ //
716
+ // The handoff is said in words: the fused purchase's second half has its
717
+ // own stages and its own percent starting at 0. Rendering it silently into
718
+ // the same line was the board's 99% -> 29% whiplash.
719
+ progress.beginScope({
720
+ name: 'export',
412
721
  typicalSeconds: CONVERSION_DURATION_HINTS.export,
413
- heartbeat,
722
+ sentence: 'conversion finished and its document is on disk; now the linked responsive'
723
+ + ' export (second half of this purchase) — it ran alongside the conversion, its'
724
+ + ' own percent starts at 0, and it usually finishes well inside the ~4m a'
725
+ + ' standalone export measures',
414
726
  });
415
727
  let exportResult = null;
728
+ let exportOperation = null;
416
729
  let exportError = null;
417
730
  try {
418
- const exportOperation = await client.waitForConversion(exportOperationId, {
731
+ exportOperation = await client.waitForConversion(exportOperationId, {
419
732
  expiresAt: created.expires_at,
420
733
  timeoutMs,
421
- onProgress: exportReporter.onProgress,
734
+ onProgress: (current) => { reportConversion(progress, current); },
422
735
  });
423
- exportReporter.done(exportOperation);
736
+ reportConversion(progress, exportOperation);
424
737
  exportResult = await downloadCompletedConversion({
425
738
  client,
426
739
  operation: exportOperation,
@@ -440,8 +753,16 @@ export const runConversionCommand = async (command, flags, journal) => {
440
753
  : await readConversionStatus(client, exportOperationId);
441
754
  const pending = isPendingConversionStatus(exportStatus);
442
755
  const collect = `12ui resume ${exportOperationId} --output html --out <file>`;
443
- console.log(JSON.stringify({
756
+ const report = {
444
757
  ...result,
758
+ nextAction: layerDocStateAction({
759
+ outFile,
760
+ linkedExport: {
761
+ id: exportOperationId,
762
+ status: exportResult ? 'succeeded' : exportStatus,
763
+ ...(exportResult ? { outFile: exportOutFile } : {}),
764
+ },
765
+ }),
445
766
  export: exportResult
446
767
  ? { ...exportResult, status: 'succeeded' }
447
768
  : {
@@ -450,7 +771,56 @@ export const runConversionCommand = async (command, flags, journal) => {
450
771
  status: exportStatus,
451
772
  ...(pending ? { nextAction: collect } : { error: exportError }),
452
773
  },
453
- }, null, 2));
774
+ };
775
+ const layerdocRow = {
776
+ name: 'layerdoc',
777
+ state: 'succeeded',
778
+ path: outFile,
779
+ };
780
+ const spendLine = `1 ${model ?? 'standard'} conversion + 1 responsive export purchased`;
781
+ if (exportResult) {
782
+ progress.finish('succeeded', {
783
+ artifacts: [
784
+ layerdocRow,
785
+ { name: 'responsive html', state: 'succeeded', path: exportOutFile },
786
+ ],
787
+ spend: spendLine,
788
+ next: `free replay: 12ui resume ${created.id}`,
789
+ });
790
+ }
791
+ else if (pending) {
792
+ // Terminal for this command, NOT for the purchase: the block must not
793
+ // look final while a paid format is live server-side.
794
+ progress.finish('deadline', {
795
+ detail: 'client wait ended; the paid export continues server-side',
796
+ artifacts: [
797
+ layerdocRow,
798
+ {
799
+ name: 'responsive html',
800
+ state: `still running server-side (${exportStatus})`,
801
+ note: `collect free with: ${collect}`,
802
+ },
803
+ ],
804
+ spend: spendLine,
805
+ next: `the purchase stays collectable until ${created.expires_at}`,
806
+ });
807
+ }
808
+ else {
809
+ progress.finish('failed', {
810
+ detail: `the linked responsive export ${exportOperationId} ${exportStatus}`,
811
+ artifacts: [
812
+ layerdocRow,
813
+ {
814
+ name: 'responsive html',
815
+ state: exportStatus,
816
+ ...(exportError ? { note: exportError } : {}),
817
+ },
818
+ ],
819
+ spend: spendLine,
820
+ next: `render a fixed page free from the document: 12ui convert ${outFile} --output html_fixed`,
821
+ });
822
+ }
823
+ writeResultWithSummary(report);
454
824
  if (!exportResult) {
455
825
  throw new Error(pending
456
826
  ? `Conversion ${created.id} succeeded and was written to ${outFile}. Its responsive HTML`