primitive-admin 1.1.0-alpha.9 → 1.1.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 (591) hide show
  1. package/README.md +523 -109
  2. package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
  3. package/dist/bin/primitive.d.ts +2 -0
  4. package/dist/bin/primitive.js +372 -21
  5. package/dist/bin/primitive.js.map +1 -1
  6. package/dist/src/commands/admins.d.ts +2 -0
  7. package/dist/src/commands/admins.js +136 -30
  8. package/dist/src/commands/admins.js.map +1 -1
  9. package/dist/src/commands/analytics.d.ts +2 -0
  10. package/dist/src/commands/analytics.js +713 -55
  11. package/dist/src/commands/analytics.js.map +1 -1
  12. package/dist/src/commands/apps.d.ts +2 -0
  13. package/dist/src/commands/apps.js +65 -100
  14. package/dist/src/commands/apps.js.map +1 -1
  15. package/dist/src/commands/auth-sessions.d.ts +7 -0
  16. package/dist/src/commands/auth-sessions.js +144 -0
  17. package/dist/src/commands/auth-sessions.js.map +1 -0
  18. package/dist/src/commands/auth.d.ts +2 -0
  19. package/dist/src/commands/auth.js +238 -108
  20. package/dist/src/commands/auth.js.map +1 -1
  21. package/dist/src/commands/blob-buckets.d.ts +2 -0
  22. package/dist/src/commands/blob-buckets.js +331 -0
  23. package/dist/src/commands/blob-buckets.js.map +1 -0
  24. package/dist/src/commands/catalog.d.ts +2 -0
  25. package/dist/src/commands/catalog.js +63 -48
  26. package/dist/src/commands/catalog.js.map +1 -1
  27. package/dist/src/commands/collection-type-configs.d.ts +2 -0
  28. package/dist/src/commands/collection-type-configs.js +85 -0
  29. package/dist/src/commands/collection-type-configs.js.map +1 -0
  30. package/dist/src/commands/collections.d.ts +2 -0
  31. package/dist/src/commands/collections.js +1282 -0
  32. package/dist/src/commands/collections.js.map +1 -0
  33. package/dist/src/commands/comparisons.d.ts +2 -0
  34. package/dist/src/commands/comparisons.js +6 -6
  35. package/dist/src/commands/comparisons.js.map +1 -1
  36. package/dist/src/commands/config.d.ts +46 -0
  37. package/dist/src/commands/config.js +465 -0
  38. package/dist/src/commands/config.js.map +1 -0
  39. package/dist/src/commands/connections.d.ts +2 -0
  40. package/dist/src/commands/connections.js +99 -0
  41. package/dist/src/commands/connections.js.map +1 -0
  42. package/dist/src/commands/cron-triggers.d.ts +2 -0
  43. package/dist/src/commands/cron-triggers.js +266 -0
  44. package/dist/src/commands/cron-triggers.js.map +1 -0
  45. package/dist/src/commands/database-type-configs.d.ts +2 -0
  46. package/dist/src/commands/database-type-configs.js +164 -0
  47. package/dist/src/commands/database-type-configs.js.map +1 -0
  48. package/dist/src/commands/database-types.d.ts +2 -0
  49. package/dist/src/commands/database-types.js +471 -0
  50. package/dist/src/commands/database-types.js.map +1 -0
  51. package/dist/src/commands/databases.d.ts +65 -0
  52. package/dist/src/commands/databases.js +1887 -243
  53. package/dist/src/commands/databases.js.map +1 -1
  54. package/dist/src/commands/documents.d.ts +60 -0
  55. package/dist/src/commands/documents.js +2888 -22
  56. package/dist/src/commands/documents.js.map +1 -1
  57. package/dist/src/commands/email-templates.d.ts +2 -0
  58. package/dist/src/commands/email-templates.js +175 -0
  59. package/dist/src/commands/email-templates.js.map +1 -0
  60. package/dist/src/commands/env.d.ts +23 -0
  61. package/dist/src/commands/env.js +395 -0
  62. package/dist/src/commands/env.js.map +1 -0
  63. package/dist/src/commands/feature-flags.d.ts +14 -0
  64. package/dist/src/commands/feature-flags.js +117 -0
  65. package/dist/src/commands/feature-flags.js.map +1 -0
  66. package/dist/src/commands/functions.d.ts +20 -0
  67. package/dist/src/commands/functions.js +1630 -0
  68. package/dist/src/commands/functions.js.map +1 -0
  69. package/dist/src/commands/group-type-configs.d.ts +2 -0
  70. package/dist/src/commands/group-type-configs.js +87 -0
  71. package/dist/src/commands/group-type-configs.js.map +1 -0
  72. package/dist/src/commands/groups.d.ts +2 -0
  73. package/dist/src/commands/groups.js +65 -113
  74. package/dist/src/commands/groups.js.map +1 -1
  75. package/dist/src/commands/guides.d.ts +223 -0
  76. package/dist/src/commands/guides.js +627 -69
  77. package/dist/src/commands/guides.js.map +1 -1
  78. package/dist/src/commands/init.d.ts +37 -0
  79. package/dist/src/commands/init.js +1693 -208
  80. package/dist/src/commands/init.js.map +1 -1
  81. package/dist/src/commands/integrations.d.ts +2 -0
  82. package/dist/src/commands/integrations.js +428 -187
  83. package/dist/src/commands/integrations.js.map +1 -1
  84. package/dist/src/commands/llm.d.ts +2 -0
  85. package/dist/src/commands/llm.js +4 -2
  86. package/dist/src/commands/llm.js.map +1 -1
  87. package/dist/src/commands/locks.d.ts +8 -0
  88. package/dist/src/commands/locks.js +175 -0
  89. package/dist/src/commands/locks.js.map +1 -0
  90. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  91. package/dist/src/commands/metadata-category-configs.js +113 -0
  92. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  93. package/dist/src/commands/metadata.d.ts +2 -0
  94. package/dist/src/commands/metadata.js +288 -0
  95. package/dist/src/commands/metadata.js.map +1 -0
  96. package/dist/src/commands/prompts.d.ts +2 -0
  97. package/dist/src/commands/prompts.js +322 -634
  98. package/dist/src/commands/prompts.js.map +1 -1
  99. package/dist/src/commands/rule-sets.d.ts +3 -0
  100. package/dist/src/commands/rule-sets.js +139 -148
  101. package/dist/src/commands/rule-sets.js.map +1 -1
  102. package/dist/src/commands/scripts.d.ts +30 -0
  103. package/dist/src/commands/scripts.js +688 -0
  104. package/dist/src/commands/scripts.js.map +1 -0
  105. package/dist/src/commands/secrets.d.ts +2 -0
  106. package/dist/src/commands/secrets.js +109 -0
  107. package/dist/src/commands/secrets.js.map +1 -0
  108. package/dist/src/commands/sessions.d.ts +2 -0
  109. package/dist/src/commands/sessions.js +76 -0
  110. package/dist/src/commands/sessions.js.map +1 -0
  111. package/dist/src/commands/skill.d.ts +2 -0
  112. package/dist/src/commands/skill.js +29 -0
  113. package/dist/src/commands/skill.js.map +1 -0
  114. package/dist/src/commands/sync-app-settings.d.ts +158 -0
  115. package/dist/src/commands/sync-app-settings.js +328 -0
  116. package/dist/src/commands/sync-app-settings.js.map +1 -0
  117. package/dist/src/commands/sync.d.ts +2670 -0
  118. package/dist/src/commands/sync.js +17144 -834
  119. package/dist/src/commands/sync.js.map +1 -1
  120. package/dist/src/commands/tokens.d.ts +2 -0
  121. package/dist/src/commands/tokens.js +132 -22
  122. package/dist/src/commands/tokens.js.map +1 -1
  123. package/dist/src/commands/users.d.ts +2 -0
  124. package/dist/src/commands/users.js +542 -24
  125. package/dist/src/commands/users.js.map +1 -1
  126. package/dist/src/commands/vars.d.ts +8 -0
  127. package/dist/src/commands/vars.js +97 -0
  128. package/dist/src/commands/vars.js.map +1 -0
  129. package/dist/src/commands/waitlist.d.ts +2 -0
  130. package/dist/src/commands/waitlist.js +12 -11
  131. package/dist/src/commands/waitlist.js.map +1 -1
  132. package/dist/src/commands/webhooks.d.ts +31 -0
  133. package/dist/src/commands/webhooks.js +633 -0
  134. package/dist/src/commands/webhooks.js.map +1 -0
  135. package/dist/src/commands/workflows.d.ts +88 -0
  136. package/dist/src/commands/workflows.js +1568 -742
  137. package/dist/src/commands/workflows.js.map +1 -1
  138. package/dist/src/lib/access-rule-display.d.ts +21 -0
  139. package/dist/src/lib/access-rule-display.js +34 -0
  140. package/dist/src/lib/access-rule-display.js.map +1 -0
  141. package/dist/src/lib/api-client.d.ts +2550 -0
  142. package/dist/src/lib/api-client.js +2503 -160
  143. package/dist/src/lib/api-client.js.map +1 -1
  144. package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
  145. package/dist/src/lib/app-settings-descriptor.js +583 -0
  146. package/dist/src/lib/app-settings-descriptor.js.map +1 -0
  147. package/dist/src/lib/auth-flow.d.ts +8 -0
  148. package/dist/src/lib/batch.d.ts +26 -0
  149. package/dist/src/lib/batch.js +32 -0
  150. package/dist/src/lib/batch.js.map +1 -0
  151. package/dist/src/lib/block-layout.d.ts +160 -0
  152. package/dist/src/lib/block-layout.js +451 -0
  153. package/dist/src/lib/block-layout.js.map +1 -0
  154. package/dist/src/lib/block-selector.d.ts +58 -0
  155. package/dist/src/lib/block-selector.js +92 -0
  156. package/dist/src/lib/block-selector.js.map +1 -0
  157. package/dist/src/lib/canonical-json.d.ts +12 -0
  158. package/dist/src/lib/canonical-json.js +35 -0
  159. package/dist/src/lib/canonical-json.js.map +1 -0
  160. package/dist/src/lib/channel.d.ts +30 -0
  161. package/dist/src/lib/channel.js +68 -0
  162. package/dist/src/lib/channel.js.map +1 -0
  163. package/dist/src/lib/cli-manifest.d.ts +68 -0
  164. package/dist/src/lib/cli-manifest.js +71 -0
  165. package/dist/src/lib/cli-manifest.js.map +1 -0
  166. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
  167. package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
  168. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
  169. package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
  170. package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
  171. package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
  172. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
  173. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
  174. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
  175. package/dist/src/lib/collection-export.d.ts +184 -0
  176. package/dist/src/lib/collection-export.js +252 -0
  177. package/dist/src/lib/collection-export.js.map +1 -0
  178. package/dist/src/lib/config-json-field.d.ts +28 -0
  179. package/dist/src/lib/config-json-field.js +56 -0
  180. package/dist/src/lib/config-json-field.js.map +1 -0
  181. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  182. package/dist/src/lib/config-object-descriptor.js +740 -0
  183. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  184. package/dist/src/lib/config-payload.d.ts +92 -0
  185. package/dist/src/lib/config-payload.js +161 -0
  186. package/dist/src/lib/config-payload.js.map +1 -0
  187. package/dist/src/lib/config-surface.d.ts +141 -0
  188. package/dist/src/lib/config-surface.js +368 -0
  189. package/dist/src/lib/config-surface.js.map +1 -0
  190. package/dist/src/lib/config-toml.d.ts +10 -0
  191. package/dist/src/lib/config-toml.js +42 -0
  192. package/dist/src/lib/config-toml.js.map +1 -0
  193. package/dist/src/lib/config.d.ts +71 -0
  194. package/dist/src/lib/config.js +71 -68
  195. package/dist/src/lib/config.js.map +1 -1
  196. package/dist/src/lib/confirm-prompt.d.ts +83 -0
  197. package/dist/src/lib/confirm-prompt.js +110 -0
  198. package/dist/src/lib/confirm-prompt.js.map +1 -0
  199. package/dist/src/lib/constants.d.ts +11 -0
  200. package/dist/src/lib/constants.js +12 -0
  201. package/dist/src/lib/constants.js.map +1 -0
  202. package/dist/src/lib/crash-handlers.d.ts +20 -0
  203. package/dist/src/lib/crash-handlers.js +49 -0
  204. package/dist/src/lib/crash-handlers.js.map +1 -0
  205. package/dist/src/lib/credentials-store.d.ts +104 -0
  206. package/dist/src/lib/credentials-store.js +336 -0
  207. package/dist/src/lib/credentials-store.js.map +1 -0
  208. package/dist/src/lib/csv.d.ts +47 -0
  209. package/dist/src/lib/csv.js +172 -0
  210. package/dist/src/lib/csv.js.map +1 -0
  211. package/dist/src/lib/data-input.d.ts +23 -0
  212. package/dist/src/lib/data-input.js +50 -0
  213. package/dist/src/lib/data-input.js.map +1 -0
  214. package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
  215. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  216. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  217. package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
  218. package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
  219. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  220. package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
  221. package/dist/src/lib/db-codegen/dbNaming.js +180 -0
  222. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  223. package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
  224. package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
  225. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  226. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
  227. package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
  228. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  229. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
  230. package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
  231. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
  232. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
  233. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
  234. package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
  235. package/dist/src/lib/deprecation.d.ts +22 -0
  236. package/dist/src/lib/deprecation.js +43 -0
  237. package/dist/src/lib/deprecation.js.map +1 -0
  238. package/dist/src/lib/document-export-permissions.d.ts +30 -0
  239. package/dist/src/lib/document-export-permissions.js +54 -0
  240. package/dist/src/lib/document-export-permissions.js.map +1 -0
  241. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  242. package/dist/src/lib/document-ingest-artifact.js +505 -0
  243. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  244. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  245. package/dist/src/lib/document-ingest-input.js +132 -0
  246. package/dist/src/lib/document-ingest-input.js.map +1 -0
  247. package/dist/src/lib/document-ingest-rows.d.ts +137 -0
  248. package/dist/src/lib/document-ingest-rows.js +181 -0
  249. package/dist/src/lib/document-ingest-rows.js.map +1 -0
  250. package/dist/src/lib/document-ingest.d.ts +101 -0
  251. package/dist/src/lib/document-ingest.js +384 -0
  252. package/dist/src/lib/document-ingest.js.map +1 -0
  253. package/dist/src/lib/env-resolver-core.d.ts +258 -0
  254. package/dist/src/lib/env-resolver-core.js +447 -0
  255. package/dist/src/lib/env-resolver-core.js.map +1 -0
  256. package/dist/src/lib/env-resolver.d.ts +99 -0
  257. package/dist/src/lib/env-resolver.js +153 -0
  258. package/dist/src/lib/env-resolver.js.map +1 -0
  259. package/dist/src/lib/fetch.d.ts +5 -0
  260. package/dist/src/lib/function-bundle.d.ts +147 -0
  261. package/dist/src/lib/function-bundle.js +341 -0
  262. package/dist/src/lib/function-bundle.js.map +1 -0
  263. package/dist/src/lib/function-collect.d.ts +123 -0
  264. package/dist/src/lib/function-collect.js +610 -0
  265. package/dist/src/lib/function-collect.js.map +1 -0
  266. package/dist/src/lib/function-db-types.d.ts +202 -0
  267. package/dist/src/lib/function-db-types.js +870 -0
  268. package/dist/src/lib/function-db-types.js.map +1 -0
  269. package/dist/src/lib/function-document-types.d.ts +144 -0
  270. package/dist/src/lib/function-document-types.js +370 -0
  271. package/dist/src/lib/function-document-types.js.map +1 -0
  272. package/dist/src/lib/function-grants-preflight.d.ts +64 -0
  273. package/dist/src/lib/function-grants-preflight.js +105 -0
  274. package/dist/src/lib/function-grants-preflight.js.map +1 -0
  275. package/dist/src/lib/function-log-lines.d.ts +76 -0
  276. package/dist/src/lib/function-log-lines.js +160 -0
  277. package/dist/src/lib/function-log-lines.js.map +1 -0
  278. package/dist/src/lib/function-log-row.d.ts +29 -0
  279. package/dist/src/lib/function-log-row.js +73 -0
  280. package/dist/src/lib/function-log-row.js.map +1 -0
  281. package/dist/src/lib/function-log-tail.d.ts +132 -0
  282. package/dist/src/lib/function-log-tail.js +262 -0
  283. package/dist/src/lib/function-log-tail.js.map +1 -0
  284. package/dist/src/lib/function-run.d.ts +289 -0
  285. package/dist/src/lib/function-run.js +389 -0
  286. package/dist/src/lib/function-run.js.map +1 -0
  287. package/dist/src/lib/function-schema-codegen.d.ts +143 -0
  288. package/dist/src/lib/function-schema-codegen.js +420 -0
  289. package/dist/src/lib/function-schema-codegen.js.map +1 -0
  290. package/dist/src/lib/function-sync.d.ts +459 -0
  291. package/dist/src/lib/function-sync.js +1258 -0
  292. package/dist/src/lib/function-sync.js.map +1 -0
  293. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  294. package/dist/src/lib/function-trigger-listing.js +70 -0
  295. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  296. package/dist/src/lib/function-typecheck.d.ts +86 -0
  297. package/dist/src/lib/function-typecheck.js +370 -0
  298. package/dist/src/lib/function-typecheck.js.map +1 -0
  299. package/dist/src/lib/function-versions.d.ts +122 -0
  300. package/dist/src/lib/function-versions.js +182 -0
  301. package/dist/src/lib/function-versions.js.map +1 -0
  302. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  303. package/dist/src/lib/generated-allowlist.js +281 -0
  304. package/dist/src/lib/generated-allowlist.js.map +1 -0
  305. package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
  306. package/dist/src/lib/generated-config-surfaces.js +10569 -0
  307. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  308. package/dist/src/lib/generated-sdk-types.d.ts +12 -0
  309. package/dist/src/lib/generated-sdk-types.js +13 -0
  310. package/dist/src/lib/generated-sdk-types.js.map +1 -0
  311. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  312. package/dist/src/lib/generated-template-lint.js +624 -0
  313. package/dist/src/lib/generated-template-lint.js.map +1 -0
  314. package/dist/src/lib/init-adopt.d.ts +16 -0
  315. package/dist/src/lib/init-adopt.js +34 -0
  316. package/dist/src/lib/init-adopt.js.map +1 -0
  317. package/dist/src/lib/init-assets.d.ts +39 -0
  318. package/dist/src/lib/init-assets.js +97 -0
  319. package/dist/src/lib/init-assets.js.map +1 -0
  320. package/dist/src/lib/init-client-platforms.d.ts +14 -0
  321. package/dist/src/lib/init-client-platforms.js +71 -0
  322. package/dist/src/lib/init-client-platforms.js.map +1 -0
  323. package/dist/src/lib/init-config.d.ts +98 -0
  324. package/dist/src/lib/init-config.js +186 -0
  325. package/dist/src/lib/init-config.js.map +1 -0
  326. package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
  327. package/dist/src/lib/init-email-redirect-uris.js +46 -0
  328. package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
  329. package/dist/src/lib/init-ios-links.d.ts +91 -0
  330. package/dist/src/lib/init-ios-links.js +219 -0
  331. package/dist/src/lib/init-ios-links.js.map +1 -0
  332. package/dist/src/lib/init-plan.d.ts +80 -0
  333. package/dist/src/lib/init-plan.js +95 -0
  334. package/dist/src/lib/init-plan.js.map +1 -0
  335. package/dist/src/lib/init-production-env.d.ts +48 -0
  336. package/dist/src/lib/init-production-env.js +59 -0
  337. package/dist/src/lib/init-production-env.js.map +1 -0
  338. package/dist/src/lib/init-schema.d.ts +74 -0
  339. package/dist/src/lib/init-schema.js +358 -0
  340. package/dist/src/lib/init-schema.js.map +1 -0
  341. package/dist/src/lib/init-xcode.d.ts +34 -0
  342. package/dist/src/lib/init-xcode.js +138 -0
  343. package/dist/src/lib/init-xcode.js.map +1 -0
  344. package/dist/src/lib/integration-request-config.d.ts +30 -0
  345. package/dist/src/lib/integration-request-config.js +145 -0
  346. package/dist/src/lib/integration-request-config.js.map +1 -0
  347. package/dist/src/lib/integration-selector.d.ts +42 -0
  348. package/dist/src/lib/integration-selector.js +46 -0
  349. package/dist/src/lib/integration-selector.js.map +1 -0
  350. package/dist/src/lib/ios-app-id.d.ts +34 -0
  351. package/dist/src/lib/ios-app-id.js +69 -0
  352. package/dist/src/lib/ios-app-id.js.map +1 -0
  353. package/dist/src/lib/list-options.d.ts +68 -0
  354. package/dist/src/lib/list-options.js +89 -0
  355. package/dist/src/lib/list-options.js.map +1 -0
  356. package/dist/src/lib/local-state.d.ts +55 -0
  357. package/dist/src/lib/local-state.js +167 -0
  358. package/dist/src/lib/local-state.js.map +1 -0
  359. package/dist/src/lib/local-test-cases.d.ts +63 -0
  360. package/dist/src/lib/local-test-cases.js +136 -0
  361. package/dist/src/lib/local-test-cases.js.map +1 -0
  362. package/dist/src/lib/log-inspection.d.ts +715 -0
  363. package/dist/src/lib/log-inspection.js +816 -0
  364. package/dist/src/lib/log-inspection.js.map +1 -0
  365. package/dist/src/lib/logout-admin-session.d.ts +33 -0
  366. package/dist/src/lib/logout-admin-session.js +70 -0
  367. package/dist/src/lib/logout-admin-session.js.map +1 -0
  368. package/dist/src/lib/migration-nag.d.ts +49 -0
  369. package/dist/src/lib/migration-nag.js +163 -0
  370. package/dist/src/lib/migration-nag.js.map +1 -0
  371. package/dist/src/lib/object-status-filter.d.ts +22 -0
  372. package/dist/src/lib/object-status-filter.js +45 -0
  373. package/dist/src/lib/object-status-filter.js.map +1 -0
  374. package/dist/src/lib/output.d.ts +124 -0
  375. package/dist/src/lib/output.js +219 -8
  376. package/dist/src/lib/output.js.map +1 -1
  377. package/dist/src/lib/package-manager.d.ts +140 -0
  378. package/dist/src/lib/package-manager.js +305 -0
  379. package/dist/src/lib/package-manager.js.map +1 -0
  380. package/dist/src/lib/paginate.d.ts +98 -0
  381. package/dist/src/lib/paginate.js +112 -0
  382. package/dist/src/lib/paginate.js.map +1 -0
  383. package/dist/src/lib/platform-owned.d.ts +63 -0
  384. package/dist/src/lib/platform-owned.js +85 -0
  385. package/dist/src/lib/platform-owned.js.map +1 -0
  386. package/dist/src/lib/project-config.d.ts +122 -0
  387. package/dist/src/lib/project-config.js +244 -0
  388. package/dist/src/lib/project-config.js.map +1 -0
  389. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  390. package/dist/src/lib/prompt-cost-format.js +41 -0
  391. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  392. package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
  393. package/dist/src/lib/prompt-schema-codegen.js +462 -0
  394. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  395. package/dist/src/lib/query-operators.d.ts +43 -0
  396. package/dist/src/lib/query-operators.js +80 -0
  397. package/dist/src/lib/query-operators.js.map +1 -0
  398. package/dist/src/lib/record-filter.d.ts +18 -0
  399. package/dist/src/lib/record-filter.js +55 -0
  400. package/dist/src/lib/record-filter.js.map +1 -0
  401. package/dist/src/lib/refresh-admin-credentials.d.ts +73 -0
  402. package/dist/src/lib/refresh-admin-credentials.js +123 -0
  403. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  404. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  405. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  406. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  407. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  408. package/dist/src/lib/resolve-init-server.js +77 -0
  409. package/dist/src/lib/resolve-init-server.js.map +1 -0
  410. package/dist/src/lib/resolve-owner.d.ts +19 -0
  411. package/dist/src/lib/resolve-owner.js +20 -0
  412. package/dist/src/lib/resolve-owner.js.map +1 -0
  413. package/dist/src/lib/resolve-platform.d.ts +74 -0
  414. package/dist/src/lib/resolve-platform.js +105 -0
  415. package/dist/src/lib/resolve-platform.js.map +1 -0
  416. package/dist/src/lib/run-status.d.ts +19 -0
  417. package/dist/src/lib/run-status.generated.d.ts +39 -0
  418. package/dist/src/lib/run-status.generated.js +66 -0
  419. package/dist/src/lib/run-status.generated.js.map +1 -0
  420. package/dist/src/lib/run-status.js +19 -0
  421. package/dist/src/lib/run-status.js.map +1 -0
  422. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  423. package/dist/src/lib/server-text-normalization.js +90 -0
  424. package/dist/src/lib/server-text-normalization.js.map +1 -0
  425. package/dist/src/lib/server-url.d.ts +22 -0
  426. package/dist/src/lib/server-url.js +33 -0
  427. package/dist/src/lib/server-url.js.map +1 -0
  428. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  429. package/dist/src/lib/signing-secret-status.js +116 -0
  430. package/dist/src/lib/signing-secret-status.js.map +1 -0
  431. package/dist/src/lib/skill-installer.d.ts +71 -0
  432. package/dist/src/lib/skill-installer.js +441 -0
  433. package/dist/src/lib/skill-installer.js.map +1 -0
  434. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  435. package/dist/src/lib/snapshot-audit-source.js +58 -0
  436. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  437. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  438. package/dist/src/lib/snapshot-audit-store.js +196 -0
  439. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  440. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  441. package/dist/src/lib/snapshot-audit.js +431 -0
  442. package/dist/src/lib/snapshot-audit.js.map +1 -0
  443. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  444. package/dist/src/lib/snapshot-build-rows.js +87 -0
  445. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  446. package/dist/src/lib/snapshot-build.d.ts +50 -0
  447. package/dist/src/lib/snapshot-build.js +111 -0
  448. package/dist/src/lib/snapshot-build.js.map +1 -0
  449. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  450. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  451. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  452. package/dist/src/lib/snapshots.d.ts +98 -0
  453. package/dist/src/lib/snapshots.js +294 -0
  454. package/dist/src/lib/snapshots.js.map +1 -0
  455. package/dist/src/lib/step-run-table.d.ts +43 -0
  456. package/dist/src/lib/step-run-table.js +129 -0
  457. package/dist/src/lib/step-run-table.js.map +1 -0
  458. package/dist/src/lib/storage-pending-retry.d.ts +26 -0
  459. package/dist/src/lib/storage-pending-retry.js +42 -0
  460. package/dist/src/lib/storage-pending-retry.js.map +1 -0
  461. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  462. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  463. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  464. package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
  465. package/dist/src/lib/swift-codegen/banners.js +25 -0
  466. package/dist/src/lib/swift-codegen/banners.js.map +1 -0
  467. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
  468. package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
  469. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
  470. package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
  471. package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
  472. package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
  473. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
  474. package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
  475. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
  476. package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
  477. package/dist/src/lib/swift-codegen/generator.js +457 -0
  478. package/dist/src/lib/swift-codegen/generator.js.map +1 -0
  479. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
  480. package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
  481. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
  482. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
  483. package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
  484. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
  485. package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
  486. package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
  487. package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
  488. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  489. package/dist/src/lib/sync-dir-selector.js +30 -0
  490. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  491. package/dist/src/lib/sync-paths.d.ts +128 -0
  492. package/dist/src/lib/sync-paths.js +195 -0
  493. package/dist/src/lib/sync-paths.js.map +1 -0
  494. package/dist/src/lib/sync-resource-types.d.ts +563 -0
  495. package/dist/src/lib/sync-resource-types.js +1073 -0
  496. package/dist/src/lib/sync-resource-types.js.map +1 -0
  497. package/dist/src/lib/sync-selectors.d.ts +138 -0
  498. package/dist/src/lib/sync-selectors.js +289 -0
  499. package/dist/src/lib/sync-selectors.js.map +1 -0
  500. package/dist/src/lib/template.d.ts +170 -0
  501. package/dist/src/lib/template.js +484 -68
  502. package/dist/src/lib/template.js.map +1 -1
  503. package/dist/src/lib/test-case-file-names.d.ts +40 -0
  504. package/dist/src/lib/test-case-file-names.js +91 -0
  505. package/dist/src/lib/test-case-file-names.js.map +1 -0
  506. package/dist/src/lib/test-case-keys.d.ts +29 -0
  507. package/dist/src/lib/test-case-keys.js +55 -0
  508. package/dist/src/lib/test-case-keys.js.map +1 -0
  509. package/dist/src/lib/test-case-variables.d.ts +29 -0
  510. package/dist/src/lib/test-case-variables.js +71 -0
  511. package/dist/src/lib/test-case-variables.js.map +1 -0
  512. package/dist/src/lib/token-inject.d.ts +56 -0
  513. package/dist/src/lib/token-inject.js +204 -0
  514. package/dist/src/lib/token-inject.js.map +1 -0
  515. package/dist/src/lib/toml-database-config.d.ts +123 -0
  516. package/dist/src/lib/toml-database-config.js +544 -0
  517. package/dist/src/lib/toml-database-config.js.map +1 -0
  518. package/dist/src/lib/toml-metadata-config.d.ts +151 -0
  519. package/dist/src/lib/toml-metadata-config.js +476 -0
  520. package/dist/src/lib/toml-metadata-config.js.map +1 -0
  521. package/dist/src/lib/toml-native-form.d.ts +46 -0
  522. package/dist/src/lib/toml-native-form.js +78 -0
  523. package/dist/src/lib/toml-native-form.js.map +1 -0
  524. package/dist/src/lib/toml-params-validator.d.ts +129 -0
  525. package/dist/src/lib/toml-params-validator.js +298 -0
  526. package/dist/src/lib/toml-params-validator.js.map +1 -0
  527. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  528. package/dist/src/lib/toml-scalar-edit.js +283 -0
  529. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  530. package/dist/src/lib/user-selector.d.ts +24 -0
  531. package/dist/src/lib/user-selector.js +33 -0
  532. package/dist/src/lib/user-selector.js.map +1 -0
  533. package/dist/src/lib/version-check.d.ts +35 -0
  534. package/dist/src/lib/version-check.js +241 -0
  535. package/dist/src/lib/version-check.js.map +1 -0
  536. package/dist/src/lib/watch.d.ts +121 -0
  537. package/dist/src/lib/watch.js +169 -0
  538. package/dist/src/lib/watch.js.map +1 -0
  539. package/dist/src/lib/web-url.d.ts +40 -0
  540. package/dist/src/lib/web-url.js +76 -0
  541. package/dist/src/lib/web-url.js.map +1 -0
  542. package/dist/src/lib/webhook-deliver.d.ts +209 -0
  543. package/dist/src/lib/webhook-deliver.js +519 -0
  544. package/dist/src/lib/webhook-deliver.js.map +1 -0
  545. package/dist/src/lib/workflow-apply.d.ts +110 -0
  546. package/dist/src/lib/workflow-apply.js +164 -0
  547. package/dist/src/lib/workflow-apply.js.map +1 -0
  548. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
  549. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
  550. package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
  551. package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
  552. package/dist/src/lib/workflow-codegen/generator.js +361 -0
  553. package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
  554. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
  555. package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
  556. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
  557. package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
  558. package/dist/src/lib/workflow-codegen/naming.js +81 -0
  559. package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
  560. package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
  561. package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
  562. package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
  563. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  564. package/dist/src/lib/workflow-config-apply.js +137 -0
  565. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  566. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  567. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  568. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  569. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  570. package/dist/src/lib/workflow-defaults.js +41 -0
  571. package/dist/src/lib/workflow-defaults.js.map +1 -0
  572. package/dist/src/lib/workflow-fragments.d.ts +64 -0
  573. package/dist/src/lib/workflow-fragments.js +342 -0
  574. package/dist/src/lib/workflow-fragments.js.map +1 -0
  575. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  576. package/dist/src/lib/workflow-include-preserve.js +286 -0
  577. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  578. package/dist/src/lib/workflow-payload.d.ts +98 -0
  579. package/dist/src/lib/workflow-payload.js +178 -0
  580. package/dist/src/lib/workflow-payload.js.map +1 -0
  581. package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
  582. package/dist/src/lib/workflow-toml-validator.js +770 -0
  583. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  584. package/dist/src/lib/workflow-usage.d.ts +196 -0
  585. package/dist/src/lib/workflow-usage.js +310 -0
  586. package/dist/src/lib/workflow-usage.js.map +1 -0
  587. package/dist/src/types/index.d.ts +591 -0
  588. package/dist/src/validators.d.ts +65 -0
  589. package/dist/src/validators.js +64 -0
  590. package/dist/src/validators.js.map +1 -0
  591. package/package.json +34 -9
@@ -1,21 +1,247 @@
1
- import { readFileSync, writeFileSync, statSync } from "fs";
2
- import { basename } from "path";
3
- import * as TOML from "@iarna/toml";
4
- import { lookup as mimeLookup } from "mime-types";
1
+ import { readFileSync, writeFileSync, readdirSync, } from "fs";
2
+ import * as path from "path";
3
+ import { stringifyConfigToml } from "../lib/config-toml.js";
4
+ import { generateWorkflowTypes, } from "../lib/workflow-codegen/generator.js";
5
+ import { generateWorkflowSwiftTypes } from "../lib/swift-codegen/generator.js";
5
6
  import { ApiClient } from "../lib/api-client.js";
6
7
  import { resolveAppId } from "../lib/config.js";
8
+ import { resolveWorkflowSelector } from "../lib/block-selector.js";
9
+ import { formatAccessRuleForDisplay } from "../lib/access-rule-display.js";
10
+ import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
11
+ import { expandWorkflow } from "../lib/workflow-fragments.js";
12
+ import { confirmPrompt } from "../lib/confirm-prompt.js";
13
+ import { parseStatusFilter } from "../lib/object-status-filter.js";
14
+ import { buildWorkflowRunEnvelope, buildWorkflowRunListEnvelope, buildWorkflowStepEnvelope, collectWorkflowFailures, describeEmptyRunPage, describeFailureSweep, formatFailureCodeCell, formatFailureErrorCell, formatFailureStepCell, selectFailedStepRow, toWorkflowRunInspectionRow, DEFAULT_FAILURE_SWEEP_MAX_SCAN, FAILURE_SWEEP_MAX_REQUESTS, SERVER_SCAN_ROWS_PER_REQUEST, } from "../lib/log-inspection.js";
7
15
  import chalk from "chalk";
8
- import { success, error, info, warn, keyValue, formatTable, formatId, formatDate, formatStatus, formatDuration, json, divider, } from "../lib/output.js";
16
+ import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, formatStatus, formatDuration, json, jsonLine, divider, dim, progress, progressEnd, } from "../lib/output.js";
17
+ import { pollUntil, followLoop, watchLoop, parseIntervalSeconds, } from "../lib/watch.js";
18
+ import { APPLY_RUN_STATUSES, RUN_STATUS_LIST, SETTLED_RUN_STATUSES, } from "../lib/run-status.js";
19
+ import { requireTestCaseInputVariables } from "../lib/test-case-variables.js";
20
+ /**
21
+ * Render the `Step Results` section of `workflows runs status`. Pure
22
+ * formatting helper — returns lines as strings so it's unit-testable
23
+ * (the caller console.logs them). For each step, emits:
24
+ *
25
+ * ` <id>: <status-label> <duration>`
26
+ *
27
+ * and, when applicable, indented follow-up lines:
28
+ *
29
+ * - `error_captured` step with `error` → ` error: <truncated message>`
30
+ * - forEach output with `errors[]` non-empty →
31
+ * ` forEach errors: <N/M> - first: <truncated message>`
32
+ *
33
+ * The truncation length matches `runs steps` ERROR column (80 chars,
34
+ * trailing "..." when truncated). See #688.
35
+ */
36
+ /**
37
+ * Stop-polling predicate for the `workflows preview --wait` loop (issues #941,
38
+ * #2348). True when the run has settled and the wait should stop — as opposed
39
+ * to a still-in-flight status ("queued"/"running") that keeps the loop going
40
+ * until the cap.
41
+ *
42
+ * Since #2348 the server reconciles every status surface onto the canonical
43
+ * vocabulary before it goes on the wire, so this reads the one shared set in
44
+ * `lib/run-status.ts` instead of re-deriving terminality here:
45
+ * - "completed" / "failed" / "terminated" — true terminals. "terminated" is
46
+ * an aborted preview, a real end state (Codex P2 on #941: it used to be
47
+ * omitted, so an abort was misreported as a wait timeout).
48
+ * - "apply_pending" / "apply_claimed" — the run finished and is parked
49
+ * waiting for a client to apply its result. It never advances on its own,
50
+ * so the previous ladder spun here until the 60s cap.
51
+ * - "missing" — no live instance to observe; more polling cannot help.
52
+ *
53
+ * The raw Cloudflare spellings ("complete"/"errored"/"waiting") are gone: the
54
+ * server no longer puts them on the wire, and matching them here would have
55
+ * kept two vocabularies alive.
56
+ */
57
+ export function isPreviewWaitTerminalStatus(s) {
58
+ return typeof s === "string" && SETTLED_RUN_STATUSES.has(s);
59
+ }
60
+ /**
61
+ * Which arm the `workflows preview --wait` render ladder takes (#2636).
62
+ *
63
+ * Extracted from the ladder so the branch decision is testable: the point of
64
+ * the `skipped` arm is that an elided preview — one whose declarative lock was
65
+ * held under `onContention: "ignore"` — reports that it did not run, instead
66
+ * of falling through to the timeout branch and exiting 124 as though it were
67
+ * still going.
68
+ */
69
+ export function previewWaitOutcome(status) {
70
+ if (status === "completed")
71
+ return "completed";
72
+ if (status === "failed")
73
+ return "failed";
74
+ if (status === "terminated")
75
+ return "terminated";
76
+ if (status === "skipped")
77
+ return "skipped";
78
+ if (typeof status === "string" && APPLY_RUN_STATUSES.has(status))
79
+ return "apply";
80
+ if (status === "missing")
81
+ return "missing";
82
+ return "timeout";
83
+ }
84
+ export function renderRunStatusStepResults(stepResults) {
85
+ const lines = [];
86
+ for (const step of stepResults || []) {
87
+ const statusLabel = step.skipped
88
+ ? chalk.gray("skipped")
89
+ : step.status === "error_captured"
90
+ ? chalk.yellow("error_captured")
91
+ : step.status === "failed"
92
+ ? chalk.red("failed")
93
+ : step.status === "completed"
94
+ ? chalk.green("completed")
95
+ : step.status || "?";
96
+ const durationLabel = formatDuration(step.durationMs);
97
+ lines.push(` ${step.id}: ${statusLabel} ${durationLabel}`);
98
+ // Single-step error_captured: show the captured error message.
99
+ if (step.status === "error_captured" && step.error) {
100
+ const errMsg = typeof step.error === "string" ? step.error : String(step.error);
101
+ const truncated = errMsg.length > 80 ? errMsg.slice(0, 77) + "..." : errMsg;
102
+ lines.push(` ${chalk.dim("error:")} ${chalk.yellow(truncated)}`);
103
+ }
104
+ else if (step.status === "error_captured" && step.errorDetails) {
105
+ // No top-level error but errorDetails present — hint to drill in.
106
+ lines.push(` ${chalk.dim("error: <see runs step-detail for full errorDetails>")}`);
107
+ }
108
+ // forEach aggregated errors: a one-line summary surfaces the count
109
+ // of `error_captured` iterations and a sample message (#688).
110
+ const fo = step.output;
111
+ if (fo && typeof fo === "object" && Array.isArray(fo.errors) && fo.errors.length > 0) {
112
+ const total = (Array.isArray(fo.items) ? fo.items.length : 0)
113
+ || (fo.totalSucceeded ?? 0) + (fo.totalFailed ?? 0);
114
+ const firstErr = (Array.isArray(fo.errorMessages) && fo.errorMessages.length > 0
115
+ ? fo.errorMessages[0]
116
+ : (fo.errors[0]?.error ?? "?"));
117
+ const firstStr = typeof firstErr === "string" ? firstErr : String(firstErr);
118
+ const truncated = firstStr.length > 80 ? firstStr.slice(0, 77) + "..." : firstStr;
119
+ lines.push(` ${chalk.dim("forEach errors:")} ${chalk.yellow(`${fo.totalFailed ?? fo.errors.length}/${total}`)} ${chalk.dim("- first:")} ${chalk.yellow(truncated)}`);
120
+ }
121
+ }
122
+ return lines;
123
+ }
124
+ // #3348 — the step table, the gap annotation and the running-step duration now
125
+ // live in `lib/step-run-table.ts`, shared with `functions runs steps`: a
126
+ // function run's steps are the same rows, and the two views must not be two
127
+ // tables that merely resemble each other. Re-exported from here because this is
128
+ // where the two helpers were first exported for testability.
129
+ export { annotateStepGaps, stepRunDisplayDurationMs, } from "../lib/step-run-table.js";
130
+ import { renderStepRunTable, stepRunDisplayDurationMs, } from "../lib/step-run-table.js";
131
+ import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
132
+ import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
133
+ /**
134
+ * #2771 — the step lines `workflows runs status` should render.
135
+ *
136
+ * Cloudflare's instance status carries `stepResults` only once the run reaches
137
+ * a terminal state, so a live run used to print `Status: running` and nothing
138
+ * else. The persisted step rows now exist DURING the run, so they are the
139
+ * fallback: mapped onto the shape `renderRunStatusStepResults` reads, with a
140
+ * running step's elapsed time in place of its 0 placeholder. The instance's own
141
+ * results always win when it has them. Exported for testability.
142
+ */
143
+ export function stepResultsForRunStatus(instanceStepResults, persistedStepRuns, now = Date.now()) {
144
+ if (instanceStepResults && instanceStepResults.length > 0)
145
+ return instanceStepResults;
146
+ return (persistedStepRuns || []).map((row) => ({
147
+ id: row?.stepId,
148
+ kind: row?.stepKind,
149
+ status: row?.status,
150
+ durationMs: stepRunDisplayDurationMs(row, now),
151
+ error: row?.error,
152
+ errorDetails: row?.errorDetails,
153
+ output: row?.output,
154
+ }));
155
+ }
156
+ /**
157
+ * #1367 — compute min / p50 / p95 / max over a set of latency samples (ms).
158
+ * Ignores non-finite values. Returns `null` when there are no valid samples.
159
+ * Percentiles use nearest-rank on the sorted samples. Exported for testability.
160
+ */
161
+ export function summarizeLatencyDistribution(values) {
162
+ const nums = (values || []).filter((v) => typeof v === "number" && Number.isFinite(v));
163
+ if (nums.length === 0)
164
+ return null;
165
+ nums.sort((a, b) => a - b);
166
+ const pct = (p) => {
167
+ const rank = Math.ceil((p / 100) * nums.length);
168
+ return nums[Math.min(nums.length - 1, Math.max(0, rank - 1))];
169
+ };
170
+ return {
171
+ count: nums.length,
172
+ min: nums[0],
173
+ p50: pct(50),
174
+ p95: pct(95),
175
+ max: nums[nums.length - 1],
176
+ };
177
+ }
178
+ /**
179
+ * Issue #687 (review feedback): render an unambiguous label for a workflow
180
+ * config slot ("active config", "draft config", etc.) when the slot may be
181
+ * unnamed. Falls back to a short ID prefix so users staging multiple unnamed
182
+ * configs can still tell them apart.
183
+ *
184
+ * formatConfigSlotLabel("active config", "v2", "01KRT...") → 'active config "v2"'
185
+ * formatConfigSlotLabel("active config", null, "01KRTABCDXYZ...") → 'active config (01KRTABCDXYZ…)'
186
+ * formatConfigSlotLabel("active config", null, null) → 'active config'
187
+ *
188
+ * Exported for testability.
189
+ */
190
+ export function formatConfigSlotLabel(slot, configName, configId, prefixLength = 12) {
191
+ if (configName) {
192
+ return `${slot} "${configName}"`;
193
+ }
194
+ if (configId) {
195
+ return `${slot} (${String(configId).slice(0, prefixLength)}…)`;
196
+ }
197
+ return slot;
198
+ }
199
+ /**
200
+ * Resolve a workflow reference — its server id, or the author-chosen key its
201
+ * TOML file is named for — for the `tests` verbs (#3249).
202
+ *
203
+ * The key is what an operator has: `workflows/<key>.toml` is the object and
204
+ * `workflows/<key>.tests/` its cases. The resolution lives in
205
+ * `block-selector.ts`, shared with `prompts`.
206
+ */
207
+ async function resolveWorkflowId(client, appId, ref) {
208
+ return (await resolveWorkflowSelector(client, appId, ref)).blockId;
209
+ }
9
210
  export function registerWorkflowsCommands(program) {
10
211
  const workflows = program
11
212
  .command("workflows")
12
- .description("Build multi-step workflows, publish revisions, and monitor runs")
213
+ .description("Inspect workflows, run and monitor them, and take one out of service")
13
214
  .addHelpText("after", `
14
215
  Examples:
15
216
  $ primitive workflows list
16
- $ primitive workflows create --from-file process-doc.toml
17
- $ primitive workflows publish 01HXY...
217
+ $ primitive config create workflow process-doc # scaffold workflows/process-doc.toml
218
+ $ primitive config push --only workflow/process-doc # apply it
18
219
  $ primitive workflows runs list 01HXY...
220
+ $ primitive workflows disable 01HXY... # take it out of service now
221
+ `);
222
+ // Server-side configuration is authored in TOML and applied with `config push`
223
+ // (issue #2645) — this group no longer carries config-setting flags.
224
+ workflows.addHelpText("after", `
225
+ Configuration lives in workflows/<key>.toml:
226
+ $ primitive config fields workflow # the file's keys, types and defaults
227
+ $ primitive config create workflow <key> # scaffold one locally
228
+ $ primitive config set workflow/<key> <path>=<value>
229
+ $ primitive config push --only workflow/<key>
230
+
231
+ Which body runs is configuration too: name it with activeConfigName in
232
+ workflows/<key>.toml (bodies live in workflows/<key>.configs/<name>.toml) and
233
+ apply it with 'config push'. There is no other write path: revisions are
234
+ read-only history from the retired legacy path (issue #2768).
235
+ `);
236
+ // Per-subject analytics have one home, the `analytics` noun (issue #2766) —
237
+ // this group's own analytics commands were removed, so say where they went.
238
+ workflows.addHelpText("after", `
239
+ Analytics live under the analytics noun:
240
+ $ primitive analytics workflows --window-days 7 --limit 10
241
+
242
+ That is the single home for per-subject analytics (workflows, prompts,
243
+ integrations). It ranks workflows by runs; there is no app-wide workflow
244
+ overview — the endpoint behind the retired overview command never existed.
19
245
  `);
20
246
  // List workflows
21
247
  workflows
@@ -23,21 +249,35 @@ Examples:
23
249
  .description("List workflows")
24
250
  .argument("[app-id]", "App ID (uses current app if not specified)")
25
251
  .option("--app <app-id>", "App ID")
26
- .option("--status <status>", "Filter by status: draft, active, archived")
252
+ .option("--status <status>", "Filter by status: active, inactive, archived")
253
+ .addOption(pageLimitOption())
254
+ .addOption(pageCursorOption())
27
255
  .option("--json", "Output as JSON")
28
256
  .action(async (appId, options) => {
257
+ const limit = parsePageLimit(options.limit);
29
258
  const resolvedAppId = resolveAppId(appId, options);
30
259
  const client = new ApiClient();
31
260
  try {
32
- const { items } = await client.listWorkflows(resolvedAppId, {
33
- status: options.status,
34
- });
261
+ // #2803 — `draft` is gone; refuse it by name rather than returning the
262
+ // empty list a canonical filter would produce. #2887 added `archived`:
263
+ // the delete flow writes one now, so asking for the retired rows is a
264
+ // question with an answer rather than a 400.
265
+ const page = normalizeCliListEnvelope(await client.listWorkflows(resolvedAppId, {
266
+ status: parseStatusFilter("workflows", options.status, [
267
+ "active",
268
+ "inactive",
269
+ "archived",
270
+ ]),
271
+ limit,
272
+ cursor: options.cursor,
273
+ }));
274
+ const items = page.items;
35
275
  if (options.json) {
36
- json(items);
276
+ json(page);
37
277
  return;
38
278
  }
39
- if (!items || items.length === 0) {
40
- info("No workflows found.");
279
+ if (items.length === 0) {
280
+ printEmptyPage(page, "workflows");
41
281
  return;
42
282
  }
43
283
  console.log(formatTable(items, [
@@ -47,89 +287,17 @@ Examples:
47
287
  { header: "STATUS", key: "status", format: formatStatus },
48
288
  { header: "MODIFIED", key: "modifiedAt", format: formatDate },
49
289
  ]));
290
+ printPageHint(page);
50
291
  }
51
292
  catch (err) {
52
293
  error(err.message);
53
294
  process.exit(1);
54
295
  }
55
296
  });
56
- // Create workflow
57
- workflows
58
- .command("create")
59
- .description("Create a new workflow")
60
- .argument("[app-id]", "App ID (uses current app if not specified)")
61
- .option("--app <app-id>", "App ID")
62
- .option("--key <key>", "Workflow key (unique identifier)")
63
- .option("--name <name>", "Display name")
64
- .option("--description <desc>", "Description")
65
- .option("--from-file <path>", "Load workflow from TOML file")
66
- .option("--requires-client-apply <bool>", "Require client-side apply: true or false")
67
- .option("--json", "Output as JSON")
68
- .action(async (appId, options) => {
69
- const resolvedAppId = resolveAppId(appId, options);
70
- const client = new ApiClient();
71
- let payload;
72
- if (options.fromFile) {
73
- try {
74
- const content = readFileSync(options.fromFile, "utf-8");
75
- const tomlData = TOML.parse(content);
76
- const workflow = tomlData.workflow || tomlData;
77
- payload = {
78
- workflowKey: workflow.key || workflow.workflowKey,
79
- name: workflow.name,
80
- description: workflow.description,
81
- steps: tomlData.steps || [],
82
- inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
83
- outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
84
- perUserMaxRunning: workflow.perUserMaxRunning,
85
- perUserMaxQueued: workflow.perUserMaxQueued,
86
- perAppMaxRunning: workflow.perAppMaxRunning,
87
- perAppMaxQueued: workflow.perAppMaxQueued,
88
- queueTtlSeconds: workflow.queueTtlSeconds,
89
- dequeueOrder: workflow.dequeueOrder,
90
- requiresClientApply: workflow.requiresClientApply,
91
- };
92
- }
93
- catch (err) {
94
- error(`Failed to read TOML file: ${err.message}`);
95
- process.exit(1);
96
- }
97
- }
98
- else {
99
- if (!options.key || !options.name) {
100
- error("Required: --key, --name (or use --from-file)");
101
- process.exit(1);
102
- }
103
- payload = {
104
- workflowKey: options.key,
105
- name: options.name,
106
- description: options.description,
107
- steps: [],
108
- };
109
- }
110
- if (options.requiresClientApply !== undefined) {
111
- payload.requiresClientApply = options.requiresClientApply === "true";
112
- }
113
- try {
114
- const result = await client.createWorkflow(resolvedAppId, payload);
115
- if (options.json) {
116
- json(result);
117
- return;
118
- }
119
- success(`Workflow created: ${result.workflow?.name || options.name}`);
120
- keyValue("Workflow ID", result.workflow?.workflowId);
121
- keyValue("Key", result.workflow?.workflowKey);
122
- }
123
- catch (err) {
124
- error(err.message);
125
- process.exit(1);
126
- }
127
- });
128
- // Get workflow
129
297
  workflows
130
298
  .command("get")
131
299
  .description("Get workflow details")
132
- .argument("<workflow-id>", "Workflow ID")
300
+ .argument("<workflow-id>", "Workflow ID or key")
133
301
  .option("--app <app-id>", "App ID (uses current app if not specified)")
134
302
  .option("--json", "Output as JSON")
135
303
  .action(async (workflowId, options) => {
@@ -142,20 +310,36 @@ Examples:
142
310
  return;
143
311
  }
144
312
  const wf = result.workflow;
145
- keyValue("Workflow ID", wf.workflowId);
146
- keyValue("Key", wf.workflowKey);
147
- keyValue("Name", wf.name);
148
- keyValue("Description", wf.description);
149
- keyValue("Status", formatStatus(wf.status));
150
- keyValue("Active Config", wf.activeConfigId || "-");
151
- keyValue("Latest Revision", wf.latestRevision || "-");
152
- keyValue("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
313
+ printResult("Workflow ID", wf.workflowId);
314
+ printResult("Key", wf.workflowKey);
315
+ printResult("Name", wf.name);
316
+ printResult("Description", wf.description);
317
+ // #2803 — ONE availability row. The server returns the canonical
318
+ // value, so a workflow an operator disabled reads `inactive` here and
319
+ // there is no second row left to disagree with it.
320
+ printResult("Status", formatStatus(wf.status));
321
+ printResult("Active Config", wf.activeConfigId || "-");
322
+ printResult("Latest Revision", wf.latestRevision || "-");
323
+ printResult("Client Apply", wf.requiresClientApply !== false ? "yes" : "no");
324
+ printResult("Sync Callable", wf.syncCallable === true ? "yes" : "no");
325
+ // #1233 — runAs unset means the engine defaults to "caller"; show that
326
+ // default rather than a blank so the deployed principal mode is visible.
327
+ printResult("Run As", wf.runAs || "caller");
328
+ printResult("Capabilities", Array.isArray(wf.capabilities) && wf.capabilities.length > 0
329
+ ? wf.capabilities.join(", ")
330
+ : "-");
331
+ // #2652: a caller workflow with no rule denies every non-admin start,
332
+ // so the row is always printed — the sentinel names the consequence,
333
+ // and a `runAs: "system"` workflow gets its own exempt wording.
334
+ printResult("Access Rule", formatAccessRuleForDisplay(wf.accessRule, "workflow", {
335
+ runAs: wf.runAs,
336
+ }));
153
337
  divider();
154
338
  info("Queue Settings:");
155
- keyValue(" Per User Max Running", wf.perUserMaxRunning);
156
- keyValue(" Per User Max Queued", wf.perUserMaxQueued);
157
- keyValue(" Per App Max Running", wf.perAppMaxRunning);
158
- keyValue(" Dequeue Order", wf.dequeueOrder);
339
+ printResult(" Per User Max Running", wf.perUserMaxRunning);
340
+ printResult(" Per User Max Queued", wf.perUserMaxQueued);
341
+ printResult(" Per App Max Running", wf.perAppMaxRunning);
342
+ printResult(" Dequeue Order", wf.dequeueOrder);
159
343
  if (wf.inputSchema) {
160
344
  divider();
161
345
  info("Input Schema:");
@@ -196,163 +380,136 @@ Examples:
196
380
  process.exit(1);
197
381
  }
198
382
  });
199
- // Update workflow
200
383
  workflows
201
- .command("update")
202
- .description("Update workflow metadata")
203
- .argument("<workflow-id>", "Workflow ID")
204
- .option("--app <app-id>", "App ID (uses current app if not specified)")
205
- .option("--name <name>", "Display name")
206
- .option("--description <desc>", "Description")
207
- .option("--status <status>", "Status: draft, active, archived")
208
- .option("--per-user-max-running <n>", "Max running per user")
209
- .option("--per-user-max-queued <n>", "Max queued per user")
210
- .option("--dequeue-order <order>", "Dequeue order: fifo, lifo")
211
- .option("--requires-client-apply <bool>", "Require client-side apply: true or false")
212
- .option("--json", "Output as JSON")
213
- .action(async (workflowId, options) => {
214
- const resolvedAppId = resolveAppId(undefined, options);
215
- const payload = {};
216
- if (options.name)
217
- payload.name = options.name;
218
- if (options.description)
219
- payload.description = options.description;
220
- if (options.status)
221
- payload.status = options.status;
222
- if (options.perUserMaxRunning)
223
- payload.perUserMaxRunning = parseInt(options.perUserMaxRunning);
224
- if (options.perUserMaxQueued)
225
- payload.perUserMaxQueued = parseInt(options.perUserMaxQueued);
226
- if (options.dequeueOrder)
227
- payload.dequeueOrder = options.dequeueOrder;
228
- if (options.requiresClientApply !== undefined) {
229
- payload.requiresClientApply = options.requiresClientApply === "true";
230
- }
231
- if (Object.keys(payload).length === 0) {
232
- error("No update options specified.");
233
- process.exit(1);
234
- }
235
- const client = new ApiClient();
384
+ .command("expand")
385
+ .description("Expand a workflow TOML's include fragments and print the result")
386
+ .argument("<file>", "Path to the workflow TOML file")
387
+ .option("--format <format>", "Output format: json (default) or toml", "json")
388
+ .action((file, options) => {
236
389
  try {
237
- const result = await client.updateWorkflow(resolvedAppId, workflowId, payload);
238
- if (options.json) {
239
- json(result);
240
- return;
390
+ const expanded = expandWorkflow(file);
391
+ if (options.format === "toml") {
392
+ // Strip the `include` key (already removed by expander) and emit
393
+ // valid TOML so the result is round-trippable.
394
+ console.log(stringifyConfigToml(expanded));
395
+ }
396
+ else {
397
+ console.log(JSON.stringify(expanded, null, 2));
241
398
  }
242
- success("Workflow updated.");
243
399
  }
244
400
  catch (err) {
245
- error(err.message);
401
+ error(err?.message ?? String(err));
246
402
  process.exit(1);
247
403
  }
248
404
  });
249
- // Delete workflow
405
+ // #2645 criterion 9 / #2803 — the operational verbs.
406
+ //
407
+ // Configuration is authored in TOML and applied with `config push`, which would
408
+ // otherwise mean an operator needs a repo checkout to stop a misbehaving
409
+ // workflow. It does not: taking something out of service is an OPERATION, and
410
+ // these two verbs are the ONLY writers of `status`, which is server-owned and
411
+ // no longer a TOML key. The committed configuration says nothing about
412
+ // availability, so `config diff` reports the server's status as operator
413
+ // state rather than as drift.
250
414
  workflows
251
- .command("delete")
252
- .description("Delete or archive a workflow")
253
- .argument("<workflow-id>", "Workflow ID")
415
+ .command("disable")
416
+ .description("Take a workflow out of service now. Operational, not configuration: `status` is server-owned and 'config push' can neither set nor clear it.")
417
+ .argument("<workflow-id>", "Workflow ID or key")
254
418
  .option("--app <app-id>", "App ID (uses current app if not specified)")
255
- .option("--hard", "Permanently delete instead of archive")
256
- .option("-y, --yes", "Skip confirmation prompt")
419
+ .option("--json", "Output as JSON")
257
420
  .action(async (workflowId, options) => {
258
421
  const resolvedAppId = resolveAppId(undefined, options);
259
- if (!options.yes) {
260
- const action = options.hard ? "permanently delete" : "archive";
261
- const inquirer = await import("inquirer");
262
- const { confirm } = await inquirer.default.prompt([
263
- {
264
- type: "confirm",
265
- name: "confirm",
266
- message: `Are you sure you want to ${action} workflow ${workflowId}?`,
267
- default: false,
268
- },
269
- ]);
270
- if (!confirm) {
271
- info("Cancelled.");
272
- return;
273
- }
274
- }
275
422
  const client = new ApiClient();
276
423
  try {
277
- if (options.hard) {
278
- await client.deleteWorkflow(resolvedAppId, workflowId);
279
- success("Workflow deleted.");
280
- }
281
- else {
282
- await client.updateWorkflow(resolvedAppId, workflowId, { status: "archived" });
283
- success("Workflow archived.");
424
+ const result = await client.disableWorkflow(resolvedAppId, workflowId);
425
+ if (options.json) {
426
+ json(result);
427
+ return;
284
428
  }
429
+ success(`Workflow disabled: ${result.workflowKey || workflowId}`);
430
+ info("New runs are refused. Configuration is unchanged — `primitive workflows enable` puts it back.");
285
431
  }
286
432
  catch (err) {
287
433
  error(err.message);
288
434
  process.exit(1);
289
435
  }
290
436
  });
291
- // Draft subcommand
292
- const draft = workflows.command("draft").description("Manage workflow draft");
293
- // Update draft
294
- draft
295
- .command("update")
296
- .description("Update workflow draft steps")
297
- .argument("<workflow-id>", "Workflow ID")
437
+ workflows
438
+ .command("enable")
439
+ .description("Put a workflow back in service.")
440
+ .argument("<workflow-id>", "Workflow ID or key")
298
441
  .option("--app <app-id>", "App ID (uses current app if not specified)")
299
- .option("--from-file <path>", "Load steps from TOML file")
300
442
  .option("--json", "Output as JSON")
301
443
  .action(async (workflowId, options) => {
302
444
  const resolvedAppId = resolveAppId(undefined, options);
303
- if (!options.fromFile) {
304
- error("--from-file is required");
305
- process.exit(1);
306
- }
307
- let payload;
308
- try {
309
- const content = readFileSync(options.fromFile, "utf-8");
310
- const tomlData = TOML.parse(content);
311
- const workflow = tomlData.workflow || tomlData;
312
- payload = {
313
- steps: tomlData.steps || [],
314
- inputSchema: workflow.inputSchema ? JSON.parse(workflow.inputSchema) : undefined,
315
- outputSchema: workflow.outputSchema ? JSON.parse(workflow.outputSchema) : undefined,
316
- };
317
- }
318
- catch (err) {
319
- error(`Failed to read TOML file: ${err.message}`);
320
- process.exit(1);
321
- }
322
445
  const client = new ApiClient();
323
446
  try {
324
- const result = await client.updateWorkflowDraft(resolvedAppId, workflowId, payload);
447
+ const result = await client.enableWorkflow(resolvedAppId, workflowId);
325
448
  if (options.json) {
326
449
  json(result);
327
450
  return;
328
451
  }
329
- success("Draft updated.");
330
- keyValue("Steps", result.draft?.steps?.length || 0);
452
+ success(`Workflow enabled: ${result.workflowKey || workflowId}`);
331
453
  }
332
454
  catch (err) {
333
455
  error(err.message);
334
456
  process.exit(1);
335
457
  }
336
458
  });
337
- // Publish workflow
459
+ // #2907 — the delete flow's CLI spelling. Not a second writer of `status`:
460
+ // this is the plain `DELETE` #2887 made retire-with-history, which until now
461
+ // only the admin API and the console could reach. Named `archive`, not
462
+ // `delete`, because a configuration noun gets no `delete` verb
463
+ // (docs/cli-design.md) and prune-by-push stays the only CLI hard-delete path.
338
464
  workflows
339
- .command("publish")
340
- .description("Publish the current draft as a new revision")
341
- .argument("<workflow-id>", "Workflow ID")
465
+ .command("archive")
466
+ .description("Retire a workflow (soft delete). Delete-lifecycle, not `disable`: the definition is kept so its history still resolves, it goes on holding its key, and there is no un-archive.")
467
+ .argument("<workflow-id>", "Workflow ID or key")
342
468
  .option("--app <app-id>", "App ID (uses current app if not specified)")
469
+ .option("-y, --yes", "Skip confirmation prompt")
343
470
  .option("--json", "Output as JSON")
471
+ .addHelpText("after", `
472
+ What archiving a workflow costs (retire-with-history):
473
+ New runs are refused, and so is every \`workflow.call\` into it.
474
+ Its runs, revisions, configurations and test cases keep resolving what they
475
+ point at — that is what the tombstone exists for.
476
+ The archived row still holds its workflowKey.
477
+ \`primitive workflows enable\` refuses an archived workflow; there is no
478
+ un-archive.
479
+
480
+ Reclaiming the key means hard-deleting the row and its cascade:
481
+ $ rm workflows/<key>.toml
482
+ $ primitive config push --prune # confirmed; hard-deletes the row
483
+ # or hard-delete it in the console, then re-add the file and push.
484
+ `)
344
485
  .action(async (workflowId, options) => {
345
486
  const resolvedAppId = resolveAppId(undefined, options);
487
+ if (!options.yes) {
488
+ let confirmed;
489
+ try {
490
+ confirmed = await confirmPrompt(`Archive workflow ${workflowId}? New runs are refused, its history is ` +
491
+ `kept, it goes on holding its key, and there is no un-archive.`);
492
+ }
493
+ catch (err) {
494
+ error(err.message);
495
+ process.exit(1);
496
+ }
497
+ if (!confirmed) {
498
+ info("Cancelled.");
499
+ return;
500
+ }
501
+ }
346
502
  const client = new ApiClient();
347
503
  try {
348
- const result = await client.publishWorkflow(resolvedAppId, workflowId);
504
+ // The soft delete: no `hard`, so the #1238 cascade does not run and the
505
+ // definition row stays queryable.
506
+ const result = await client.deleteWorkflow(resolvedAppId, workflowId);
349
507
  if (options.json) {
350
508
  json(result);
351
509
  return;
352
510
  }
353
- success("Workflow published.");
354
- keyValue("Revision ID", result.revision?.revisionId);
355
- keyValue("Published At", formatDate(result.revision?.publishedAt));
511
+ success(`Workflow archived: ${workflowId}`);
512
+ info("Runs, revisions and test cases keep resolving. The row still holds its key — reclaim it by removing its file and running a confirmed `primitive config push --prune`.");
356
513
  }
357
514
  catch (err) {
358
515
  error(err.message);
@@ -363,14 +520,20 @@ Examples:
363
520
  workflows
364
521
  .command("preview")
365
522
  .description("Run a preview execution of the workflow")
366
- .argument("<workflow-id>", "Workflow ID")
523
+ .argument("<workflow-id>", "Workflow ID or key")
367
524
  .option("--app <app-id>", "App ID (uses current app if not specified)")
368
- .option("--config <config-id>", "Use specific configuration (uses draft if not specified)")
525
+ .option("--config <config-id>", "Use specific configuration")
526
+ .option("--draft", "Force preview of the draft version, even if active is newer")
527
+ .option("--active", "Force preview of the active config, even if a newer draft exists (issue #687)")
369
528
  .option("--input <json>", "Root input as JSON")
370
529
  .option("--wait", "Wait for completion and show result")
371
530
  .option("--json", "Output as JSON")
372
531
  .action(async (workflowId, options) => {
373
532
  const resolvedAppId = resolveAppId(undefined, options);
533
+ if (options.draft && options.active) {
534
+ error("--draft and --active are mutually exclusive");
535
+ process.exit(1);
536
+ }
374
537
  let rootInput;
375
538
  if (options.input) {
376
539
  try {
@@ -386,7 +549,15 @@ Examples:
386
549
  const result = await client.previewWorkflow(resolvedAppId, workflowId, {
387
550
  rootInput,
388
551
  configId: options.config,
552
+ useDraft: options.draft || false,
553
+ // Issue #687: --active forces the active config even when a newer
554
+ // draft exists (the inverse of --draft).
555
+ preferActive: options.active || false,
389
556
  });
557
+ // Display warning from the server (e.g. "previewing draft because newer than active")
558
+ if (result.warning) {
559
+ warn(result.warning);
560
+ }
390
561
  if (!options.wait) {
391
562
  if (options.json) {
392
563
  json(result);
@@ -394,26 +565,77 @@ Examples:
394
565
  }
395
566
  success("Preview started.");
396
567
  keyValue("Instance ID", result.instanceId);
568
+ // Issue #687: name the side we ran so the user always knows which
569
+ // version produced the output (the "print the source" pattern).
570
+ const source = result.source;
571
+ if (source === "draft") {
572
+ info("Previewing draft version.");
573
+ }
574
+ else if (source === "active") {
575
+ // Issue #687 (review feedback): when configName is missing,
576
+ // fall back to a short ID prefix so users staging multiple
577
+ // unnamed configs can disambiguate.
578
+ const label = formatConfigSlotLabel("active config", result.configName, result.configId);
579
+ info(`Previewing ${label}.`);
580
+ }
581
+ else if (source === "config") {
582
+ const label = formatConfigSlotLabel("the requested config", result.configName, result.configId);
583
+ info(`Previewing ${label}.`);
584
+ }
397
585
  info("Use 'workflows runs status' to check progress.");
398
586
  return;
399
587
  }
400
- // Poll for completion
588
+ // Poll for completion. Issue #941: track whether the loop broke on a
589
+ // terminal state vs. ran to exhaustion, so a timed-out wait is
590
+ // distinguishable from a workflow that legitimately reported "running".
401
591
  info("Waiting for completion...");
402
592
  let status;
403
- const maxAttempts = 60;
593
+ // Fixed ~60s cap (60 × 1s sleep + RTT). PRIMITIVE_PREVIEW_WAIT_ATTEMPTS
594
+ // overrides the iteration count for tests that exercise the
595
+ // poll-loop-exhaustion path without a full 60s wait; unset in normal use.
596
+ const maxAttempts = (() => {
597
+ const raw = process.env.PRIMITIVE_PREVIEW_WAIT_ATTEMPTS;
598
+ const parsed = raw ? parseInt(raw, 10) : NaN;
599
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 60;
600
+ })();
601
+ let reachedTerminal = false;
404
602
  for (let i = 0; i < maxAttempts; i++) {
405
603
  await new Promise((r) => setTimeout(r, 1000));
406
604
  const statusResult = await client.getPreviewStatus(resolvedAppId, workflowId, result.instanceId);
407
605
  status = statusResult.status;
408
- if (status?.status === "completed" || status?.status === "failed") {
606
+ const s = status?.status;
607
+ // Issue #941 (Codex P2): an aborted preview reports
608
+ // status:"terminated" from the admin preview-status endpoint, which
609
+ // is a real terminal state — the wait did NOT time out. The shared
610
+ // predicate includes it so the loop breaks and `waitTimedOut` stays
611
+ // false (vs. the prior check that omitted "terminated" and let the
612
+ // loop run to the cap, misreporting an abort as a timeout).
613
+ if (isPreviewWaitTerminalStatus(s)) {
614
+ reachedTerminal = true;
409
615
  break;
410
616
  }
411
617
  }
618
+ // Issue #941: on poll-loop exhaustion with a still-non-terminal run,
619
+ // emit an explicit timeout signal and exit non-zero (124, the GNU
620
+ // `timeout` convention) instead of silently returning the bare
621
+ // non-terminal status as if the wait succeeded.
622
+ const waitTimedOut = !reachedTerminal;
412
623
  if (options.json) {
413
- json({ instanceId: result.instanceId, status });
624
+ // Additive field only — `status` is preserved so existing parsers
625
+ // don't break; `waitTimedOut` lets automation tell "still running,
626
+ // wait gave up" from "finished".
627
+ json({ instanceId: result.instanceId, status, waitTimedOut });
628
+ if (waitTimedOut) {
629
+ process.exit(124);
630
+ }
414
631
  return;
415
632
  }
416
- if (status?.status === "completed") {
633
+ // #2348: this render ladder used to re-derive terminality (and matched
634
+ // the raw "complete"/"errored" spellings the server no longer emits).
635
+ // It now branches on the same canonical values the poll loop stops on,
636
+ // via the shared sets in lib/run-status.ts.
637
+ const waitOutcome = previewWaitOutcome(status?.status);
638
+ if (waitOutcome === "completed") {
417
639
  success("Preview completed.");
418
640
  if (status.output) {
419
641
  console.log("\nOutput:");
@@ -427,15 +649,45 @@ Examples:
427
649
  });
428
650
  }
429
651
  }
430
- else if (status?.status === "failed") {
652
+ else if (waitOutcome === "failed") {
431
653
  error("Preview failed.");
432
654
  if (status.error) {
433
655
  console.log("\nError:");
434
656
  console.log(JSON.stringify(status.error, null, 2));
435
657
  }
436
658
  }
659
+ else if (waitOutcome === "terminated") {
660
+ // Issue #941 (Codex P2): the preview was aborted (e.g. via
661
+ // `workflows preview ... abort`, or an external terminate). This is a
662
+ // real terminal state — NOT a timeout — so render it distinctly and
663
+ // exit 0 (the wait succeeded in observing a terminal result).
664
+ warn("Preview was terminated (aborted before completion).");
665
+ }
666
+ else if (waitOutcome === "skipped") {
667
+ // #2636 — the preview's declarative lock was held and the definition
668
+ // declared `onContention: "ignore"`, so it never ran. That is an
669
+ // outcome, not a timeout: report it and exit 0.
670
+ warn(`Preview did not run — lock held${status?.skipReason ? ` (${status.skipReason})` : ""}.`);
671
+ }
672
+ else if (waitOutcome === "apply") {
673
+ // #2348: the run finished and is parked waiting for a client to claim
674
+ // and apply its result. It will not advance on its own, so the wait
675
+ // stopping here is a success, not a timeout.
676
+ warn(`Preview finished and is waiting to be applied (status: ${status.status}).`);
677
+ if (status.output) {
678
+ console.log("\nOutput:");
679
+ console.log(JSON.stringify(status.output, null, 2));
680
+ }
681
+ }
682
+ else if (waitOutcome === "missing") {
683
+ // #2348: no live instance to observe — reporting a timeout here would
684
+ // suggest the preview is still running, which it is not.
685
+ error("Preview instance not found (status: missing).");
686
+ }
437
687
  else {
438
- info("Preview still running. Check status with 'workflows runs status'.");
688
+ // Timed out without ever reaching a terminal state (issue #941).
689
+ error(`Wait timed out after ${maxAttempts}s; preview still running. Check status with 'workflows runs status'.`);
690
+ process.exit(124);
439
691
  }
440
692
  }
441
693
  catch (err) {
@@ -443,51 +695,401 @@ Examples:
443
695
  process.exit(1);
444
696
  }
445
697
  });
698
+ // #1367 — hidden queue-latency measurement tool. Fires a batch of runs of a
699
+ // (trivial, no-op) workflow, polls until each has an executionStartedAt, and
700
+ // reports the observed queueDelayMs / createCallDurationMs distribution. This
701
+ // is the post-deploy verification vehicle: local miniflare does not exhibit
702
+ // real Cloudflare scheduling delay, so run it against an agents/alpha scratch
703
+ // app. Creates are paced (default 10/s) to stay clear of CF's ~100/s shared
704
+ // instance-creation limit; a create failure (e.g. 429) is recorded, not fatal.
705
+ workflows
706
+ .command("queue-latency-test", { hidden: true })
707
+ .description("(hidden) Fire N no-op runs and report the queue-delay distribution (post-deploy tool)")
708
+ .argument("<workflow-id>", "Workflow ID or key of a trivial no-op workflow")
709
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
710
+ .option("--count <n>", "Number of runs to fire", "50")
711
+ .option("--rate <r>", "Max creates per second (<=10 recommended)", "10")
712
+ .option("--input <json>", "Root input as JSON")
713
+ .option("--timeout <sec>", "Seconds to wait for all runs to start", "120")
714
+ .option("--verbose", "Print a per-run row")
715
+ .option("--json", "Output as JSON")
716
+ .action(async (workflowId, options) => {
717
+ const resolvedAppId = resolveAppId(undefined, options);
718
+ const client = new ApiClient();
719
+ const count = Math.max(1, parseInt(options.count, 10) || 50);
720
+ const rate = Math.max(1, parseInt(options.rate, 10) || 10);
721
+ const timeoutSec = Math.max(1, parseInt(options.timeout, 10) || 120);
722
+ const intervalMs = Math.ceil(1000 / rate);
723
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
724
+ let rootInput;
725
+ if (options.input) {
726
+ try {
727
+ rootInput = JSON.parse(options.input);
728
+ }
729
+ catch {
730
+ error("Invalid JSON in --input");
731
+ process.exit(1);
732
+ }
733
+ }
734
+ const fired = [];
735
+ if (!options.json) {
736
+ info(`Firing ${count} run(s) of ${workflowId} at up to ${rate}/s (interval ${intervalMs}ms)...`);
737
+ }
738
+ for (let i = 0; i < count; i++) {
739
+ try {
740
+ const res = await client.previewWorkflow(resolvedAppId, workflowId, {
741
+ rootInput,
742
+ });
743
+ fired.push({
744
+ index: i,
745
+ instanceId: res?.instanceId,
746
+ createFailed: false,
747
+ started: false,
748
+ });
749
+ }
750
+ catch (err) {
751
+ // A create failure (e.g. HTTP 500 from CF's shared-class rate limit) is
752
+ // a recorded datapoint, not a reason to abort the whole batch.
753
+ fired.push({
754
+ index: i,
755
+ createFailed: true,
756
+ createError: err?.message ? String(err.message) : String(err),
757
+ started: false,
758
+ });
759
+ }
760
+ if (i < count - 1)
761
+ await sleep(intervalMs);
762
+ }
763
+ // Poll the run list until every non-failed run has executionStartedAt set
764
+ // (queueDelayMs present) or we hit the timeout.
765
+ const byInstance = new Map();
766
+ for (const f of fired) {
767
+ if (f.instanceId)
768
+ byInstance.set(f.instanceId, f);
769
+ }
770
+ const deadline = Date.now() + timeoutSec * 1000;
771
+ const pending = () => fired.filter((f) => !f.createFailed && !f.started).length;
772
+ // The admin run-list endpoint caps each page at 200, so a single large
773
+ // request cannot cover a batch of more than 200 runs — the earliest ones
774
+ // would never be matched and would be falsely reported as not started.
775
+ // Page through with the cursor (runs come back newest-first, so our batch
776
+ // is at the front) until every pending run is matched or we run out of
777
+ // pages. Bound the scan so we never walk the whole run history.
778
+ const PAGE_LIMIT = 200;
779
+ const scanBudget = count + PAGE_LIMIT;
780
+ while (pending() > 0 && Date.now() < deadline) {
781
+ await sleep(1000);
782
+ try {
783
+ let cursor;
784
+ let scanned = 0;
785
+ do {
786
+ const { items, cursor: nextCursor } = await client.listWorkflowRuns(resolvedAppId, workflowId, { limit: PAGE_LIMIT, cursor: cursor ?? undefined });
787
+ for (const run of items || []) {
788
+ const f = run?.instanceId ? byInstance.get(run.instanceId) : undefined;
789
+ if (f && !f.started && run.executionStartedAt) {
790
+ f.started = true;
791
+ f.queueDelayMs =
792
+ typeof run.queueDelayMs === "number" ? run.queueDelayMs : null;
793
+ f.createCallDurationMs =
794
+ typeof run.createCallDurationMs === "number"
795
+ ? run.createCallDurationMs
796
+ : null;
797
+ }
798
+ }
799
+ scanned += items?.length || 0;
800
+ cursor = nextCursor;
801
+ } while (pending() > 0 && cursor && scanned < scanBudget);
802
+ }
803
+ catch {
804
+ // Transient list error — keep polling until the deadline.
805
+ }
806
+ }
807
+ const createFailures = fired.filter((f) => f.createFailed).length;
808
+ const startedRuns = fired.filter((f) => f.started);
809
+ const queueDelays = startedRuns.map((f) => f.queueDelayMs);
810
+ const createDurations = startedRuns.map((f) => f.createCallDurationMs);
811
+ const queueDist = summarizeLatencyDistribution(queueDelays);
812
+ const createDist = summarizeLatencyDistribution(createDurations);
813
+ const notStarted = fired.filter((f) => !f.createFailed && !f.started).length;
814
+ if (options.json) {
815
+ json({
816
+ workflowId,
817
+ requested: count,
818
+ created: count - createFailures,
819
+ createFailures,
820
+ started: startedRuns.length,
821
+ notStartedWithinTimeout: notStarted,
822
+ queueDelayMs: queueDist,
823
+ createCallDurationMs: createDist,
824
+ runs: options.verbose
825
+ ? fired.map((f) => ({
826
+ index: f.index,
827
+ instanceId: f.instanceId ?? null,
828
+ createFailed: f.createFailed,
829
+ createError: f.createError ?? null,
830
+ started: f.started,
831
+ queueDelayMs: f.queueDelayMs ?? null,
832
+ createCallDurationMs: f.createCallDurationMs ?? null,
833
+ }))
834
+ : undefined,
835
+ });
836
+ return;
837
+ }
838
+ divider();
839
+ keyValue("Requested", count);
840
+ keyValue("Created", count - createFailures);
841
+ keyValue("Create failures", createFailures);
842
+ keyValue("Started", startedRuns.length);
843
+ keyValue("Not started (timeout)", notStarted);
844
+ divider();
845
+ if (queueDist) {
846
+ info("Queue delay (executionStartedAt - requested):");
847
+ keyValue(" min", formatDuration(queueDist.min));
848
+ keyValue(" p50", formatDuration(queueDist.p50));
849
+ keyValue(" p95", formatDuration(queueDist.p95));
850
+ keyValue(" max", formatDuration(queueDist.max));
851
+ }
852
+ else {
853
+ warn("No runs started — no queue-delay samples collected.");
854
+ }
855
+ if (createDist) {
856
+ divider();
857
+ info("Create-call duration:");
858
+ keyValue(" min", formatDuration(createDist.min));
859
+ keyValue(" p50", formatDuration(createDist.p50));
860
+ keyValue(" p95", formatDuration(createDist.p95));
861
+ keyValue(" max", formatDuration(createDist.max));
862
+ }
863
+ if (options.verbose) {
864
+ divider();
865
+ console.log(formatTable(fired.map((f) => ({
866
+ run: f.index,
867
+ instanceId: f.instanceId ?? "-",
868
+ status: f.createFailed ? "create-failed" : f.started ? "started" : "pending",
869
+ queueDelayMs: f.queueDelayMs,
870
+ createCallDurationMs: f.createCallDurationMs,
871
+ })), [
872
+ { header: "RUN", key: "run" },
873
+ { header: "INSTANCE", key: "instanceId", format: formatId },
874
+ { header: "STATE", key: "status" },
875
+ { header: "DELAY", key: "queueDelayMs", format: formatDuration },
876
+ { header: "CREATE", key: "createCallDurationMs", format: formatDuration },
877
+ ]));
878
+ }
879
+ });
446
880
  // Runs subcommand
447
881
  const runs = workflows.command("runs").description("Manage workflow runs");
448
882
  // List runs
883
+ const runsTableColumns = [
884
+ { header: "RUN ID", key: "runId", format: formatId },
885
+ { header: "STATUS", key: "status", format: formatStatus },
886
+ { header: "STARTED", key: "startedAt", format: formatDate },
887
+ // #1367 — queue delay (requested → execution start).
888
+ { header: "DELAY", key: "queueDelayMs", format: formatDuration },
889
+ { header: "ENDED", key: "endedAt", format: formatDate },
890
+ { header: "PREVIEW", key: "isPreview", format: (v) => (v ? "yes" : "") },
891
+ ];
449
892
  runs
450
893
  .command("list")
451
894
  .description("List workflow runs")
452
- .argument("<workflow-id>", "Workflow ID")
895
+ // #1967 — the workflow id is optional when `--user-id` is given: that
896
+ // form lists one user's runs across every workflow.
897
+ .argument("[workflow-id]", "Workflow ID or key (optional with --user-id)")
453
898
  .option("--app <app-id>", "App ID (uses current app if not specified)")
454
- .option("--status <status>", "Filter by status: pending, running, completed, failed")
899
+ // #2348: the canonical run-status vocabulary. The old text advertised
900
+ // "pending", which no server path ever writes, and omitted "terminated".
901
+ // An unrecognised value is now a 400 from the server, not an empty page.
902
+ .option("--status <status>", `Filter by status: ${RUN_STATUS_LIST}`)
903
+ .option("--user-id <user-id>", "List runs started by this user, across every workflow")
455
904
  .option("--limit <n>", "Number of runs to show", "20")
905
+ .option("--cursor <cursor>", "Pagination cursor from a previous page")
906
+ // #1969 — shared tailing flags. `--watch` re-renders the current list each
907
+ // interval (snapshot); `--follow` appends new/changed runs (tail). Mutually
908
+ // exclusive; `--interval` floor is 1s (default 2s).
909
+ .option("--watch", "Re-render the current run list every interval (snapshot)")
910
+ .option("--follow", "Append runs as they start or change (tail)")
911
+ .option("--interval <seconds>", "Poll interval in seconds (min 1, default 2)")
456
912
  .option("--json", "Output as JSON")
457
913
  .action(async (workflowId, options) => {
458
914
  const resolvedAppId = resolveAppId(undefined, options);
459
915
  const client = new ApiClient();
460
- try {
461
- const { items } = await client.listWorkflowRuns(resolvedAppId, workflowId, {
462
- status: options.status,
463
- limit: parseInt(options.limit),
464
- });
465
- if (options.json) {
466
- json(items);
467
- return;
916
+ const limit = parseInt(options.limit);
917
+ if (options.watch && options.follow) {
918
+ error("--watch and --follow are mutually exclusive; pass only one.");
919
+ process.exit(1);
920
+ }
921
+ // #1967 — one of the two selectors is required; `list` never enumerates
922
+ // every run in the app.
923
+ if (!workflowId && !options.userId) {
924
+ error("Provide a workflow ID, or --user-id to list one user's runs across every workflow.");
925
+ process.exit(1);
926
+ }
927
+ // The user view reads the runs-by-user index, which has no resume
928
+ // contract — `--follow`'s checkpoint is keyed on the by-workflow index.
929
+ if (options.userId && options.follow) {
930
+ error("--follow is not supported with --user-id (it reads a different index). Use --watch instead.");
931
+ process.exit(1);
932
+ }
933
+ // Both selectors resolve through the same page shape, so the rest of the
934
+ // command is identical either way.
935
+ const fetchPage = (params) => options.userId
936
+ ? client.listUserWorkflowRuns(resolvedAppId, options.userId, {
937
+ ...params,
938
+ // With both a workflow id and --user-id, narrow to that workflow.
939
+ workflowId: workflowId || undefined,
940
+ })
941
+ : client.listWorkflowRuns(resolvedAppId, workflowId, params);
942
+ // One-shot list (no tailing).
943
+ if (!options.watch && !options.follow) {
944
+ try {
945
+ const page = await fetchPage({
946
+ status: options.status,
947
+ limit,
948
+ cursor: options.cursor,
949
+ });
950
+ const { items } = page;
951
+ if (options.json) {
952
+ // The CLI's one list envelope with the shared inspection item
953
+ // shape — never a bare array, and since #3646 no `nextCursor:
954
+ // null` and no deprecated `cursor` alias either, because this is a
955
+ // `list` verb like any other (behaviors 6, 7).
956
+ json(buildWorkflowRunListEnvelope(items, page));
957
+ return;
958
+ }
959
+ // The cursor hint prints whether or not this page had rows: a
960
+ // filtered read can come back empty with more of the index still to
961
+ // walk, and a bare "No runs found." would read as "this user has no
962
+ // matching runs" (#1967 review).
963
+ const printCursorHint = () => {
964
+ if (!page.nextCursor)
965
+ return;
966
+ console.log();
967
+ dim(`More runs. Use --cursor ${page.nextCursor} for the next page.`);
968
+ };
969
+ if (!items || items.length === 0) {
970
+ // #2237 — the honest empty state: a bounded filtered read can stop
971
+ // before it reaches a match, so the message says how far it got and
972
+ // how to continue instead of asserting there are none. `runs list`
973
+ // deliberately does NOT auto-resume like `runs failures` — it is a
974
+ // pager, and its `--limit` is a page size.
975
+ info(describeEmptyRunPage(page, !!options.cursor));
976
+ return;
977
+ }
978
+ console.log(formatTable(items, runsTableColumns));
979
+ printCursorHint();
468
980
  }
469
- if (!items || items.length === 0) {
470
- info("No runs found.");
471
- return;
981
+ catch (err) {
982
+ error(err.message);
983
+ process.exit(1);
472
984
  }
473
- console.log(formatTable(items, [
474
- { header: "RUN ID", key: "runId", format: formatId },
475
- { header: "STATUS", key: "status", format: formatStatus },
476
- { header: "STARTED", key: "startedAt", format: formatDate },
477
- { header: "ENDED", key: "endedAt", format: formatDate },
478
- { header: "PREVIEW", key: "isPreview", format: (v) => v ? "yes" : "" },
479
- ]));
985
+ return;
986
+ }
987
+ // Tailing (`--watch` / `--follow`). SIGINT → AbortController installed
988
+ // here at the command level; the handler is removed in `finally` so it
989
+ // never leaks past this command (unlike the persistent process.on).
990
+ let intervalSeconds;
991
+ try {
992
+ intervalSeconds = parseIntervalSeconds(options.interval);
480
993
  }
481
994
  catch (err) {
482
995
  error(err.message);
483
996
  process.exit(1);
484
997
  }
998
+ const intervalMs = intervalSeconds * 1000;
999
+ const controller = new AbortController();
1000
+ const onSigint = () => controller.abort();
1001
+ process.on("SIGINT", onSigint);
1002
+ try {
1003
+ if (options.follow) {
1004
+ const source = {
1005
+ fetchAfter: async (resumeAfter) => {
1006
+ if (resumeAfter === undefined) {
1007
+ // Bootstrap: newest position, descending, one row. followLoop
1008
+ // discards the items and keeps only the baseline checkpoint.
1009
+ const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
1010
+ follow: true,
1011
+ forward: false,
1012
+ limit: 1,
1013
+ status: options.status,
1014
+ });
1015
+ return { items: page.items, hasMore: false, resumeAfter: page.resumeAfter };
1016
+ }
1017
+ const page = await client.listWorkflowRuns(resolvedAppId, workflowId, {
1018
+ follow: true,
1019
+ forward: true,
1020
+ after: resumeAfter,
1021
+ limit,
1022
+ status: options.status,
1023
+ });
1024
+ return {
1025
+ items: page.items,
1026
+ hasMore: !!page.hasMore,
1027
+ resumeAfter: page.resumeAfter,
1028
+ };
1029
+ },
1030
+ };
1031
+ if (!options.json) {
1032
+ info("Following runs (Ctrl-C to stop)…");
1033
+ }
1034
+ await followLoop(source, {
1035
+ intervalMs,
1036
+ signal: controller.signal,
1037
+ render: (items) => {
1038
+ if (options.json) {
1039
+ // One JSON object per line (a tail has no closing bracket to
1040
+ // wait for), each in the shared inspection item shape — the
1041
+ // same items the one-shot envelope carries (behavior 7).
1042
+ for (const run of items)
1043
+ jsonLine(toWorkflowRunInspectionRow(run));
1044
+ return;
1045
+ }
1046
+ for (const run of items) {
1047
+ console.log(`${formatDate(run.startedAt)} ${formatStatus(run.status)} ${formatId(run.runId)}`);
1048
+ }
1049
+ },
1050
+ });
1051
+ }
1052
+ else {
1053
+ // --watch: periodic snapshot redraw. The most recent page's
1054
+ // pagination fields are kept so the `--json` redraw emits the same
1055
+ // envelope the one-shot path does, not a bare array (behavior 7).
1056
+ let lastPage = {};
1057
+ await watchLoop(async () => {
1058
+ const page = await fetchPage({
1059
+ status: options.status,
1060
+ limit,
1061
+ cursor: options.cursor,
1062
+ });
1063
+ lastPage = page;
1064
+ return page.items;
1065
+ }, {
1066
+ intervalMs,
1067
+ signal: controller.signal,
1068
+ render: (items) => {
1069
+ if (options.json) {
1070
+ json(buildWorkflowRunListEnvelope(items, lastPage));
1071
+ return;
1072
+ }
1073
+ // Renderer owns the redraw: clear the screen, then print.
1074
+ process.stdout.write("\x1b[2J\x1b[H");
1075
+ if (!items || items.length === 0) {
1076
+ console.log("No runs found.");
1077
+ return;
1078
+ }
1079
+ console.log(formatTable(items, runsTableColumns));
1080
+ },
1081
+ });
1082
+ }
1083
+ }
1084
+ finally {
1085
+ process.removeListener("SIGINT", onSigint);
1086
+ }
485
1087
  });
486
1088
  // Run status
487
1089
  runs
488
1090
  .command("status")
489
1091
  .description("Get status of a workflow run")
490
- .argument("<workflow-id>", "Workflow ID")
1092
+ .argument("<workflow-id>", "Workflow ID or key")
491
1093
  .argument("<run-id>", "Run ID")
492
1094
  .option("--app <app-id>", "App ID (uses current app if not specified)")
493
1095
  .option("--json", "Output as JSON")
@@ -496,310 +1098,620 @@ Examples:
496
1098
  const client = new ApiClient();
497
1099
  try {
498
1100
  const result = await client.getWorkflowRunStatus(resolvedAppId, workflowId, runId);
1101
+ // #2771 — a Cloudflare instance reports `stepResults` only once the run
1102
+ // is terminal, so a live run used to answer with `status: running` and
1103
+ // nothing else for its whole duration. Step rows are now written as the
1104
+ // run goes, so fall back to them: the steps finished so far plus the one
1105
+ // currently executing. Resolved BEFORE the output branches — a script
1106
+ // polling `--json` needs the in-flight trace as much as a human does.
1107
+ // Best-effort: a lookup failure still leaves the run details intact.
1108
+ const instanceStepResults = result.instanceStatus?.stepResults;
1109
+ let persistedStepRuns = [];
1110
+ if (!instanceStepResults || instanceStepResults.length === 0) {
1111
+ try {
1112
+ const stepRuns = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
1113
+ persistedStepRuns = stepRuns?.items || [];
1114
+ }
1115
+ catch {
1116
+ persistedStepRuns = [];
1117
+ }
1118
+ }
499
1119
  if (options.json) {
500
- json(result);
1120
+ // Purely additive: `stepRuns` appears only when the persisted rows are
1121
+ // what carries the trace, so a terminal run's JSON is unchanged.
1122
+ json(persistedStepRuns.length > 0 ? { ...result, stepRuns: persistedStepRuns } : result);
501
1123
  return;
502
1124
  }
503
1125
  const run = result.run;
504
1126
  keyValue("Run ID", run.runId);
505
1127
  keyValue("Status", formatStatus(run.status));
506
- keyValue("Started", formatDate(run.startedAt));
1128
+ // #2636 — the machine-readable outcome next to the status: why a
1129
+ // failed run failed, or why an elided run never ran.
1130
+ if (run.errorCode)
1131
+ keyValue("Error code", run.errorCode);
1132
+ if (run.skipReason)
1133
+ keyValue("Skip reason", run.skipReason);
1134
+ // #1367 — `startedAt` is the request time; `executionStartedAt` is when
1135
+ // Cloudflare actually started running the instance. Queue delay is the
1136
+ // gap. A still-queued run shows "-" for execution start / queue delay.
1137
+ keyValue("Requested", formatDate(run.startedAt));
1138
+ keyValue("Execution started", formatDate(run.executionStartedAt));
1139
+ keyValue("Queue delay", formatDuration(run.queueDelayMs));
1140
+ keyValue("Create call", formatDuration(run.createCallDurationMs));
507
1141
  keyValue("Ended", formatDate(run.endedAt));
508
1142
  keyValue("Preview", run.isPreview ? "yes" : "no");
509
- if (result.instanceStatus) {
510
- const status = result.instanceStatus;
511
- if (status.output) {
512
- divider();
513
- info("Output:");
514
- console.log(JSON.stringify(status.output, null, 2));
515
- }
516
- if (status.stepResults && status.stepResults.length > 0) {
517
- divider();
518
- info("Step Results:");
519
- status.stepResults.forEach((step) => {
520
- console.log(` ${step.id}: ${formatDuration(step.durationMs)} ${step.skipped ? "(skipped)" : ""}`);
521
- });
522
- }
523
- if (status.error) {
524
- divider();
525
- error("Error:");
526
- console.log(JSON.stringify(status.error, null, 2));
1143
+ const stepResultLines = renderRunStatusStepResults(stepResultsForRunStatus(instanceStepResults, persistedStepRuns));
1144
+ if (result.instanceStatus?.output) {
1145
+ divider();
1146
+ info("Output:");
1147
+ console.log(JSON.stringify(result.instanceStatus.output, null, 2));
1148
+ }
1149
+ if (stepResultLines.length > 0) {
1150
+ divider();
1151
+ info("Step Results:");
1152
+ for (const line of stepResultLines) {
1153
+ console.log(line);
527
1154
  }
528
1155
  }
1156
+ if (result.instanceStatus?.error) {
1157
+ divider();
1158
+ error("Error:");
1159
+ console.log(JSON.stringify(result.instanceStatus.error, null, 2));
1160
+ }
529
1161
  }
530
1162
  catch (err) {
531
1163
  error(err.message);
532
1164
  process.exit(1);
533
1165
  }
534
1166
  });
535
- // ============================================
536
- // CONFIGS SUBCOMMAND
537
- // ============================================
538
- const configs = workflows
539
- .command("configs")
540
- .description("Manage workflow configurations")
541
- .addHelpText("after", `
542
- Examples:
543
- $ primitive workflows configs list <workflow-id>
544
- $ primitive workflows configs get <workflow-id> <config-id>
545
- $ primitive workflows configs create <workflow-id> --name "v2" --from-file workflow.toml
546
- $ primitive workflows configs activate <workflow-id> <config-id>
547
- $ primitive workflows configs duplicate <workflow-id> <config-id> --name "v2-copy"
548
- `);
549
- // List configs
550
- configs
551
- .command("list")
552
- .description("List configurations for a workflow")
553
- .argument("<workflow-id>", "Workflow ID")
554
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1167
+ // Steps for a run
1168
+ runs
1169
+ .command("steps")
1170
+ .description("Show step-level details for a workflow run")
1171
+ .argument("<workflow-id>", "Workflow ID or key")
1172
+ .argument("<run-id>", "Run ID")
1173
+ .option("--app <app-id>", "App ID")
555
1174
  .option("--json", "Output as JSON")
556
- .action(async (workflowId, options) => {
1175
+ .action(async (workflowId, runId, options) => {
557
1176
  const resolvedAppId = resolveAppId(undefined, options);
558
1177
  const client = new ApiClient();
559
1178
  try {
560
- const { items } = await client.listWorkflowConfigs(resolvedAppId, workflowId);
1179
+ const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
561
1180
  if (options.json) {
562
- json(items);
1181
+ // Steps are unpaginated (one run's full trace) — the shared
1182
+ // inspection item shape wrapped in `{items}`, no cursor (behavior 6).
1183
+ json(buildWorkflowStepEnvelope(items));
563
1184
  return;
564
1185
  }
565
1186
  if (!items || items.length === 0) {
566
- info("No configurations found.");
1187
+ info("No step runs found.");
567
1188
  return;
568
1189
  }
569
- // Get workflow to find active config
570
- const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
571
- const activeConfigId = workflow?.activeConfigId;
572
- console.log(formatTable(items, [
573
- { header: "ID", key: "configId", format: formatId },
574
- { header: "NAME", key: "configName" },
575
- { header: "STATUS", key: "status", format: formatStatus },
576
- {
577
- header: "ACTIVE",
578
- key: "configId",
579
- format: (v) => v === activeConfigId ? "★" : "",
580
- },
581
- { header: "MODIFIED", key: "modifiedAt", format: formatDate },
582
- ]));
1190
+ console.log(renderStepRunTable(items));
583
1191
  }
584
1192
  catch (err) {
585
1193
  error(err.message);
586
1194
  process.exit(1);
587
1195
  }
588
1196
  });
589
- // Get config
590
- configs
591
- .command("get")
592
- .description("Get configuration details")
593
- .argument("<workflow-id>", "Workflow ID")
594
- .argument("<config-id>", "Configuration ID")
595
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1197
+ // Step detail
1198
+ runs
1199
+ .command("step-detail")
1200
+ .description("Show full details for a single step")
1201
+ .argument("<workflow-id>", "Workflow ID or key")
1202
+ .argument("<run-id>", "Run ID")
1203
+ .argument("<step-id>", "Step ID")
1204
+ .option("--app <app-id>", "App ID")
596
1205
  .option("--json", "Output as JSON")
597
- .action(async (workflowId, configId, options) => {
1206
+ .action(async (workflowId, runId, stepId, options) => {
598
1207
  const resolvedAppId = resolveAppId(undefined, options);
599
1208
  const client = new ApiClient();
600
1209
  try {
601
- const config = await client.getWorkflowConfig(resolvedAppId, workflowId, configId);
1210
+ const { items } = await client.getWorkflowStepRuns(resolvedAppId, workflowId, runId);
1211
+ const step = items.find((s) => s.stepId === stepId);
1212
+ if (!step) {
1213
+ error(`Step "${stepId}" not found in run ${runId}`);
1214
+ process.exit(1);
1215
+ }
602
1216
  if (options.json) {
603
- json(config);
1217
+ json(step);
604
1218
  return;
605
1219
  }
606
- keyValue("Config ID", config.configId);
607
- keyValue("Name", config.configName);
608
- keyValue("Description", config.description || "-");
609
- keyValue("Status", formatStatus(config.status));
610
- keyValue("Created", formatDate(config.createdAt));
611
- keyValue("Modified", formatDate(config.modifiedAt));
612
- if (config.steps && config.steps.length > 0) {
1220
+ keyValue("Step", step.stepId);
1221
+ keyValue("Kind", step.stepKind);
1222
+ keyValue("Status", formatStatus(step.status));
1223
+ keyValue("Duration", formatDuration(step.durationMs));
1224
+ keyValue("Started", formatDate(step.startedAt));
1225
+ keyValue("Ended", formatDate(step.endedAt));
1226
+ if (step.totalTokens) {
1227
+ keyValue("Tokens", `${step.inputTokens || 0} in / ${step.outputTokens || 0} out / ${step.totalTokens} total`);
1228
+ }
1229
+ if (step.retryCount) {
1230
+ keyValue("Retries", String(step.retryCount));
1231
+ }
1232
+ if (step.config) {
613
1233
  divider();
614
- info(`Steps (${config.steps.length}):`);
615
- config.steps.forEach((step, idx) => {
616
- console.log(` ${idx + 1}. ${step.id} (${step.kind})`);
1234
+ info("Config (rendered):");
1235
+ console.log(JSON.stringify(step.config, null, 2));
1236
+ }
1237
+ if (step.rawConfig) {
1238
+ divider();
1239
+ info("Config (raw):");
1240
+ console.log(JSON.stringify(step.rawConfig, null, 2));
1241
+ }
1242
+ if (step.input) {
1243
+ divider();
1244
+ info("Input:");
1245
+ console.log(JSON.stringify(step.input, null, 2));
1246
+ }
1247
+ if (step.output) {
1248
+ divider();
1249
+ info("Output:");
1250
+ console.log(JSON.stringify(step.output, null, 2));
1251
+ }
1252
+ if (step.error) {
1253
+ divider();
1254
+ error("Error: " + step.error);
1255
+ if (step.errorDetails) {
1256
+ console.log(step.errorDetails);
1257
+ }
1258
+ }
1259
+ if (step.templateWarnings && step.templateWarnings.length > 0) {
1260
+ divider();
1261
+ warn("Template Warnings:");
1262
+ step.templateWarnings.forEach((w) => {
1263
+ console.log(` ${w.expression}: ${w.message}`);
617
1264
  });
618
1265
  }
1266
+ if (step.context) {
1267
+ divider();
1268
+ info("Context Snapshot:");
1269
+ console.log(JSON.stringify(step.context, null, 2));
1270
+ }
619
1271
  }
620
1272
  catch (err) {
621
1273
  error(err.message);
622
1274
  process.exit(1);
623
1275
  }
624
1276
  });
625
- // Create config
626
- configs
627
- .command("create")
628
- .description("Create a new configuration")
629
- .argument("<workflow-id>", "Workflow ID")
1277
+ // Integration call logs for a run (cross-pivot from workflow-run to
1278
+ // integrations.logs — see issue #699).
1279
+ runs
1280
+ .command("logs")
1281
+ .description("List integration calls made by a specific workflow run (cross-pivot of `integrations logs`)")
1282
+ .argument("<run-id>", "Workflow run ID")
630
1283
  .option("--app <app-id>", "App ID (uses current app if not specified)")
631
- .option("--name <name>", "Configuration name (required)")
632
- .option("--description <desc>", "Description")
633
- .option("--from-file <path>", "Load steps from TOML file")
1284
+ .option("--limit <n>", "Number of logs to show", "100")
634
1285
  .option("--json", "Output as JSON")
635
- .action(async (workflowId, options) => {
1286
+ .action(async (runId, options) => {
636
1287
  const resolvedAppId = resolveAppId(undefined, options);
637
- if (!options.name) {
638
- error("--name is required");
1288
+ const client = new ApiClient();
1289
+ try {
1290
+ const logs = await client.listWorkflowRunIntegrationLogs(resolvedAppId, runId, { limit: parseInt(options.limit) });
1291
+ if (options.json) {
1292
+ json(logs);
1293
+ return;
1294
+ }
1295
+ if (!logs || logs.length === 0) {
1296
+ info("No integration calls found for this run.");
1297
+ return;
1298
+ }
1299
+ console.log(formatTable(logs, [
1300
+ { header: "TIME", key: "timestamp", format: formatDate },
1301
+ { header: "STEP", key: "stepId", format: (v) => v || "" },
1302
+ {
1303
+ header: "INTEGRATION",
1304
+ key: "integrationKey",
1305
+ format: (v) => v || "",
1306
+ },
1307
+ { header: "METHOD", key: "method" },
1308
+ { header: "PATH", key: "path" },
1309
+ { header: "STATUS", key: "status" },
1310
+ { header: "DURATION", key: "durationMs", format: formatDuration },
1311
+ { header: "TRACE", key: "traceId", format: formatId },
1312
+ ]));
1313
+ }
1314
+ catch (err) {
1315
+ error(err.message);
639
1316
  process.exit(1);
640
1317
  }
641
- let steps = [];
642
- if (options.fromFile) {
643
- try {
644
- const content = readFileSync(options.fromFile, "utf-8");
645
- const tomlData = TOML.parse(content);
646
- steps = tomlData.steps || [];
1318
+ });
1319
+ // Error summary
1320
+ runs
1321
+ .command("error")
1322
+ .description("Show error details for a failed workflow run")
1323
+ .argument("<workflow-id>", "Workflow ID or key")
1324
+ .argument("<run-id>", "Run ID")
1325
+ .option("--app <app-id>", "App ID")
1326
+ .option("--json", "Output as JSON")
1327
+ .action(async (workflowId, runId, options) => {
1328
+ const resolvedAppId = resolveAppId(undefined, options);
1329
+ const client = new ApiClient();
1330
+ try {
1331
+ const [runResult, stepsResult] = await Promise.all([
1332
+ client.getWorkflowRunStatus(resolvedAppId, workflowId, runId),
1333
+ client.getWorkflowStepRuns(resolvedAppId, workflowId, runId),
1334
+ ]);
1335
+ // #2074 — one selection rule shared with the STEP column of
1336
+ // `runs failures`: prefer the run's own denormalized attribution, and
1337
+ // fall back to the lowest-`stepIndex` failed row (never "whatever the
1338
+ // query returned first") so the two views can't disagree.
1339
+ const failedStep = selectFailedStepRow(stepsResult.items, runResult.run?.failedStepId);
1340
+ const errorInfo = {
1341
+ runId,
1342
+ status: runResult.run?.status,
1343
+ // #2636 — the platform's classification of the failure, when it owns
1344
+ // one (a lost declarative-lock race, an exhausted lock wait).
1345
+ errorCode: runResult.run?.errorCode ?? null,
1346
+ errorMessage: runResult.run?.errorMessage || runResult.instanceStatus?.error?.message,
1347
+ failedStep: failedStep ? {
1348
+ stepId: failedStep.stepId,
1349
+ stepKind: failedStep.stepKind,
1350
+ error: failedStep.error,
1351
+ errorDetails: failedStep.errorDetails,
1352
+ input: failedStep.input,
1353
+ config: failedStep.config,
1354
+ } : null,
1355
+ };
1356
+ if (options.json) {
1357
+ json(errorInfo);
1358
+ return;
647
1359
  }
648
- catch (err) {
649
- error(`Failed to read TOML file: ${err.message}`);
650
- process.exit(1);
1360
+ keyValue("Run ID", runId);
1361
+ keyValue("Status", formatStatus(errorInfo.status));
1362
+ if (errorInfo.errorCode) {
1363
+ keyValue("Code", errorInfo.errorCode);
1364
+ }
1365
+ if (errorInfo.errorMessage) {
1366
+ divider();
1367
+ error("Error: " + errorInfo.errorMessage);
1368
+ }
1369
+ if (failedStep) {
1370
+ divider();
1371
+ info(`Failed Step: ${failedStep.stepId} (${failedStep.stepKind})`);
1372
+ if (failedStep.error) {
1373
+ error(" " + failedStep.error);
1374
+ }
1375
+ if (failedStep.errorDetails) {
1376
+ console.log(failedStep.errorDetails);
1377
+ }
1378
+ if (failedStep.input) {
1379
+ divider();
1380
+ info("Step Input:");
1381
+ console.log(JSON.stringify(failedStep.input, null, 2));
1382
+ }
1383
+ if (failedStep.config) {
1384
+ divider();
1385
+ info("Step Config:");
1386
+ console.log(JSON.stringify(failedStep.config, null, 2));
1387
+ }
1388
+ }
1389
+ else {
1390
+ info("No failed step found in step runs.");
651
1391
  }
652
1392
  }
1393
+ catch (err) {
1394
+ error(err.message);
1395
+ process.exit(1);
1396
+ }
1397
+ });
1398
+ // Failures list
1399
+ runs
1400
+ .command("failures")
1401
+ .description("List recent workflow run failures")
1402
+ .argument("<workflow-id>", "Workflow ID or key")
1403
+ .option("--app <app-id>", "App ID")
1404
+ // #2237 — `--limit` is the number of FAILURES to collect, as the help text
1405
+ // always claimed. It used to be passed straight through as the server's
1406
+ // bounded page limit, so a workflow with a long success streak answered
1407
+ // "No failures found." while failures sat just past the scan budget.
1408
+ .option("--limit <n>", "Number of failures to collect", "10")
1409
+ .option("--max-scan <n>", `Stop after examining this many runs, to the nearest request (each examines up to ${SERVER_SCAN_ROWS_PER_REQUEST}; 0 = no cap; a sweep issues at most ${FAILURE_SWEEP_MAX_REQUESTS} requests either way)`, String(DEFAULT_FAILURE_SWEEP_MAX_SCAN))
1410
+ // `--cursor` seeds the sweep's starting position; the sweep then resumes
1411
+ // itself, so it is only needed to continue an earlier capped search.
1412
+ .option("--cursor <cursor>", "Resume the search from a previous cursor")
1413
+ .option("--json", "Output as JSON")
1414
+ .action(async (workflowId, options) => {
1415
+ const resolvedAppId = resolveAppId(undefined, options);
653
1416
  const client = new ApiClient();
1417
+ const limit = parseInt(options.limit, 10);
1418
+ const maxScan = parseInt(options.maxScan, 10);
1419
+ if (!Number.isFinite(limit) || limit < 1) {
1420
+ error("--limit must be a positive integer.");
1421
+ process.exit(1);
1422
+ }
1423
+ if (!Number.isFinite(maxScan) || maxScan < 0) {
1424
+ error("--max-scan must be a non-negative integer (0 = no cap).");
1425
+ process.exit(1);
1426
+ }
1427
+ // Ctrl-C during a multi-second sweep prints what was collected plus the
1428
+ // resume cursor rather than dropping the work on the floor. The handler
1429
+ // is removed in `finally` so it never leaks past this command.
1430
+ //
1431
+ // Registering a listener replaces Node's terminate-on-SIGINT default, and
1432
+ // `shouldStop` is only read between requests — so a first Ctrl-C during a
1433
+ // slow in-flight request would otherwise appear to do nothing and leave
1434
+ // the command unkillable. A second one exits immediately (#2237 review).
1435
+ let interrupted = false;
1436
+ let sigints = 0;
1437
+ const onSigint = () => {
1438
+ interrupted = true;
1439
+ if (++sigints > 1)
1440
+ process.exit(130);
1441
+ };
1442
+ process.on("SIGINT", onSigint);
1443
+ // Progress on stderr, TTY only, never in `--json` mode: a silent
1444
+ // multi-second command invites the Ctrl-C it is trying to survive.
1445
+ const showProgress = !options.json && !!process.stderr.isTTY;
654
1446
  try {
655
- const config = await client.createWorkflowConfig(resolvedAppId, workflowId, {
656
- configName: options.name,
657
- description: options.description,
658
- steps,
1447
+ const sweep = await collectWorkflowFailures(({ cursor, limit: pageLimit }) => client.listWorkflowRuns(resolvedAppId, workflowId, {
1448
+ status: "failed",
1449
+ limit: pageLimit,
1450
+ cursor,
1451
+ }), {
1452
+ limit,
1453
+ maxScan,
1454
+ startCursor: options.cursor,
1455
+ shouldStop: () => interrupted,
1456
+ onProgress: showProgress
1457
+ ? (scanned, collected) => progress(`Searching… ${scanned} runs examined, ${collected} failure${collected === 1 ? "" : "s"} found`)
1458
+ : undefined,
659
1459
  });
1460
+ if (showProgress && sweep.requests >= 1) {
1461
+ // Clear the progress line so it does not sit above the table. One
1462
+ // request is the common case — `onProgress` fires after EVERY request
1463
+ // including the first, so guarding on `> 1` left the line on screen
1464
+ // with the cursor parked at its end (#2237 review).
1465
+ process.stderr.write("\r\x1b[K");
1466
+ }
1467
+ // A SIGINT that arrived after the sweep returned (while the table is
1468
+ // rendering) did not truncate anything, so only a sweep that actually
1469
+ // stopped on the interrupt reports 130. `exitCode` rather than
1470
+ // `process.exit()` so a piped stdout is not truncated mid-write.
1471
+ const exitInterrupted = sweep.stoppedBecause === "interrupted";
1472
+ const items = sweep.items;
660
1473
  if (options.json) {
661
- json(config);
1474
+ // #2074 — the shared inspection item shape inside the endpoint's own
1475
+ // #1316 envelope, never a bare array. #2237 — `items` is the
1476
+ // concatenation across every resumed page, `scanned` their sum, and
1477
+ // `nextCursor` the last one observed.
1478
+ json(buildWorkflowRunEnvelope(items, {
1479
+ nextCursor: sweep.nextCursor,
1480
+ hasMore: !!sweep.nextCursor,
1481
+ ...(sweep.scannedKnown ? { scanned: sweep.scanned } : {}),
1482
+ }));
1483
+ if (exitInterrupted)
1484
+ process.exitCode = 130;
662
1485
  return;
663
1486
  }
664
- success(`Configuration created: ${config.configName}`);
665
- keyValue("Config ID", config.configId);
1487
+ if (items.length > 0) {
1488
+ console.log(formatTable(items, [
1489
+ { header: "RUN ID", key: "runId", format: formatId },
1490
+ // STEP sits before ERROR so the truncated message stays the last
1491
+ // wide column and the step id is never the thing that gets cut.
1492
+ // `-` is a routine outcome, not a bug: launch-time failures
1493
+ // (create/schema/R2 aborts, reclaimed runs) legitimately have no
1494
+ // failed step, and runs that predate #2074 carry no attribution.
1495
+ { header: "STEP", key: "failedStepId", format: formatFailureStepCell },
1496
+ // #2636 — the platform's classification, next to the message an
1497
+ // operator would otherwise have had to pattern-match.
1498
+ { header: "CODE", key: "errorCode", format: formatFailureCodeCell },
1499
+ { header: "ERROR", key: "errorMessage", format: formatFailureErrorCell },
1500
+ { header: "STARTED", key: "startedAt", format: formatDate },
1501
+ { header: "ENDED", key: "endedAt", format: formatDate },
1502
+ ]));
1503
+ }
1504
+ // #2237 — replaces the unconditional "No failures found.": every form
1505
+ // either states the search was complete or says how far it got and how
1506
+ // to continue. A triage command must never assert an absence it did
1507
+ // not establish.
1508
+ const summary = describeFailureSweep(sweep, limit);
1509
+ if (summary) {
1510
+ if (items.length > 0)
1511
+ console.log();
1512
+ info(summary);
1513
+ }
1514
+ if (exitInterrupted)
1515
+ process.exitCode = 130;
666
1516
  }
667
1517
  catch (err) {
668
1518
  error(err.message);
669
1519
  process.exit(1);
670
1520
  }
1521
+ finally {
1522
+ process.removeListener("SIGINT", onSigint);
1523
+ }
671
1524
  });
672
- // Update config
673
- configs
674
- .command("update")
675
- .description("Update a configuration")
676
- .argument("<workflow-id>", "Workflow ID")
677
- .argument("<config-id>", "Configuration ID")
1525
+ // ── Iterations: inspect / reset iterate-users singleton iterations (#1209) ──
1526
+ const iterations = workflows
1527
+ .command("iterations")
1528
+ .description("Inspect and reset iterate-users iterations");
1529
+ iterations
1530
+ .command("list")
1531
+ .description("List iterate-users iterations for the app")
678
1532
  .option("--app <app-id>", "App ID (uses current app if not specified)")
679
- .option("--name <name>", "Configuration name")
680
- .option("--description <desc>", "Description")
681
- .option("--from-file <path>", "Update steps from TOML file")
682
1533
  .option("--json", "Output as JSON")
683
- .action(async (workflowId, configId, options) => {
1534
+ .action(async (options) => {
684
1535
  const resolvedAppId = resolveAppId(undefined, options);
685
- const payload = {};
686
- if (options.name)
687
- payload.configName = options.name;
688
- if (options.description !== undefined)
689
- payload.description = options.description;
690
- if (options.fromFile) {
691
- try {
692
- const content = readFileSync(options.fromFile, "utf-8");
693
- const tomlData = TOML.parse(content);
694
- payload.steps = tomlData.steps || [];
1536
+ const client = new ApiClient();
1537
+ try {
1538
+ const { items } = await client.listIterations(resolvedAppId);
1539
+ if (options.json) {
1540
+ json(wholeListEnvelope(items));
1541
+ return;
695
1542
  }
696
- catch (err) {
697
- error(`Failed to read TOML file: ${err.message}`);
698
- process.exit(1);
1543
+ if (!items || items.length === 0) {
1544
+ info("No iterations found.");
1545
+ return;
699
1546
  }
1547
+ console.log(formatTable(items, [
1548
+ { header: "ITERATION", key: "iterationName" },
1549
+ { header: "STATUS", key: "status", format: formatStatus },
1550
+ { header: "ACQUIRED", key: "lastAcquireMode", format: (v) => v || "" },
1551
+ { header: "PROCESSED", key: "totalProcessed" },
1552
+ { header: "FAILED", key: "failedCount" },
1553
+ { header: "LAST RUN", key: "lastRunAt", format: formatDate },
1554
+ ]));
700
1555
  }
701
- if (Object.keys(payload).length === 0) {
702
- error("No update options specified. Use --name, --description, or --from-file.");
1556
+ catch (err) {
1557
+ error(err.message);
703
1558
  process.exit(1);
704
1559
  }
1560
+ });
1561
+ iterations
1562
+ .command("get")
1563
+ .description("Show details for a single iterate-users iteration")
1564
+ .argument("<name>", "Iteration name")
1565
+ .option("--app <app-id>", "App ID (uses current app if not specified)")
1566
+ .option("--json", "Output as JSON")
1567
+ .action(async (name, options) => {
1568
+ const resolvedAppId = resolveAppId(undefined, options);
705
1569
  const client = new ApiClient();
706
1570
  try {
707
- const config = await client.updateWorkflowConfig(resolvedAppId, workflowId, configId, payload);
1571
+ const it = await client.getIteration(resolvedAppId, name);
708
1572
  if (options.json) {
709
- json(config);
1573
+ json(it);
710
1574
  return;
711
1575
  }
712
- success("Configuration updated.");
713
- keyValue("Config ID", config.configId);
714
- keyValue("Name", config.configName);
1576
+ keyValue("Iteration", it.iterationName);
1577
+ keyValue("Status", formatStatus(it.status));
1578
+ keyValue("Acquired as", it.lastAcquireMode || "(unknown)");
1579
+ keyValue("Run ID", it.runId);
1580
+ keyValue("Instance ID", it.instanceId);
1581
+ keyValue("Started", formatDate(it.startedAt));
1582
+ keyValue("Last run", formatDate(it.lastRunAt));
1583
+ keyValue("Lock expires", formatDate(it.lockExpiresAt));
1584
+ keyValue("Processed", String(it.totalProcessed ?? 0));
1585
+ keyValue("Succeeded", String(it.succeededCount ?? 0));
1586
+ keyValue("Failed (total)", String(it.failedCount ?? 0));
1587
+ keyValue("Skipped", String(it.skippedCount ?? 0));
1588
+ if (it.lastError) {
1589
+ keyValue("Last error", it.lastError);
1590
+ }
1591
+ const sample = Array.isArray(it.failedUserIdsSample)
1592
+ ? it.failedUserIdsSample
1593
+ : [];
1594
+ if (sample.length > 0) {
1595
+ const label = it.failedUserIdsTruncated
1596
+ ? `Failed user IDs (sample, truncated; ${it.failedCount} total)`
1597
+ : `Failed user IDs (sample)`;
1598
+ keyValue(label, sample.join(", "));
1599
+ }
715
1600
  }
716
1601
  catch (err) {
717
1602
  error(err.message);
718
1603
  process.exit(1);
719
1604
  }
720
1605
  });
721
- // Activate config
722
- configs
723
- .command("activate")
724
- .description("Set a configuration as the active (default) configuration")
725
- .argument("<workflow-id>", "Workflow ID")
726
- .argument("<config-id>", "Configuration ID")
1606
+ iterations
1607
+ .command("reset")
1608
+ .description("Reset a terminal (completed/failed) iteration so the next trigger runs fresh")
1609
+ .argument("<name>", "Iteration name")
727
1610
  .option("--app <app-id>", "App ID (uses current app if not specified)")
728
1611
  .option("--json", "Output as JSON")
729
- .action(async (workflowId, configId, options) => {
1612
+ .action(async (name, options) => {
730
1613
  const resolvedAppId = resolveAppId(undefined, options);
731
1614
  const client = new ApiClient();
732
1615
  try {
733
- const result = await client.activateWorkflowConfig(resolvedAppId, workflowId, configId);
1616
+ const res = await client.resetIteration(resolvedAppId, name);
734
1617
  if (options.json) {
735
- json(result);
1618
+ json(res);
736
1619
  return;
737
1620
  }
738
- success("Configuration activated.");
739
- keyValue("Active Config ID", result.activeConfigId || configId);
1621
+ success(`Reset iteration "${name}" (was ${res.previousStatus}). The next trigger will run fresh.`);
740
1622
  }
741
1623
  catch (err) {
1624
+ // A running iteration is refused with 409 + lock metadata — surface a
1625
+ // clear message rather than a raw error.
742
1626
  error(err.message);
743
1627
  process.exit(1);
744
1628
  }
745
1629
  });
746
- // Duplicate config
1630
+ // ============================================
1631
+ // CONFIGS SUBCOMMAND
1632
+ // ============================================
1633
+ const configs = workflows
1634
+ .command("configs")
1635
+ .description("Manage workflow configurations")
1636
+ .addHelpText("after", `
1637
+ Examples:
1638
+ $ primitive workflows configs list <workflow-id>
1639
+ $ primitive workflows configs get <workflow-id> <config-id>
1640
+
1641
+ Config bodies are authored as files, not verbs: write
1642
+ workflows/<key>.configs/<name>.toml and name the live one with
1643
+ activeConfigName in workflows/<key>.toml, then run 'primitive config push'.
1644
+ `);
1645
+ // List configs
747
1646
  configs
748
- .command("duplicate")
749
- .description("Duplicate a configuration")
750
- .argument("<workflow-id>", "Workflow ID")
751
- .argument("<config-id>", "Configuration ID to duplicate")
1647
+ .command("list")
1648
+ .description("List configurations for a workflow")
1649
+ .argument("<workflow-id>", "Workflow ID or key")
752
1650
  .option("--app <app-id>", "App ID (uses current app if not specified)")
753
- .option("--name <name>", "Name for the new configuration")
754
1651
  .option("--json", "Output as JSON")
755
- .action(async (workflowId, configId, options) => {
1652
+ .action(async (workflowId, options) => {
756
1653
  const resolvedAppId = resolveAppId(undefined, options);
757
1654
  const client = new ApiClient();
758
1655
  try {
759
- const config = await client.duplicateWorkflowConfig(resolvedAppId, workflowId, configId, {
760
- configName: options.name,
761
- });
1656
+ const { items } = await client.listWorkflowConfigs(resolvedAppId, workflowId);
762
1657
  if (options.json) {
763
- json(config);
1658
+ json(wholeListEnvelope(items));
1659
+ return;
1660
+ }
1661
+ if (!items || items.length === 0) {
1662
+ info("No configurations found.");
764
1663
  return;
765
1664
  }
766
- success(`Configuration duplicated: ${config.configName}`);
767
- keyValue("New Config ID", config.configId);
1665
+ // Get workflow to find active config
1666
+ const { workflow } = await client.getWorkflow(resolvedAppId, workflowId);
1667
+ const activeConfigId = workflow?.activeConfigId;
1668
+ console.log(formatTable(items, [
1669
+ { header: "ID", key: "configId", format: formatId },
1670
+ { header: "NAME", key: "configName" },
1671
+ { header: "STATUS", key: "status", format: formatStatus },
1672
+ {
1673
+ header: "ACTIVE",
1674
+ key: "configId",
1675
+ format: (v) => v === activeConfigId ? "★" : "",
1676
+ },
1677
+ { header: "MODIFIED", key: "modifiedAt", format: formatDate },
1678
+ ]));
768
1679
  }
769
1680
  catch (err) {
770
1681
  error(err.message);
771
1682
  process.exit(1);
772
1683
  }
773
1684
  });
774
- // Archive config
1685
+ // Get config
775
1686
  configs
776
- .command("archive")
777
- .description("Archive a configuration")
778
- .argument("<workflow-id>", "Workflow ID")
1687
+ .command("get")
1688
+ .description("Get configuration details")
1689
+ .argument("<workflow-id>", "Workflow ID or key")
779
1690
  .argument("<config-id>", "Configuration ID")
780
1691
  .option("--app <app-id>", "App ID (uses current app if not specified)")
781
- .option("-y, --yes", "Skip confirmation prompt")
1692
+ .option("--json", "Output as JSON")
782
1693
  .action(async (workflowId, configId, options) => {
783
1694
  const resolvedAppId = resolveAppId(undefined, options);
784
- if (!options.yes) {
785
- const inquirer = await import("inquirer");
786
- const { confirm } = await inquirer.default.prompt([
787
- {
788
- type: "confirm",
789
- name: "confirm",
790
- message: `Are you sure you want to archive configuration ${configId}?`,
791
- default: false,
792
- },
793
- ]);
794
- if (!confirm) {
795
- info("Cancelled.");
796
- return;
797
- }
798
- }
799
1695
  const client = new ApiClient();
800
1696
  try {
801
- await client.archiveWorkflowConfig(resolvedAppId, workflowId, configId);
802
- success("Configuration archived.");
1697
+ const config = await client.getWorkflowConfig(resolvedAppId, workflowId, configId);
1698
+ if (options.json) {
1699
+ json(config);
1700
+ return;
1701
+ }
1702
+ printResult("Config ID", config.configId);
1703
+ printResult("Name", config.configName);
1704
+ printResult("Description", config.description || "-");
1705
+ printResult("Status", formatStatus(config.status));
1706
+ printResult("Created", formatDate(config.createdAt));
1707
+ printResult("Modified", formatDate(config.modifiedAt));
1708
+ if (config.steps && config.steps.length > 0) {
1709
+ divider();
1710
+ info(`Steps (${config.steps.length}):`);
1711
+ config.steps.forEach((step, idx) => {
1712
+ console.log(` ${idx + 1}. ${step.id} (${step.kind})`);
1713
+ });
1714
+ }
803
1715
  }
804
1716
  catch (err) {
805
1717
  error(err.message);
@@ -811,33 +1723,50 @@ Examples:
811
1723
  // ============================================
812
1724
  const tests = workflows
813
1725
  .command("tests")
814
- .description("Manage and run workflow test cases")
1726
+ .description("Run and inspect workflow test cases")
815
1727
  .addHelpText("after", `
1728
+ Test cases are authored in TOML at workflows/<key>.tests/<name>.toml and applied
1729
+ with \`primitive config push\` (issue #2769); \`config fields workflow\` lists the
1730
+ fields. Deleting a case is removing its file and running \`config push --prune\`.
1731
+
1732
+ Every verb here takes the workflow's KEY or its ID (#3249). An identifier
1733
+ that names no workflow in the app fails; "No test cases found." only ever
1734
+ means the workflow exists and has no cases.
1735
+
816
1736
  Examples:
817
- $ primitive workflows tests list <workflow-id>
818
- $ primitive workflows tests create <workflow-id> --name "Basic test" --vars '{"input":"hello"}'
819
- $ primitive workflows tests run <workflow-id> <test-case-id>
820
- $ primitive workflows tests run-all <workflow-id>
821
- $ primitive workflows tests run-all <workflow-id> --test-cases 01ABC,01DEF,01GHI
1737
+ $ primitive workflows tests list <workflow>
1738
+ $ primitive workflows tests run <workflow> <test-case-id>
1739
+ $ primitive workflows tests run-all <workflow>
1740
+ $ primitive workflows tests run-all <workflow> --test-cases 01ABC,01DEF,01GHI
822
1741
  `);
823
- // List test cases
824
1742
  tests
825
1743
  .command("list")
826
1744
  .description("List test cases for a workflow")
827
- .argument("<workflow-id>", "Workflow ID")
1745
+ .argument("<workflow>", "Workflow key or ID")
828
1746
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1747
+ .addOption(pageLimitOption())
1748
+ .addOption(pageCursorOption())
829
1749
  .option("--json", "Output as JSON")
830
- .action(async (workflowId, options) => {
1750
+ .action(async (workflow, options) => {
1751
+ const limit = parsePageLimit(options.limit);
831
1752
  const resolvedAppId = resolveAppId(undefined, options);
832
1753
  const client = new ApiClient();
833
1754
  try {
834
- const { items } = await client.listTestCases(resolvedAppId, "workflow", workflowId);
1755
+ // #3249 — resolve BEFORE listing: the test-case endpoint answers an
1756
+ // unknown block with an empty page, so an unresolved identifier would
1757
+ // print "No test cases found." and exit 0.
1758
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1759
+ const page = normalizeCliListEnvelope(await client.listTestCases(resolvedAppId, "workflow", workflowId, {
1760
+ limit,
1761
+ cursor: options.cursor,
1762
+ }));
1763
+ const items = page.items;
835
1764
  if (options.json) {
836
- json(items);
1765
+ json(page);
837
1766
  return;
838
1767
  }
839
- if (!items || items.length === 0) {
840
- info("No test cases found.");
1768
+ if (items.length === 0) {
1769
+ printEmptyPage(page, "test cases");
841
1770
  return;
842
1771
  }
843
1772
  console.log(formatTable(items, [
@@ -845,73 +1774,7 @@ Examples:
845
1774
  { header: "NAME", key: "name" },
846
1775
  { header: "CREATED", key: "createdAt", format: formatDate },
847
1776
  ]));
848
- }
849
- catch (err) {
850
- error(err.message);
851
- process.exit(1);
852
- }
853
- });
854
- // Create test case
855
- tests
856
- .command("create")
857
- .description("Create a test case for a workflow")
858
- .argument("<workflow-id>", "Workflow ID")
859
- .option("--app <app-id>", "App ID (uses current app if not specified)")
860
- .option("--name <name>", "Test case name (required)")
861
- .option("--vars <json>", "Input variables as JSON (required)")
862
- .option("--pattern <regex>", "Expected output pattern (regex)")
863
- .option("--contains <json>", "Expected strings to contain (JSON array)")
864
- .option("--json-subset <json>", "Expected JSON subset to match")
865
- .option("--json", "Output as JSON")
866
- .action(async (workflowId, options) => {
867
- const resolvedAppId = resolveAppId(undefined, options);
868
- if (!options.name || !options.vars) {
869
- error("Required: --name and --vars");
870
- process.exit(1);
871
- }
872
- let inputVariables;
873
- try {
874
- inputVariables = JSON.parse(options.vars);
875
- }
876
- catch {
877
- error("Invalid JSON in --vars");
878
- process.exit(1);
879
- }
880
- let expectedOutputContains;
881
- if (options.contains) {
882
- try {
883
- expectedOutputContains = JSON.parse(options.contains);
884
- }
885
- catch {
886
- error("Invalid JSON in --contains");
887
- process.exit(1);
888
- }
889
- }
890
- let expectedJsonSubset;
891
- if (options.jsonSubset) {
892
- try {
893
- expectedJsonSubset = JSON.parse(options.jsonSubset);
894
- }
895
- catch {
896
- error("Invalid JSON in --json-subset");
897
- process.exit(1);
898
- }
899
- }
900
- const client = new ApiClient();
901
- try {
902
- const result = await client.createTestCase(resolvedAppId, "workflow", workflowId, {
903
- name: options.name,
904
- inputVariables,
905
- expectedOutputPattern: options.pattern,
906
- expectedOutputContains,
907
- expectedJsonSubset,
908
- });
909
- if (options.json) {
910
- json(result);
911
- return;
912
- }
913
- success(`Test case created: ${result.name}`);
914
- keyValue("Test Case ID", result.testCaseId);
1777
+ printPageHint(page);
915
1778
  }
916
1779
  catch (err) {
917
1780
  error(err.message);
@@ -922,26 +1785,29 @@ Examples:
922
1785
  tests
923
1786
  .command("get")
924
1787
  .description("Get test case details")
925
- .argument("<workflow-id>", "Workflow ID")
1788
+ .argument("<workflow>", "Workflow key or ID")
926
1789
  .argument("<test-case-id>", "Test Case ID")
927
1790
  .option("--app <app-id>", "App ID (uses current app if not specified)")
928
1791
  .option("--json", "Output as JSON")
929
- .action(async (workflowId, testCaseId, options) => {
1792
+ .action(async (workflow, testCaseId, options) => {
930
1793
  const resolvedAppId = resolveAppId(undefined, options);
931
1794
  const client = new ApiClient();
932
1795
  try {
1796
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
933
1797
  const result = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
934
1798
  if (options.json) {
935
1799
  json(result);
936
1800
  return;
937
1801
  }
938
- keyValue("Test Case ID", result.testCaseId);
939
- keyValue("Name", result.name);
940
- keyValue("Created", formatDate(result.createdAt));
1802
+ printResult("Test Case ID", result.testCaseId);
1803
+ printResult("Name", result.name);
1804
+ printResult("Created", formatDate(result.createdAt));
941
1805
  divider();
942
1806
  info("Input Variables:");
943
1807
  try {
944
- const vars = JSON.parse(result.inputVariables || "{}");
1808
+ const vars = typeof result.inputVariables === 'string'
1809
+ ? JSON.parse(result.inputVariables || "{}")
1810
+ : (result.inputVariables || {});
945
1811
  console.log(JSON.stringify(vars, null, 2));
946
1812
  }
947
1813
  catch {
@@ -949,13 +1815,16 @@ Examples:
949
1815
  }
950
1816
  if (result.expectedOutputPattern) {
951
1817
  divider();
952
- keyValue("Expected Pattern", result.expectedOutputPattern);
1818
+ printResult("Expected Pattern", result.expectedOutputPattern);
953
1819
  }
954
1820
  if (result.expectedOutputContains) {
955
1821
  divider();
956
1822
  info("Expected Contains:");
957
1823
  try {
958
- console.log(JSON.stringify(JSON.parse(result.expectedOutputContains), null, 2));
1824
+ const contains = typeof result.expectedOutputContains === 'string'
1825
+ ? JSON.parse(result.expectedOutputContains)
1826
+ : result.expectedOutputContains;
1827
+ console.log(JSON.stringify(contains, null, 2));
959
1828
  }
960
1829
  catch {
961
1830
  console.log(result.expectedOutputContains);
@@ -965,7 +1834,10 @@ Examples:
965
1834
  divider();
966
1835
  info("Expected JSON Subset:");
967
1836
  try {
968
- console.log(JSON.stringify(JSON.parse(result.expectedJsonSubset), null, 2));
1837
+ const subset = typeof result.expectedJsonSubset === 'string'
1838
+ ? JSON.parse(result.expectedJsonSubset)
1839
+ : result.expectedJsonSubset;
1840
+ console.log(JSON.stringify(subset, null, 2));
969
1841
  }
970
1842
  catch {
971
1843
  console.log(result.expectedJsonSubset);
@@ -977,140 +1849,25 @@ Examples:
977
1849
  process.exit(1);
978
1850
  }
979
1851
  });
980
- // Update test case
981
- tests
982
- .command("update")
983
- .description("Update a test case")
984
- .argument("<workflow-id>", "Workflow ID")
985
- .argument("<test-case-id>", "Test Case ID")
986
- .option("--app <app-id>", "App ID (uses current app if not specified)")
987
- .option("--name <name>", "Test case name")
988
- .option("--vars <json>", "Input variables as JSON")
989
- .option("--pattern <regex>", "Expected output pattern (regex)")
990
- .option("--contains <json>", "Expected strings to contain (JSON array)")
991
- .option("--json-subset <json>", "Expected JSON subset to match")
992
- .option("--clear-pattern", "Clear expected pattern")
993
- .option("--clear-contains", "Clear expected contains")
994
- .option("--clear-json-subset", "Clear expected JSON subset")
995
- .option("--json", "Output as JSON")
996
- .action(async (workflowId, testCaseId, options) => {
997
- const resolvedAppId = resolveAppId(undefined, options);
998
- const payload = {};
999
- if (options.name)
1000
- payload.name = options.name;
1001
- if (options.vars) {
1002
- try {
1003
- payload.inputVariables = JSON.parse(options.vars);
1004
- }
1005
- catch {
1006
- error("Invalid JSON in --vars");
1007
- process.exit(1);
1008
- }
1009
- }
1010
- if (options.clearPattern) {
1011
- payload.expectedOutputPattern = null;
1012
- }
1013
- else if (options.pattern) {
1014
- payload.expectedOutputPattern = options.pattern;
1015
- }
1016
- if (options.clearContains) {
1017
- payload.expectedOutputContains = null;
1018
- }
1019
- else if (options.contains) {
1020
- try {
1021
- payload.expectedOutputContains = JSON.parse(options.contains);
1022
- }
1023
- catch {
1024
- error("Invalid JSON in --contains");
1025
- process.exit(1);
1026
- }
1027
- }
1028
- if (options.clearJsonSubset) {
1029
- payload.expectedJsonSubset = null;
1030
- }
1031
- else if (options.jsonSubset) {
1032
- try {
1033
- payload.expectedJsonSubset = JSON.parse(options.jsonSubset);
1034
- }
1035
- catch {
1036
- error("Invalid JSON in --json-subset");
1037
- process.exit(1);
1038
- }
1039
- }
1040
- if (Object.keys(payload).length === 0) {
1041
- error("No update options specified.");
1042
- process.exit(1);
1043
- }
1044
- const client = new ApiClient();
1045
- try {
1046
- const result = await client.updateTestCase(resolvedAppId, "workflow", workflowId, testCaseId, payload);
1047
- if (options.json) {
1048
- json(result);
1049
- return;
1050
- }
1051
- success("Test case updated.");
1052
- }
1053
- catch (err) {
1054
- error(err.message);
1055
- process.exit(1);
1056
- }
1057
- });
1058
- // Delete test case
1059
- tests
1060
- .command("delete")
1061
- .description("Delete a test case")
1062
- .argument("<workflow-id>", "Workflow ID")
1063
- .argument("<test-case-id>", "Test Case ID")
1064
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1065
- .option("-y, --yes", "Skip confirmation prompt")
1066
- .action(async (workflowId, testCaseId, options) => {
1067
- const resolvedAppId = resolveAppId(undefined, options);
1068
- if (!options.yes) {
1069
- const inquirer = await import("inquirer");
1070
- const { confirm } = await inquirer.default.prompt([
1071
- {
1072
- type: "confirm",
1073
- name: "confirm",
1074
- message: `Are you sure you want to delete test case ${testCaseId}?`,
1075
- default: false,
1076
- },
1077
- ]);
1078
- if (!confirm) {
1079
- info("Cancelled.");
1080
- return;
1081
- }
1082
- }
1083
- const client = new ApiClient();
1084
- try {
1085
- await client.deleteTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
1086
- success("Test case deleted.");
1087
- }
1088
- catch (err) {
1089
- error(err.message);
1090
- process.exit(1);
1091
- }
1092
- });
1093
1852
  // Run a single test case
1094
1853
  tests
1095
1854
  .command("run")
1096
1855
  .description("Run a single test case")
1097
- .argument("<workflow-id>", "Workflow ID")
1856
+ .argument("<workflow>", "Workflow key or ID")
1098
1857
  .argument("<test-case-id>", "Test Case ID")
1099
1858
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1100
1859
  .option("--json", "Output as JSON")
1101
- .action(async (workflowId, testCaseId, options) => {
1860
+ .action(async (workflow, testCaseId, options) => {
1102
1861
  const resolvedAppId = resolveAppId(undefined, options);
1103
1862
  const client = new ApiClient();
1104
1863
  try {
1864
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1105
1865
  // Get test case to retrieve its variables
1106
1866
  const testCase = await client.getTestCase(resolvedAppId, "workflow", workflowId, testCaseId);
1107
- let variables = {};
1108
- try {
1109
- variables = JSON.parse(testCase.inputVariables || "{}");
1110
- }
1111
- catch {
1112
- // Use empty object if parsing fails
1113
- }
1867
+ // #3158 — the case's stored input IS the run's input, and the server
1868
+ // hands it back parsed; `JSON.parse` on that object threw and the run
1869
+ // went ahead with `{}`, disagreeing with `run-all` over the same case.
1870
+ const variables = requireTestCaseInputVariables(testCase.inputVariables, testCaseId);
1114
1871
  const result = await client.executeBlockTest(resolvedAppId, "workflow", workflowId, {
1115
1872
  variables,
1116
1873
  testCaseId,
@@ -1163,14 +1920,17 @@ Examples:
1163
1920
  tests
1164
1921
  .command("run-all")
1165
1922
  .description("Run all test cases for a workflow, or a specific subset")
1166
- .argument("<workflow-id>", "Workflow ID")
1923
+ .argument("<workflow>", "Workflow key or ID")
1167
1924
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1168
1925
  .option("--test-cases <ids>", "Comma-separated list of test case IDs to run (runs only these)")
1169
1926
  .option("--json", "Output as JSON")
1170
- .action(async (workflowId, options) => {
1927
+ .action(async (workflow, options) => {
1171
1928
  const resolvedAppId = resolveAppId(undefined, options);
1172
1929
  const client = new ApiClient();
1173
1930
  try {
1931
+ // #3249 — resolve first, so `Total Tests: 0` can only ever mean the
1932
+ // workflow exists and has no cases, never that the identifier was wrong.
1933
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1174
1934
  // Parse test case IDs if provided
1175
1935
  let testCaseIds;
1176
1936
  if (options.testCases) {
@@ -1227,15 +1987,16 @@ Examples:
1227
1987
  tests
1228
1988
  .command("runs")
1229
1989
  .description("List recent test runs for a workflow")
1230
- .argument("<workflow-id>", "Workflow ID")
1990
+ .argument("<workflow>", "Workflow key or ID")
1231
1991
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1232
1992
  .option("--limit <n>", "Maximum number of runs to show", "20")
1233
1993
  .option("--group <group>", "Filter by comparison group")
1234
1994
  .option("--json", "Output as JSON")
1235
- .action(async (workflowId, options) => {
1995
+ .action(async (workflow, options) => {
1236
1996
  const resolvedAppId = resolveAppId(undefined, options);
1237
1997
  const client = new ApiClient();
1238
1998
  try {
1999
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1239
2000
  const { items } = await client.listTestRuns(resolvedAppId, "workflow", workflowId, {
1240
2001
  limit: parseInt(options.limit),
1241
2002
  comparisonGroup: options.group,
@@ -1277,23 +2038,24 @@ Examples:
1277
2038
  .description("Run tests in parallel using workflow-based execution")
1278
2039
  .addHelpText("after", `
1279
2040
  Examples:
1280
- $ primitive workflows tests batch start <workflow-id>
1281
- $ primitive workflows tests batch start <workflow-id> --test-cases 01ABC,01DEF
1282
- $ primitive workflows tests batch status <workflow-id> <batch-id>
1283
- $ primitive workflows tests batch cancel <workflow-id> <batch-id>
2041
+ $ primitive workflows tests batch start <workflow>
2042
+ $ primitive workflows tests batch start <workflow> --test-cases 01ABC,01DEF
2043
+ $ primitive workflows tests batch status <workflow> <batch-id>
2044
+ $ primitive workflows tests batch cancel <workflow> <batch-id>
1284
2045
  `);
1285
2046
  // Start batch tests
1286
2047
  batch
1287
2048
  .command("start")
1288
2049
  .description("Start parallel batch test execution")
1289
- .argument("<workflow-id>", "Workflow ID")
2050
+ .argument("<workflow>", "Workflow key or ID")
1290
2051
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1291
2052
  .option("--test-cases <ids>", "Comma-separated list of test case IDs to run")
1292
2053
  .option("--json", "Output as JSON")
1293
- .action(async (workflowId, options) => {
2054
+ .action(async (workflow, options) => {
1294
2055
  const resolvedAppId = resolveAppId(undefined, options);
1295
2056
  const client = new ApiClient();
1296
2057
  try {
2058
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1297
2059
  let testCaseIds;
1298
2060
  if (options.testCases) {
1299
2061
  testCaseIds = options.testCases.split(",").map((id) => id.trim()).filter(Boolean);
@@ -1329,27 +2091,37 @@ Examples:
1329
2091
  batch
1330
2092
  .command("status")
1331
2093
  .description("Get status of a batch test execution")
1332
- .argument("<workflow-id>", "Workflow ID")
2094
+ .argument("<workflow>", "Workflow key or ID")
1333
2095
  .argument("<batch-id>", "Batch ID")
1334
2096
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1335
2097
  .option("--wait", "Wait for completion (polls every 2 seconds)")
1336
2098
  .option("--json", "Output as JSON")
1337
- .action(async (workflowId, batchId, options) => {
2099
+ .action(async (workflow, batchId, options) => {
1338
2100
  const resolvedAppId = resolveAppId(undefined, options);
1339
2101
  const client = new ApiClient();
1340
2102
  try {
2103
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1341
2104
  const fetchStatus = async () => {
1342
2105
  return client.getBatchTestStatus(resolvedAppId, "workflow", workflowId, batchId);
1343
2106
  };
1344
2107
  let result = await fetchStatus();
1345
2108
  if (options.wait && result.status === "running") {
1346
2109
  info("Waiting for batch completion...");
1347
- while (result.status === "running") {
1348
- await new Promise((r) => setTimeout(r, 2000));
1349
- result = await fetchStatus();
1350
- process.stdout.write(`\r Completed: ${result.completed}/${result.results?.length || 0} `);
1351
- }
1352
- console.log();
2110
+ // #1969 — shared poll primitive (delay-then-probe every 2s, honoring
2111
+ // an AbortSignal). `onError: "throw"` preserves the prior loop's
2112
+ // fail-fast behavior: a throwing fetchStatus (expired token, deleted
2113
+ // batch → 401/404) propagates to the command-level catch below and
2114
+ // exits non-zero, rather than spinning forever with a frozen progress
2115
+ // line. (The follow/watch tail loops legitimately keep "continue".)
2116
+ const controller = new AbortController();
2117
+ result = await pollUntil(fetchStatus, (r) => r.status !== "running", {
2118
+ intervalMs: 2000,
2119
+ signal: controller.signal,
2120
+ onError: "throw",
2121
+ // Progress goes to stderr so it can't corrupt JSON on stdout under --json.
2122
+ onValue: (r) => progress(` Completed: ${r.completed}/${r.results?.length || 0} `),
2123
+ });
2124
+ progressEnd();
1353
2125
  }
1354
2126
  if (options.json) {
1355
2127
  json(result);
@@ -1385,23 +2157,22 @@ Examples:
1385
2157
  batch
1386
2158
  .command("cancel")
1387
2159
  .description("Cancel a running batch test execution")
1388
- .argument("<workflow-id>", "Workflow ID")
2160
+ .argument("<workflow>", "Workflow key or ID")
1389
2161
  .argument("<batch-id>", "Batch ID")
1390
2162
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1391
2163
  .option("-y, --yes", "Skip confirmation prompt")
1392
2164
  .option("--json", "Output as JSON")
1393
- .action(async (workflowId, batchId, options) => {
2165
+ .action(async (workflow, batchId, options) => {
1394
2166
  const resolvedAppId = resolveAppId(undefined, options);
1395
2167
  if (!options.yes) {
1396
- const inquirer = await import("inquirer");
1397
- const { confirm } = await inquirer.default.prompt([
1398
- {
1399
- type: "confirm",
1400
- name: "confirm",
1401
- message: `Are you sure you want to cancel batch ${batchId}?`,
1402
- default: false,
1403
- },
1404
- ]);
2168
+ let confirm;
2169
+ try {
2170
+ confirm = await confirmPrompt(`Are you sure you want to cancel batch ${batchId}?`);
2171
+ }
2172
+ catch (err) {
2173
+ error(err.message);
2174
+ process.exit(1);
2175
+ }
1405
2176
  if (!confirm) {
1406
2177
  info("Cancelled.");
1407
2178
  return;
@@ -1409,6 +2180,7 @@ Examples:
1409
2180
  }
1410
2181
  const client = new ApiClient();
1411
2182
  try {
2183
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1412
2184
  // First get current status to get instance IDs
1413
2185
  const status = await client.getBatchTestStatus(resolvedAppId, "workflow", workflowId, batchId);
1414
2186
  // Get instance IDs that are still running
@@ -1441,29 +2213,32 @@ Examples:
1441
2213
  // ============================================
1442
2214
  const attachments = tests
1443
2215
  .command("attachments")
1444
- .description("Manage test case file attachments")
2216
+ .description("Inspect test case file attachments")
1445
2217
  .addHelpText("after", `
2218
+ Attachments are authored as files in workflows/<key>.tests/<name>/ and applied
2219
+ with \`primitive config push\` (#2769); removing one and running
2220
+ \`config push --prune\` deletes it server-side. These verbs read server state.
2221
+
1446
2222
  Examples:
1447
- $ primitive workflows tests attachments list <workflow-id> <test-case-id>
1448
- $ primitive workflows tests attachments upload <workflow-id> <test-case-id> ./document.pdf
1449
- $ primitive workflows tests attachments download <workflow-id> <test-case-id> document.pdf
1450
- $ primitive workflows tests attachments delete <workflow-id> <test-case-id> document.pdf
2223
+ $ primitive workflows tests attachments list <workflow> <test-case-id>
2224
+ $ primitive workflows tests attachments download <workflow> <test-case-id> document.pdf
1451
2225
  `);
1452
2226
  // List attachments
1453
2227
  attachments
1454
2228
  .command("list")
1455
2229
  .description("List attachments for a test case")
1456
- .argument("<workflow-id>", "Workflow ID")
2230
+ .argument("<workflow>", "Workflow key or ID")
1457
2231
  .argument("<test-case-id>", "Test Case ID")
1458
2232
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1459
2233
  .option("--json", "Output as JSON")
1460
- .action(async (workflowId, testCaseId, options) => {
2234
+ .action(async (workflow, testCaseId, options) => {
1461
2235
  const resolvedAppId = resolveAppId(undefined, options);
1462
2236
  const client = new ApiClient();
1463
2237
  try {
2238
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1464
2239
  const result = await client.listTestCaseAttachments(resolvedAppId, "workflow", workflowId, testCaseId);
1465
2240
  if (options.json) {
1466
- json(result.attachments);
2241
+ json(wholeListEnvelope(result.attachments));
1467
2242
  return;
1468
2243
  }
1469
2244
  if (!result.attachments || result.attachments.length === 0) {
@@ -1485,62 +2260,20 @@ Examples:
1485
2260
  process.exit(1);
1486
2261
  }
1487
2262
  });
1488
- // Upload attachment
1489
- attachments
1490
- .command("upload")
1491
- .description("Upload a file attachment to a test case")
1492
- .argument("<workflow-id>", "Workflow ID")
1493
- .argument("<test-case-id>", "Test Case ID")
1494
- .argument("<file-path>", "Path to the file to upload")
1495
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1496
- .option("--name <name>", "Override the filename")
1497
- .option("--json", "Output as JSON")
1498
- .action(async (workflowId, testCaseId, filePath, options) => {
1499
- const resolvedAppId = resolveAppId(undefined, options);
1500
- const client = new ApiClient();
1501
- try {
1502
- // Check file exists and get stats
1503
- const stats = statSync(filePath);
1504
- if (!stats.isFile()) {
1505
- error(`Not a file: ${filePath}`);
1506
- process.exit(1);
1507
- }
1508
- // Check file size (10 MB limit)
1509
- const TEN_MB = 10 * 1024 * 1024;
1510
- if (stats.size > TEN_MB) {
1511
- error(`File too large (max 10MB): ${formatFileSize(stats.size)}`);
1512
- process.exit(1);
1513
- }
1514
- const filename = options.name || basename(filePath);
1515
- const data = readFileSync(filePath);
1516
- const contentType = mimeLookup(filePath) || "application/octet-stream";
1517
- const result = await client.uploadTestCaseAttachment(resolvedAppId, "workflow", workflowId, testCaseId, filename, data, contentType);
1518
- if (options.json) {
1519
- json(result);
1520
- return;
1521
- }
1522
- success(`Uploaded: ${filename}`);
1523
- keyValue("Size", formatFileSize(result.attachment.size));
1524
- keyValue("Type", result.attachment.contentType);
1525
- }
1526
- catch (err) {
1527
- error(err.message);
1528
- process.exit(1);
1529
- }
1530
- });
1531
2263
  // Download attachment
1532
2264
  attachments
1533
2265
  .command("download")
1534
2266
  .description("Download a file attachment from a test case")
1535
- .argument("<workflow-id>", "Workflow ID")
2267
+ .argument("<workflow>", "Workflow key or ID")
1536
2268
  .argument("<test-case-id>", "Test Case ID")
1537
2269
  .argument("<filename>", "Name of the file to download")
1538
2270
  .argument("[output-path]", "Output path (defaults to current directory)")
1539
2271
  .option("--app <app-id>", "App ID (uses current app if not specified)")
1540
- .action(async (workflowId, testCaseId, filename, outputPath, options) => {
2272
+ .action(async (workflow, testCaseId, filename, outputPath, options) => {
1541
2273
  const resolvedAppId = resolveAppId(undefined, options);
1542
2274
  const client = new ApiClient();
1543
2275
  try {
2276
+ const workflowId = await resolveWorkflowId(client, resolvedAppId, workflow);
1544
2277
  const result = await client.downloadTestCaseAttachment(resolvedAppId, "workflow", workflowId, testCaseId, filename);
1545
2278
  const finalPath = outputPath || filename;
1546
2279
  writeFileSync(finalPath, result.data);
@@ -1553,36 +2286,129 @@ Examples:
1553
2286
  process.exit(1);
1554
2287
  }
1555
2288
  });
1556
- // Delete attachment
1557
- attachments
1558
- .command("delete")
1559
- .description("Delete a file attachment from a test case")
1560
- .argument("<workflow-id>", "Workflow ID")
1561
- .argument("<test-case-id>", "Test Case ID")
1562
- .argument("<filename>", "Name of the file to delete")
1563
- .option("--app <app-id>", "App ID (uses current app if not specified)")
1564
- .option("-y, --yes", "Skip confirmation prompt")
1565
- .action(async (workflowId, testCaseId, filename, options) => {
1566
- const resolvedAppId = resolveAppId(undefined, options);
1567
- if (!options.yes) {
1568
- const inquirer = await import("inquirer");
1569
- const { confirm } = await inquirer.default.prompt([
1570
- {
1571
- type: "confirm",
1572
- name: "confirm",
1573
- message: `Are you sure you want to delete attachment "${filename}"?`,
1574
- default: false,
1575
- },
1576
- ]);
1577
- if (!confirm) {
1578
- info("Cancelled.");
2289
+ // ============================================
2290
+ // Codegen command (issue #1442)
2291
+ // ============================================
2292
+ workflows
2293
+ .command("codegen")
2294
+ .description("Generate typed client signatures from the local workflows/*.toml schemas. TypeScript (default): one <key>.generated.ts per workflow (<Key>Input/<Key>Output types + a <key>(client) invoker factory). Swift (--lang swift): one <key>.generated.swift per workflow (<Key>Input/<Key>Output Codable types).\n\nFormatting: the emitted TypeScript is already formatted the way Prettier's DEFAULT config formats it, so a `codegen` -> `prettier --write` -> `codegen --check` pipeline stays green. If your project uses a NON-default Prettier config (a different printWidth, trailingComma, or quote style), Prettier will reformat the generated files and `--check` will then fail: add the generated directory to `.prettierignore` and commit the raw emission instead.")
2295
+ .argument("[workflow-key]", "Generate for a single workflow (defaults to every workflows/*.toml found)")
2296
+ .option("-o, --output <dir>", "Output directory for the generated files (defaults to <config-dir>/workflows/generated/)")
2297
+ .option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
2298
+ .option("--check", "Exit non-zero if generated output is out of date (CI guard); does not write. Compares byte-for-byte, so the file on disk must match the emission exactly.")
2299
+ .option("--json", "Output the result summary as JSON")
2300
+ .action(async (workflowKey, options) => {
2301
+ try {
2302
+ // 0. Validate --lang up front so an unknown value fails clearly rather
2303
+ // than silently defaulting to the TS emitter.
2304
+ const lang = String(options.lang ?? "ts").toLowerCase();
2305
+ if (lang !== "ts" && lang !== "swift") {
2306
+ error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
2307
+ process.exit(1);
2308
+ }
2309
+ // 1. The single source workflows/ directory: the environment's, and
2310
+ // only ever the environment's (#1510, #3154).
2311
+ const workflowsSourceDir = resolveCodegenSourceDir({
2312
+ subdir: "workflows",
2313
+ });
2314
+ // 2. Collect the source .toml files (one per workflow), filtered to a
2315
+ // single workflow when an argument is given. Match on the file stem;
2316
+ // the generator resolves the real key from `[workflow].key`.
2317
+ const inputs = [];
2318
+ for (const fileName of readdirSync(workflowsSourceDir)) {
2319
+ if (!fileName.endsWith(".toml"))
2320
+ continue;
2321
+ const stem = fileName.slice(0, -".toml".length);
2322
+ if (workflowKey && stem !== workflowKey)
2323
+ continue;
2324
+ const tomlPath = path.join(workflowsSourceDir, fileName);
2325
+ inputs.push({
2326
+ fileStem: stem,
2327
+ tomlPath,
2328
+ tomlContent: readFileSync(tomlPath, "utf-8"),
2329
+ });
2330
+ }
2331
+ if (inputs.length === 0) {
2332
+ error(workflowKey
2333
+ ? `No workflows/${workflowKey}.toml found under primitive/<env>/.`
2334
+ : "No workflows/*.toml files found to generate from.");
2335
+ process.exit(1);
2336
+ }
2337
+ // 3. Resolve the output directory. Default: a `generated/` subdir of
2338
+ // the first source dir (keeps generated output beside the schema).
2339
+ const outputDir = options.output
2340
+ ? path.resolve(options.output)
2341
+ : path.join(path.dirname(inputs[0].tomlPath), "generated");
2342
+ // 4. Run codegen (or --check) with the selected emitter. Both the TS
2343
+ // and Swift generators return the same WriteOrCheckResult shape, so
2344
+ // the reporting below is emitter-agnostic. Fork 1 = A: only the
2345
+ // emitter differs; both read the same synced TOMLs and the same
2346
+ // drift-guarded schema descriptor.
2347
+ const codegenResult = lang === "swift"
2348
+ ? await generateWorkflowSwiftTypes({
2349
+ inputs,
2350
+ outputDir,
2351
+ check: !!options.check,
2352
+ singleWorkflow: !!workflowKey,
2353
+ })
2354
+ : await generateWorkflowTypes({
2355
+ inputs,
2356
+ outputDir,
2357
+ check: !!options.check,
2358
+ singleWorkflow: !!workflowKey,
2359
+ });
2360
+ if (options.check) {
2361
+ if (codegenResult.mismatches.length > 0) {
2362
+ if (options.json) {
2363
+ json({ ok: false, mismatches: codegenResult.mismatches });
2364
+ }
2365
+ else {
2366
+ error(`Check failed: ${codegenResult.mismatches.length} file(s) out of date.`);
2367
+ for (const m of codegenResult.mismatches) {
2368
+ error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
2369
+ }
2370
+ // Echo back the options that decide WHERE and WHAT was
2371
+ // checked. Suggesting a bare `workflows codegen` after a
2372
+ // `--check -o <dir>` sent people to regenerate into the
2373
+ // default <config-dir>/workflows/generated/ instead of the
2374
+ // directory that is actually out of date, so the files stayed
2375
+ // stale and the next check failed the same way.
2376
+ const regenerate = ["primitive workflows codegen"];
2377
+ if (workflowKey)
2378
+ regenerate.push(workflowKey);
2379
+ if (lang === "swift")
2380
+ regenerate.push("--lang swift");
2381
+ if (options.output)
2382
+ regenerate.push(`-o ${options.output}`);
2383
+ info(`Run \`${regenerate.join(" ")}\` to regenerate.`);
2384
+ }
2385
+ process.exit(1);
2386
+ }
2387
+ if (options.json) {
2388
+ json({ ok: true, checked: codegenResult.writtenFiles.length });
2389
+ }
2390
+ else {
2391
+ success(`Check passed: ${codegenResult.writtenFiles.length} file(s) up to date.`);
2392
+ }
1579
2393
  return;
1580
2394
  }
1581
- }
1582
- const client = new ApiClient();
1583
- try {
1584
- await client.deleteTestCaseAttachment(resolvedAppId, "workflow", workflowId, testCaseId, filename);
1585
- success(`Deleted: ${filename}`);
2395
+ if (options.json) {
2396
+ json({
2397
+ written: codegenResult.writtenFiles,
2398
+ deleted: codegenResult.deletedFiles,
2399
+ });
2400
+ return;
2401
+ }
2402
+ success(`Generated ${codegenResult.writtenFiles.length} file(s)` +
2403
+ (codegenResult.deletedFiles.length > 0
2404
+ ? `, deleted ${codegenResult.deletedFiles.length} stale file(s).`
2405
+ : "."));
2406
+ for (const f of codegenResult.writtenFiles) {
2407
+ keyValue(" wrote", path.relative(process.cwd(), f));
2408
+ }
2409
+ for (const f of codegenResult.deletedFiles) {
2410
+ keyValue(" deleted", path.relative(process.cwd(), f));
2411
+ }
1586
2412
  }
1587
2413
  catch (err) {
1588
2414
  error(err.message);