@xdelivered/emberflow 0.2.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 (414) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/bin/commands.test.ts +185 -0
  4. package/bin/commands.ts +306 -0
  5. package/bin/emberflow.mjs +38 -0
  6. package/bin/init.test.ts +165 -0
  7. package/bin/init.ts +297 -0
  8. package/bin/runtime.mjs +105 -0
  9. package/bin/runtime.test.ts +167 -0
  10. package/bin/skillTargets.test.ts +108 -0
  11. package/bin/skillTargets.ts +36 -0
  12. package/bin/skillTemplates.test.ts +37 -0
  13. package/bin/tsconfig.json +23 -0
  14. package/dist/bin/commands.d.ts +24 -0
  15. package/dist/bin/commands.js +252 -0
  16. package/dist/bin/init.d.ts +17 -0
  17. package/dist/bin/init.js +241 -0
  18. package/dist/bin/runtime.d.mts +39 -0
  19. package/dist/bin/runtime.mjs +96 -0
  20. package/dist/bin/skillTargets.d.ts +13 -0
  21. package/dist/bin/skillTargets.js +22 -0
  22. package/dist/server/agents/claudeAdapter.d.ts +8 -0
  23. package/dist/server/agents/claudeAdapter.js +190 -0
  24. package/dist/server/agents/claudeParse.d.ts +2 -0
  25. package/dist/server/agents/claudeParse.js +162 -0
  26. package/dist/server/agents/codexAdapter.d.ts +13 -0
  27. package/dist/server/agents/codexAdapter.js +174 -0
  28. package/dist/server/agents/codexParse.d.ts +2 -0
  29. package/dist/server/agents/codexParse.js +74 -0
  30. package/dist/server/agents/detect.d.ts +23 -0
  31. package/dist/server/agents/detect.js +35 -0
  32. package/dist/server/agents/gitScope.d.ts +24 -0
  33. package/dist/server/agents/gitScope.js +95 -0
  34. package/dist/server/agents/modelRejectionHint.d.ts +8 -0
  35. package/dist/server/agents/modelRejectionHint.js +12 -0
  36. package/dist/server/agents/prompt.d.ts +79 -0
  37. package/dist/server/agents/prompt.js +364 -0
  38. package/dist/server/agents/runManager.d.ts +90 -0
  39. package/dist/server/agents/runManager.js +259 -0
  40. package/dist/server/agents/types.d.ts +15 -0
  41. package/dist/server/agents/types.js +1 -0
  42. package/dist/server/apiStore.d.ts +60 -0
  43. package/dist/server/apiStore.js +249 -0
  44. package/dist/server/auth/enforce.d.ts +1 -0
  45. package/dist/server/auth/enforce.js +6 -0
  46. package/dist/server/auth/types.d.ts +1 -0
  47. package/dist/server/auth/types.js +1 -0
  48. package/dist/server/auth/verifiers.d.ts +1 -0
  49. package/dist/server/auth/verifiers.js +4 -0
  50. package/dist/server/authAttach.d.ts +19 -0
  51. package/dist/server/authAttach.js +79 -0
  52. package/dist/server/cli.d.ts +7 -0
  53. package/dist/server/cli.js +804 -0
  54. package/dist/server/client.d.ts +130 -0
  55. package/dist/server/client.js +247 -0
  56. package/dist/server/environments.d.ts +103 -0
  57. package/dist/server/environments.js +486 -0
  58. package/dist/server/flowStore.d.ts +37 -0
  59. package/dist/server/flowStore.js +117 -0
  60. package/dist/server/httpOperations.d.ts +32 -0
  61. package/dist/server/httpOperations.js +62 -0
  62. package/dist/server/index.d.ts +1 -0
  63. package/dist/server/index.js +1022 -0
  64. package/dist/server/infrastructure.d.ts +51 -0
  65. package/dist/server/infrastructure.js +116 -0
  66. package/dist/server/login.d.ts +21 -0
  67. package/dist/server/login.js +44 -0
  68. package/dist/server/mcp.d.ts +1 -0
  69. package/dist/server/mcp.js +182 -0
  70. package/dist/server/migrateFlows.d.ts +8 -0
  71. package/dist/server/migrateFlows.js +25 -0
  72. package/dist/server/mockHandler.d.ts +15 -0
  73. package/dist/server/mockHandler.js +35 -0
  74. package/dist/server/nodesPayload.d.ts +14 -0
  75. package/dist/server/nodesPayload.js +14 -0
  76. package/dist/server/normalizeFlow.d.ts +21 -0
  77. package/dist/server/normalizeFlow.js +48 -0
  78. package/dist/server/openBrowser.d.ts +8 -0
  79. package/dist/server/openBrowser.js +16 -0
  80. package/dist/server/operationResult.d.ts +12 -0
  81. package/dist/server/operationResult.js +26 -0
  82. package/dist/server/pathGuard.d.ts +1 -0
  83. package/dist/server/pathGuard.js +16 -0
  84. package/dist/server/pathSafety.d.ts +20 -0
  85. package/dist/server/pathSafety.js +35 -0
  86. package/dist/server/projectConfig.d.ts +47 -0
  87. package/dist/server/projectConfig.js +61 -0
  88. package/dist/server/projectMode.d.ts +26 -0
  89. package/dist/server/projectMode.js +44 -0
  90. package/dist/server/redact.d.ts +6 -0
  91. package/dist/server/redact.js +37 -0
  92. package/dist/server/runRegistry.d.ts +84 -0
  93. package/dist/server/runRegistry.js +200 -0
  94. package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
  95. package/dist/server/runtime/RuntimeAdapter.js +1 -0
  96. package/dist/server/runtime/expressAdapter.d.ts +19 -0
  97. package/dist/server/runtime/expressAdapter.js +56 -0
  98. package/dist/server/scenarioTest.d.ts +14 -0
  99. package/dist/server/scenarioTest.js +66 -0
  100. package/dist/server/subflowRunner.d.ts +32 -0
  101. package/dist/server/subflowRunner.js +72 -0
  102. package/dist/server/testRunner.d.ts +70 -0
  103. package/dist/server/testRunner.js +151 -0
  104. package/dist/src/config.d.ts +3 -0
  105. package/dist/src/config.js +2 -0
  106. package/dist/src/engine/authVerify.d.ts +33 -0
  107. package/dist/src/engine/authVerify.js +63 -0
  108. package/dist/src/engine/diagnostics.d.ts +62 -0
  109. package/dist/src/engine/diagnostics.js +148 -0
  110. package/dist/src/engine/executor.d.ts +151 -0
  111. package/dist/src/engine/executor.js +684 -0
  112. package/dist/src/engine/httpError.d.ts +7 -0
  113. package/dist/src/engine/httpError.js +14 -0
  114. package/dist/src/engine/index.d.ts +11 -0
  115. package/dist/src/engine/index.js +11 -0
  116. package/dist/src/engine/output.d.ts +11 -0
  117. package/dist/src/engine/output.js +20 -0
  118. package/dist/src/engine/publish.d.ts +30 -0
  119. package/dist/src/engine/publish.js +95 -0
  120. package/dist/src/engine/registry.d.ts +34 -0
  121. package/dist/src/engine/registry.js +67 -0
  122. package/dist/src/engine/scenarios.d.ts +12 -0
  123. package/dist/src/engine/scenarios.js +19 -0
  124. package/dist/src/engine/schemaCheck.d.ts +4 -0
  125. package/dist/src/engine/schemaCheck.js +34 -0
  126. package/dist/src/engine/trace.d.ts +12 -0
  127. package/dist/src/engine/trace.js +12 -0
  128. package/dist/src/engine/types.d.ts +308 -0
  129. package/dist/src/engine/types.js +1 -0
  130. package/dist/src/engine/validation.d.ts +26 -0
  131. package/dist/src/engine/validation.js +318 -0
  132. package/dist/src/flows/anomaly-flows.d.ts +23 -0
  133. package/dist/src/flows/anomaly-flows.js +151 -0
  134. package/dist/src/flows/city-sweep-flow.d.ts +10 -0
  135. package/dist/src/flows/city-sweep-flow.js +127 -0
  136. package/dist/src/flows/login-flow.d.ts +13 -0
  137. package/dist/src/flows/login-flow.js +145 -0
  138. package/dist/src/flows/pradar-flows.d.ts +5 -0
  139. package/dist/src/flows/pradar-flows.js +375 -0
  140. package/dist/src/flows/weather-flow.d.ts +12 -0
  141. package/dist/src/flows/weather-flow.js +150 -0
  142. package/dist/src/lib/pathParams.d.ts +3 -0
  143. package/dist/src/lib/pathParams.js +6 -0
  144. package/dist/src/nodes/anomaly.d.ts +2 -0
  145. package/dist/src/nodes/anomaly.js +556 -0
  146. package/dist/src/nodes/flow-control.d.ts +7 -0
  147. package/dist/src/nodes/flow-control.js +270 -0
  148. package/dist/src/nodes/index.d.ts +3 -0
  149. package/dist/src/nodes/index.js +20 -0
  150. package/dist/src/nodes/login.d.ts +7 -0
  151. package/dist/src/nodes/login.js +158 -0
  152. package/dist/src/nodes/loops.d.ts +11 -0
  153. package/dist/src/nodes/loops.js +60 -0
  154. package/dist/src/nodes/pradar/index.d.ts +7 -0
  155. package/dist/src/nodes/pradar/index.js +9 -0
  156. package/dist/src/nodes/pradar/logic.d.ts +189 -0
  157. package/dist/src/nodes/pradar/logic.js +370 -0
  158. package/dist/src/nodes/pradar/nodes.d.ts +42 -0
  159. package/dist/src/nodes/pradar/nodes.js +801 -0
  160. package/dist/src/nodes/requireAuth.d.ts +20 -0
  161. package/dist/src/nodes/requireAuth.js +48 -0
  162. package/dist/src/nodes/response.d.ts +8 -0
  163. package/dist/src/nodes/response.js +33 -0
  164. package/dist/src/nodes/weather.d.ts +33 -0
  165. package/dist/src/nodes/weather.js +260 -0
  166. package/package.json +92 -0
  167. package/server/agentRoute.test.ts +157 -0
  168. package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
  169. package/server/agents/__fixtures__/fake-claude.mjs +55 -0
  170. package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
  171. package/server/agents/__fixtures__/fake-codex.mjs +52 -0
  172. package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
  173. package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
  174. package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
  175. package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
  176. package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
  177. package/server/agents/claudeAdapter.test.ts +127 -0
  178. package/server/agents/claudeAdapter.ts +212 -0
  179. package/server/agents/claudeParse.ts +193 -0
  180. package/server/agents/codexAdapter.test.ts +125 -0
  181. package/server/agents/codexAdapter.ts +198 -0
  182. package/server/agents/codexParse.test.ts +58 -0
  183. package/server/agents/codexParse.ts +126 -0
  184. package/server/agents/detect.test.ts +65 -0
  185. package/server/agents/detect.ts +43 -0
  186. package/server/agents/gitScope.test.ts +129 -0
  187. package/server/agents/gitScope.ts +110 -0
  188. package/server/agents/modelRejectionHint.test.ts +30 -0
  189. package/server/agents/modelRejectionHint.ts +14 -0
  190. package/server/agents/prompt.test.ts +613 -0
  191. package/server/agents/prompt.ts +527 -0
  192. package/server/agents/runManager.test.ts +251 -0
  193. package/server/agents/runManager.ts +286 -0
  194. package/server/agents/types.ts +17 -0
  195. package/server/apiMount.test.ts +591 -0
  196. package/server/apiStore.test.ts +324 -0
  197. package/server/apiStore.ts +241 -0
  198. package/server/auth/enforce.test.ts +61 -0
  199. package/server/auth/enforce.ts +6 -0
  200. package/server/auth/types.ts +5 -0
  201. package/server/auth/verifiers.test.ts +37 -0
  202. package/server/auth/verifiers.ts +9 -0
  203. package/server/authAttach.test.ts +175 -0
  204. package/server/authAttach.ts +99 -0
  205. package/server/cli.ts +847 -0
  206. package/server/cliAuth.test.ts +229 -0
  207. package/server/cliCreate.test.ts +131 -0
  208. package/server/cliDoctor.test.ts +326 -0
  209. package/server/cliTest.test.ts +735 -0
  210. package/server/client.ts +314 -0
  211. package/server/demoProject.test.ts +29 -0
  212. package/server/diagnosticsRoute.test.ts +169 -0
  213. package/server/environments.test.ts +620 -0
  214. package/server/environments.ts +603 -0
  215. package/server/environmentsRoute.test.ts +385 -0
  216. package/server/errorWorkflow.test.ts +205 -0
  217. package/server/flowStore.test.ts +84 -0
  218. package/server/flowStore.ts +125 -0
  219. package/server/httpOperations.test.ts +377 -0
  220. package/server/httpOperations.ts +96 -0
  221. package/server/index.ts +1111 -0
  222. package/server/infrastructure.test.ts +154 -0
  223. package/server/infrastructure.ts +161 -0
  224. package/server/infrastructureRoute.test.ts +105 -0
  225. package/server/login.test.ts +153 -0
  226. package/server/login.ts +67 -0
  227. package/server/mcp.ts +278 -0
  228. package/server/migrateFlows.test.ts +55 -0
  229. package/server/migrateFlows.ts +23 -0
  230. package/server/mockHandler.test.ts +138 -0
  231. package/server/mockHandler.ts +52 -0
  232. package/server/mockRun.test.ts +367 -0
  233. package/server/mockServe.test.ts +467 -0
  234. package/server/nodesPayload.test.ts +29 -0
  235. package/server/nodesPayload.ts +26 -0
  236. package/server/normalizeFlow.test.ts +80 -0
  237. package/server/normalizeFlow.ts +59 -0
  238. package/server/openBrowser.test.ts +20 -0
  239. package/server/openBrowser.ts +22 -0
  240. package/server/operationResult.test.ts +67 -0
  241. package/server/operationResult.ts +27 -0
  242. package/server/pathGuard.test.ts +19 -0
  243. package/server/pathGuard.ts +12 -0
  244. package/server/pathSafety.test.ts +38 -0
  245. package/server/pathSafety.ts +33 -0
  246. package/server/projectConfig.test.ts +67 -0
  247. package/server/projectConfig.ts +101 -0
  248. package/server/projectMode.test.ts +106 -0
  249. package/server/projectMode.ts +59 -0
  250. package/server/redact.test.ts +88 -0
  251. package/server/redact.ts +35 -0
  252. package/server/redactEgress.test.ts +281 -0
  253. package/server/runRegistry.ts +270 -0
  254. package/server/runsAuth.test.ts +119 -0
  255. package/server/runtime/RuntimeAdapter.ts +24 -0
  256. package/server/runtime/expressAdapter.test.ts +38 -0
  257. package/server/runtime/expressAdapter.ts +70 -0
  258. package/server/scenarioTest.test.ts +148 -0
  259. package/server/scenarioTest.ts +88 -0
  260. package/server/setupStatus.test.ts +127 -0
  261. package/server/subflowRunner.ts +104 -0
  262. package/server/testRunner.ts +216 -0
  263. package/server/tsconfig.json +20 -0
  264. package/server/workflowTestRoute.test.ts +297 -0
  265. package/src/App.tsx +233 -0
  266. package/src/components/AgentConsole.tsx +461 -0
  267. package/src/components/BranchRulesEditor.tsx +156 -0
  268. package/src/components/CreateModal.tsx +275 -0
  269. package/src/components/Dock.tsx +487 -0
  270. package/src/components/EnvironmentDialog.tsx +569 -0
  271. package/src/components/EnvironmentPicker.tsx +357 -0
  272. package/src/components/EnvironmentsDialog.tsx +144 -0
  273. package/src/components/ExecutionPager.tsx +67 -0
  274. package/src/components/InfraPanel.test.tsx +103 -0
  275. package/src/components/InfraPanel.tsx +171 -0
  276. package/src/components/Inspector.tsx +1182 -0
  277. package/src/components/Json.tsx +50 -0
  278. package/src/components/JsonModal.tsx +52 -0
  279. package/src/components/LogMessage.tsx +61 -0
  280. package/src/components/NodeRunModal.tsx +213 -0
  281. package/src/components/RunLogPanel.tsx +397 -0
  282. package/src/components/RunbookView.tsx +1085 -0
  283. package/src/components/Scenarios.tsx +476 -0
  284. package/src/components/SettingsDialog.tsx +229 -0
  285. package/src/components/Sidebar.tsx +436 -0
  286. package/src/components/StatusBar.tsx +302 -0
  287. package/src/components/Toolbar.tsx +663 -0
  288. package/src/components/WelcomeDialog.test.tsx +107 -0
  289. package/src/components/WelcomeDialog.tsx +412 -0
  290. package/src/components/agentMarkdown.test.tsx +91 -0
  291. package/src/components/agentMarkdown.tsx +158 -0
  292. package/src/components/ui/badge.tsx +31 -0
  293. package/src/components/ui/button.tsx +46 -0
  294. package/src/components/ui/combobox.tsx +114 -0
  295. package/src/components/ui/command.tsx +82 -0
  296. package/src/components/ui/dialog.tsx +73 -0
  297. package/src/components/ui/input.tsx +20 -0
  298. package/src/components/ui/popover.tsx +32 -0
  299. package/src/components/ui/resizable.tsx +44 -0
  300. package/src/components/ui/tabs.tsx +36 -0
  301. package/src/config.ts +4 -0
  302. package/src/engine/authVerify.ts +81 -0
  303. package/src/engine/diagnostics.test.ts +437 -0
  304. package/src/engine/diagnostics.ts +211 -0
  305. package/src/engine/executor.test.ts +1339 -0
  306. package/src/engine/executor.ts +840 -0
  307. package/src/engine/httpError.test.ts +12 -0
  308. package/src/engine/httpError.ts +16 -0
  309. package/src/engine/index.ts +11 -0
  310. package/src/engine/output.ts +21 -0
  311. package/src/engine/publish.test.ts +176 -0
  312. package/src/engine/publish.ts +127 -0
  313. package/src/engine/registry.test.ts +64 -0
  314. package/src/engine/registry.ts +80 -0
  315. package/src/engine/scenarios.test.ts +55 -0
  316. package/src/engine/scenarios.ts +29 -0
  317. package/src/engine/schemaCheck.test.ts +21 -0
  318. package/src/engine/schemaCheck.ts +35 -0
  319. package/src/engine/trace.test.ts +19 -0
  320. package/src/engine/trace.ts +23 -0
  321. package/src/engine/types.test.ts +22 -0
  322. package/src/engine/types.ts +316 -0
  323. package/src/engine/validation.test.ts +314 -0
  324. package/src/engine/validation.ts +354 -0
  325. package/src/flows/anomaly-flows.test.ts +81 -0
  326. package/src/flows/anomaly-flows.ts +202 -0
  327. package/src/flows/city-sweep-flow.ts +131 -0
  328. package/src/flows/flows.test.ts +30 -0
  329. package/src/flows/login-flow.ts +150 -0
  330. package/src/flows/pradar-flows.test.ts +204 -0
  331. package/src/flows/pradar-flows.ts +419 -0
  332. package/src/flows/weather-flow.ts +154 -0
  333. package/src/globals.css +147 -0
  334. package/src/lib/pathParams.test.ts +24 -0
  335. package/src/lib/pathParams.ts +6 -0
  336. package/src/lib/registerLens.test.ts +79 -0
  337. package/src/lib/registerLens.ts +59 -0
  338. package/src/lib/runbookModel.test.ts +61 -0
  339. package/src/lib/runbookModel.ts +290 -0
  340. package/src/lib/runbookProjection.test.ts +417 -0
  341. package/src/lib/runbookProjection.ts +254 -0
  342. package/src/lib/useTailAnchor.ts +53 -0
  343. package/src/lib/utils.ts +7 -0
  344. package/src/main.tsx +13 -0
  345. package/src/nodes/anomaly.test.ts +98 -0
  346. package/src/nodes/anomaly.ts +637 -0
  347. package/src/nodes/effects-scan.test.ts +44 -0
  348. package/src/nodes/flow-control.test.ts +280 -0
  349. package/src/nodes/flow-control.ts +305 -0
  350. package/src/nodes/index.ts +22 -0
  351. package/src/nodes/login.test.ts +81 -0
  352. package/src/nodes/login.ts +186 -0
  353. package/src/nodes/loops.ts +69 -0
  354. package/src/nodes/pradar/index.ts +11 -0
  355. package/src/nodes/pradar/logic.test.ts +288 -0
  356. package/src/nodes/pradar/logic.ts +582 -0
  357. package/src/nodes/pradar/nodes.test.ts +146 -0
  358. package/src/nodes/pradar/nodes.ts +955 -0
  359. package/src/nodes/requireAuth.test.ts +75 -0
  360. package/src/nodes/requireAuth.ts +55 -0
  361. package/src/nodes/response.test.ts +33 -0
  362. package/src/nodes/response.ts +39 -0
  363. package/src/nodes/traceDetail.test.ts +39 -0
  364. package/src/nodes/weather.test.ts +65 -0
  365. package/src/nodes/weather.ts +306 -0
  366. package/src/store/agentClient.test.ts +103 -0
  367. package/src/store/agentClient.ts +131 -0
  368. package/src/store/apiTree.test.ts +17 -0
  369. package/src/store/apiTree.ts +73 -0
  370. package/src/store/builderStore.test.ts +1234 -0
  371. package/src/store/builderStore.ts +1879 -0
  372. package/src/store/infraClient.ts +63 -0
  373. package/src/store/nodeMeta.test.ts +24 -0
  374. package/src/store/nodeMeta.ts +21 -0
  375. package/src/store/persistence.test.ts +15 -0
  376. package/src/store/persistence.ts +54 -0
  377. package/src/store/serverRunner.ts +381 -0
  378. package/src/store/setupClient.ts +57 -0
  379. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  380. package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  381. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  382. package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  383. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  384. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  385. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  386. package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  387. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  388. package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  389. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  390. package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  391. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  392. package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  393. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  394. package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  395. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  396. package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  397. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  398. package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  399. package/studio-dist/assets/index-DNJwW-hM.css +1 -0
  400. package/studio-dist/assets/index-O26dKRjW.js +65 -0
  401. package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  402. package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  403. package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  404. package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  405. package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  406. package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  407. package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  408. package/studio-dist/favicon.svg +1 -0
  409. package/studio-dist/icons.svg +24 -0
  410. package/studio-dist/index.html +14 -0
  411. package/templates/skills/emberflow-basics/SKILL.md +316 -0
  412. package/templates/skills/emberflow-model-process/SKILL.md +209 -0
  413. package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
  414. package/templates/skills/emberflow-review-workflow/SKILL.md +148 -0
@@ -0,0 +1,735 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { runCli } from './cli';
6
+
7
+ const dirs: string[] = [];
8
+ function scratch(): string {
9
+ const d = mkdtempSync(join(tmpdir(), 'ef-clitest-'));
10
+ dirs.push(d);
11
+ return d;
12
+ }
13
+ afterEach(() => {
14
+ for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
15
+ delete process.env.EMBERFLOW_PROJECT;
16
+ });
17
+
18
+ const savedEnv = { ...process.env };
19
+ beforeEach(() => {
20
+ process.env = { ...savedEnv };
21
+ });
22
+
23
+ /** A single op — `authcheck` — with a Route on `authed` into two Response
24
+ * branches (200 ok / 401 unauthorized), plus a sidecar with three scenarios:
25
+ * one that should pass, one that should deliberately fail, and one with no
26
+ * `expect` (skipped). */
27
+ function writeAuthcheckOp(root: string, scenarios: unknown[]): void {
28
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
29
+ mkdirSync(apisDir, { recursive: true });
30
+ writeFileSync(
31
+ join(apisDir, 'authcheck.json'),
32
+ JSON.stringify(
33
+ {
34
+ id: 'authcheck',
35
+ name: 'Auth Check',
36
+ version: 1,
37
+ http: { method: 'POST', path: '/authcheck' },
38
+ nodes: [
39
+ {
40
+ id: 'input',
41
+ type: 'Input',
42
+ label: 'Input',
43
+ position: { x: 0, y: 0 },
44
+ config: { fields: [{ name: 'authed', type: 'boolean' }] },
45
+ },
46
+ {
47
+ id: 'route',
48
+ type: 'Route',
49
+ label: 'Route',
50
+ position: { x: 200, y: 0 },
51
+ config: { field: 'authed', branches: ['true', 'false'] },
52
+ inputMap: { value: { sourceNodeId: 'input', sourceField: '$' } },
53
+ },
54
+ {
55
+ id: 'respOk',
56
+ type: 'Response',
57
+ label: 'OK',
58
+ position: { x: 400, y: -80 },
59
+ config: { status: 200, body: { ok: true } },
60
+ },
61
+ {
62
+ id: 'respUnauth',
63
+ type: 'Response',
64
+ label: 'Unauthorized',
65
+ position: { x: 400, y: 80 },
66
+ config: { status: 401, body: { error: 'unauthorized' } },
67
+ },
68
+ ],
69
+ edges: [
70
+ { id: 'e1', source: 'input', target: 'route' },
71
+ { id: 'e2', source: 'route', target: 'respOk', sourceHandle: 'true' },
72
+ { id: 'e3', source: 'route', target: 'respUnauth', sourceHandle: 'false' },
73
+ ],
74
+ createdAt: '2026-01-01T00:00:00Z',
75
+ updatedAt: '2026-01-01T00:00:00Z',
76
+ },
77
+ null,
78
+ 2,
79
+ ),
80
+ );
81
+ writeFileSync(join(apisDir, 'authcheck.scenarios.json'), JSON.stringify(scenarios, null, 2));
82
+ }
83
+
84
+ function writeProjectConfig(root: string): void {
85
+ writeFileSync(join(root, 'emberflow.config.mjs'), 'export default {};\n');
86
+ }
87
+
88
+ /** Config registering a 'FakeDb' node whose real implementation always
89
+ * throws — proof that a passing --mock run never called it for real (an
90
+ * unmocked throw would fail the run, and thus the scenario's expectation). */
91
+ function writeProjectConfigWithThrowingDbNode(root: string): void {
92
+ writeFileSync(
93
+ join(root, 'emberflow.config.mjs'),
94
+ `export default {
95
+ registerNodes(registry) {
96
+ registry.register(
97
+ {
98
+ type: 'FakeDb',
99
+ label: 'Fake DB',
100
+ traceKind: 'db',
101
+ inputSchema: { fields: [] },
102
+ },
103
+ async () => {
104
+ throw new Error('real DB touched — this must never run under --mock');
105
+ },
106
+ );
107
+ },
108
+ };\n`,
109
+ );
110
+ }
111
+
112
+ /** A single op — 'infra-op' — with one 'FakeDb' (traceKind: db) node feeding
113
+ * Result, op-level mocked (`db: {id:1, source:'op-mock'}`). Carries two
114
+ * expecting scenarios: one relies on the op-level mock as-is, the other
115
+ * overrides it per nodeId via its own `mocks`. */
116
+ function writeInfraOp(root: string): void {
117
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
118
+ mkdirSync(apisDir, { recursive: true });
119
+ writeFileSync(
120
+ join(apisDir, 'infra-op.json'),
121
+ JSON.stringify({
122
+ id: 'infra-op',
123
+ name: 'Infra Op',
124
+ version: 1,
125
+ nodes: [
126
+ { id: 'db', type: 'FakeDb', label: 'DB', position: { x: 0, y: 0 }, config: {} },
127
+ {
128
+ id: 'result',
129
+ type: 'Result',
130
+ label: 'Result',
131
+ position: { x: 200, y: 0 },
132
+ config: {},
133
+ inputMap: { row: { sourceNodeId: 'db', sourceField: '$' } },
134
+ },
135
+ ],
136
+ edges: [{ id: 'e1', source: 'db', target: 'result' }],
137
+ createdAt: '2026-01-01T00:00:00Z',
138
+ updatedAt: '2026-01-01T00:00:00Z',
139
+ }),
140
+ );
141
+ writeFileSync(
142
+ join(apisDir, 'infra-op.scenarios.json'),
143
+ JSON.stringify({
144
+ scenarios: [
145
+ {
146
+ id: 's-mocked',
147
+ name: 'db-mocked',
148
+ input: {},
149
+ expect: { body: { row: { id: 1, source: 'op-mock' } } },
150
+ },
151
+ {
152
+ id: 's-scenario-override',
153
+ name: 'db-scenario-override',
154
+ input: {},
155
+ expect: { body: { row: { id: 2, source: 'scenario-mock' } } },
156
+ mocks: { db: { id: 2, source: 'scenario-mock' } },
157
+ },
158
+ ],
159
+ mocks: { db: { id: 1, source: 'op-mock' } },
160
+ }),
161
+ );
162
+ }
163
+
164
+ /** A second op — 'infra-op-unmocked' — same 'FakeDb' node, but NO mock
165
+ * anywhere (no op-level, no scenario-level). Proves the fail-loud path. */
166
+ function writeInfraOpUnmocked(root: string): void {
167
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
168
+ mkdirSync(apisDir, { recursive: true });
169
+ writeFileSync(
170
+ join(apisDir, 'infra-op-unmocked.json'),
171
+ JSON.stringify({
172
+ id: 'infra-op-unmocked',
173
+ name: 'Infra Op Unmocked',
174
+ version: 1,
175
+ nodes: [
176
+ { id: 'db', type: 'FakeDb', label: 'DB', position: { x: 0, y: 0 }, config: {} },
177
+ {
178
+ id: 'result',
179
+ type: 'Result',
180
+ label: 'Result',
181
+ position: { x: 200, y: 0 },
182
+ config: {},
183
+ inputMap: { row: { sourceNodeId: 'db', sourceField: '$' } },
184
+ },
185
+ ],
186
+ edges: [{ id: 'e1', source: 'db', target: 'result' }],
187
+ createdAt: '2026-01-01T00:00:00Z',
188
+ updatedAt: '2026-01-01T00:00:00Z',
189
+ }),
190
+ );
191
+ writeFileSync(
192
+ join(apisDir, 'infra-op-unmocked.scenarios.json'),
193
+ JSON.stringify({
194
+ scenarios: [
195
+ {
196
+ id: 's1',
197
+ name: 'db-untouched',
198
+ input: {},
199
+ // `detail` deliberately wrong — forces the failure diff to quote the
200
+ // ACTUAL error string, proving it's the executor's fail-loud message.
201
+ expect: { status: 200, body: { error: 'run failed', detail: 'unreachable' } },
202
+ },
203
+ ],
204
+ }),
205
+ );
206
+ }
207
+
208
+ describe('emberflow test (CLI)', () => {
209
+ let logs: string[];
210
+ let errs: string[];
211
+
212
+ // Capture process.stdout/stderr writes directly (no vi.spyOn — runCli is
213
+ // in-process, so its writes land on the real streams); restored below.
214
+ function captureOutput(): void {
215
+ logs = [];
216
+ errs = [];
217
+ const origOut = process.stdout.write.bind(process.stdout);
218
+ const origErr = process.stderr.write.bind(process.stderr);
219
+ process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
220
+ logs.push(String(chunk));
221
+ return origOut(chunk as never, ...(rest as []));
222
+ }) as typeof process.stdout.write;
223
+ process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
224
+ errs.push(String(chunk));
225
+ return origErr(chunk as never, ...(rest as []));
226
+ }) as typeof process.stderr.write;
227
+ restoreOutput = () => {
228
+ process.stdout.write = origOut;
229
+ process.stderr.write = origErr;
230
+ };
231
+ }
232
+ let restoreOutput: () => void = () => {};
233
+
234
+ beforeEach(() => {
235
+ captureOutput();
236
+ });
237
+ afterEach(() => {
238
+ restoreOutput();
239
+ });
240
+
241
+ it('exits 1 and reports 1 failed/1 passed/1 skipped for a mixed scenario set', async () => {
242
+ const root = scratch();
243
+ writeProjectConfig(root);
244
+ writeAuthcheckOp(root, [
245
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200, body: { ok: true } } },
246
+ // Deliberately wrong expectation: authed:true routes to 200, but this
247
+ // scenario asserts 401 — must fail.
248
+ { id: 's-fail', name: 'expired', input: { authed: true }, expect: { status: 401 } },
249
+ { id: 's-skip', name: 'no-assertion', input: { authed: false } },
250
+ ]);
251
+ process.env.EMBERFLOW_PROJECT = root;
252
+
253
+ const code = await runCli(['test']);
254
+
255
+ expect(code).toBe(1);
256
+ const out = logs.join('');
257
+ expect(out).toContain('1 passed');
258
+ expect(out).toContain('1 failed');
259
+ expect(out).toContain('1 skipped');
260
+ expect(out).toContain('✓ authcheck · authed-ok');
261
+ expect(out).toContain('✗ authcheck · expired');
262
+ expect(out).toContain('status: expected 401, got 200');
263
+ // Skipped scenarios aren't printed as their own line.
264
+ expect(out).not.toContain('no-assertion');
265
+ });
266
+
267
+ it('exits 0 when every expecting scenario passes', async () => {
268
+ const root = scratch();
269
+ writeProjectConfig(root);
270
+ writeAuthcheckOp(root, [
271
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200, body: { ok: true } } },
272
+ { id: 's-pass2', name: 'unauthed-401', input: { authed: false }, expect: { status: 401 } },
273
+ { id: 's-skip', name: 'fixture-only', input: { authed: true } },
274
+ { id: 's-empty', name: 'empty-expect', input: { authed: true }, expect: {} },
275
+ ]);
276
+ process.env.EMBERFLOW_PROJECT = root;
277
+
278
+ const code = await runCli(['test']);
279
+
280
+ expect(code).toBe(0);
281
+ const out = logs.join('');
282
+ expect(out).toContain('2 passed');
283
+ expect(out).toContain('0 failed');
284
+ expect(out).toContain('2 skipped');
285
+ });
286
+
287
+ it('--json prints a machine-readable array carrying per-scenario failures', async () => {
288
+ const root = scratch();
289
+ writeProjectConfig(root);
290
+ writeAuthcheckOp(root, [
291
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200 } },
292
+ { id: 's-fail', name: 'expired', input: { authed: true }, expect: { status: 401 } },
293
+ ]);
294
+ process.env.EMBERFLOW_PROJECT = root;
295
+
296
+ const code = await runCli(['test', '--json']);
297
+
298
+ expect(code).toBe(1);
299
+ const parsed = JSON.parse(logs.join('')) as Array<{
300
+ opId: string;
301
+ scenario: string;
302
+ status: string;
303
+ failures?: string[];
304
+ }>;
305
+ expect(parsed).toHaveLength(2);
306
+ const passed = parsed.find((r) => r.scenario === 'authed-ok')!;
307
+ expect(passed.status).toBe('passed');
308
+ const failed = parsed.find((r) => r.scenario === 'expired')!;
309
+ expect(failed.status).toBe('failed');
310
+ expect(failed.failures).toEqual(['status: expected 401, got 200']);
311
+ });
312
+
313
+ it('unknown opId exits 2', async () => {
314
+ const root = scratch();
315
+ writeProjectConfig(root);
316
+ writeAuthcheckOp(root, [
317
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200 } },
318
+ ]);
319
+ process.env.EMBERFLOW_PROJECT = root;
320
+
321
+ const code = await runCli(['test', 'no-such-op']);
322
+ expect(code).toBe(2);
323
+ });
324
+
325
+ it('unknown environment exits 2', async () => {
326
+ const root = scratch();
327
+ writeProjectConfig(root);
328
+ writeAuthcheckOp(root, [
329
+ { id: 's-pass', name: 'authed-ok', input: { authed: true }, expect: { status: 200 } },
330
+ ]);
331
+ process.env.EMBERFLOW_PROJECT = root;
332
+
333
+ const code = await runCli(['test', '--environment', 'no-such-env']);
334
+ expect(code).toBe(2);
335
+ });
336
+
337
+ it('an op with zero scenarios is silently ignored', async () => {
338
+ const root = scratch();
339
+ writeProjectConfig(root);
340
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
341
+ mkdirSync(apisDir, { recursive: true });
342
+ writeFileSync(
343
+ join(apisDir, 'bare.json'),
344
+ JSON.stringify({
345
+ id: 'bare',
346
+ name: 'Bare',
347
+ version: 1,
348
+ nodes: [{ id: 'result', type: 'Result', label: 'Result', position: { x: 0, y: 0 }, config: {} }],
349
+ edges: [],
350
+ createdAt: '2026-01-01T00:00:00Z',
351
+ updatedAt: '2026-01-01T00:00:00Z',
352
+ }),
353
+ );
354
+ process.env.EMBERFLOW_PROJECT = root;
355
+
356
+ const code = await runCli(['test']);
357
+ expect(code).toBe(0);
358
+ expect(logs.join('')).toContain('0 passed, 0 failed, 0 skipped');
359
+ // Nothing was asserted — a vacuous green must warn on stderr (exit stays 0).
360
+ expect(errs.join('')).toContain('warning: no scenarios asserted (0 skipped)');
361
+ });
362
+
363
+ it('warns on stderr when every scenario is skipped (nothing asserted), still exit 0', async () => {
364
+ const root = scratch();
365
+ writeProjectConfig(root);
366
+ writeAuthcheckOp(root, [
367
+ { id: 's1', name: 'fixture-only', input: { authed: true } },
368
+ { id: 's2', name: 'empty-expect', input: { authed: false }, expect: {} },
369
+ ]);
370
+ process.env.EMBERFLOW_PROJECT = root;
371
+
372
+ const code = await runCli(['test']);
373
+ expect(code).toBe(0);
374
+ expect(logs.join('')).toContain('0 passed, 0 failed, 2 skipped');
375
+ expect(errs.join('')).toContain('warning: no scenarios asserted (2 skipped)');
376
+ });
377
+
378
+ it('redacts secret values out of failure diffs (never prints the raw secret)', async () => {
379
+ const root = scratch();
380
+ // Project node that receives a secret via {"$secret": ...} config and
381
+ // echoes it into the run output — the worst case for a failure diff.
382
+ writeFileSync(
383
+ join(root, 'emberflow.config.mjs'),
384
+ `export default {
385
+ registerNodes(registry) {
386
+ registry.register(
387
+ { type: 'EchoToken', label: 'Echo Token', inputSchema: { fields: [{ name: 'token', type: 'string' }] } },
388
+ async (ctx) => ({ token: ctx.input.token }),
389
+ );
390
+ },
391
+ };\n`,
392
+ );
393
+ writeFileSync(
394
+ join(root, 'emberflow.environments.json'),
395
+ JSON.stringify({
396
+ defaultEnvironment: 'local',
397
+ environments: { local: { vars: {}, secrets: { API_TOKEN: 'supersecretvalue' } } },
398
+ }),
399
+ );
400
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
401
+ mkdirSync(apisDir, { recursive: true });
402
+ writeFileSync(
403
+ join(apisDir, 'leaky.json'),
404
+ JSON.stringify({
405
+ id: 'leaky',
406
+ name: 'Leaky',
407
+ version: 1,
408
+ nodes: [
409
+ {
410
+ id: 'echo',
411
+ type: 'EchoToken',
412
+ label: 'Echo',
413
+ position: { x: 0, y: 0 },
414
+ config: { token: { $secret: 'API_TOKEN' } },
415
+ },
416
+ {
417
+ id: 'result',
418
+ type: 'Result',
419
+ label: 'Result',
420
+ position: { x: 200, y: 0 },
421
+ config: {},
422
+ inputMap: { token: { sourceNodeId: 'echo', sourceField: 'token' } },
423
+ },
424
+ ],
425
+ edges: [{ id: 'e1', source: 'echo', target: 'result' }],
426
+ createdAt: '2026-01-01T00:00:00Z',
427
+ updatedAt: '2026-01-01T00:00:00Z',
428
+ }),
429
+ );
430
+ writeFileSync(
431
+ join(apisDir, 'leaky.scenarios.json'),
432
+ JSON.stringify([
433
+ // Deliberate body mismatch so the failure diff quotes the ACTUAL body
434
+ // (which contains the echoed secret) — it must arrive redacted.
435
+ { id: 's1', name: 'mismatch', input: {}, expect: { body: { token: 'not-the-secret' } } },
436
+ ]),
437
+ );
438
+ process.env.EMBERFLOW_PROJECT = root;
439
+
440
+ const code = await runCli(['test', '--json']);
441
+
442
+ expect(code).toBe(1);
443
+ const allOutput = logs.join('') + errs.join('');
444
+ expect(allOutput).not.toContain('supersecretvalue');
445
+ expect(allOutput).toContain('«secret:API_TOKEN»');
446
+ const parsed = JSON.parse(logs.join('')) as Array<{ status: string; failures?: string[] }>;
447
+ expect(parsed[0].status).toBe('failed');
448
+ expect(parsed[0].failures!.join(' ')).toContain('«secret:API_TOKEN»');
449
+ });
450
+
451
+ describe('--mock', () => {
452
+ it('passes a scenario relying on the op-level mock, without ever calling the real (throwing) node', async () => {
453
+ const root = scratch();
454
+ writeProjectConfigWithThrowingDbNode(root);
455
+ writeInfraOp(root);
456
+ process.env.EMBERFLOW_PROJECT = root;
457
+
458
+ const code = await runCli(['test', 'infra-op', '--json', '--mock']);
459
+
460
+ expect(code).toBe(0);
461
+ const parsed = JSON.parse(logs.join('')) as Array<{ scenario: string; status: string; failures?: string[] }>;
462
+ const mocked = parsed.find((r) => r.scenario === 'db-mocked')!;
463
+ expect(mocked.status).toBe('passed');
464
+ });
465
+
466
+ it('scenario-level mocks override the op-level mock per nodeId', async () => {
467
+ const root = scratch();
468
+ writeProjectConfigWithThrowingDbNode(root);
469
+ writeInfraOp(root);
470
+ process.env.EMBERFLOW_PROJECT = root;
471
+
472
+ const code = await runCli(['test', 'infra-op', '--json', '--mock']);
473
+
474
+ expect(code).toBe(0);
475
+ const parsed = JSON.parse(logs.join('')) as Array<{ scenario: string; status: string; failures?: string[] }>;
476
+ const overridden = parsed.find((r) => r.scenario === 'db-scenario-override')!;
477
+ expect(overridden.status).toBe('passed');
478
+ });
479
+
480
+ it('an unmocked infra node under --mock fails loudly, and the scenario reports failure', async () => {
481
+ const root = scratch();
482
+ writeProjectConfigWithThrowingDbNode(root);
483
+ writeInfraOpUnmocked(root);
484
+ process.env.EMBERFLOW_PROJECT = root;
485
+
486
+ const code = await runCli(['test', 'infra-op-unmocked', '--json', '--mock']);
487
+
488
+ expect(code).toBe(1);
489
+ const parsed = JSON.parse(logs.join('')) as Array<{ scenario: string; status: string; failures?: string[] }>;
490
+ expect(parsed).toHaveLength(1);
491
+ expect(parsed[0].status).toBe('failed');
492
+ // The executor's fail-loud message, surfaced through the 500 fallback
493
+ // extractResponse builds when no Response node succeeded.
494
+ expect(parsed[0].failures!.join(' ')).toContain('would touch real infrastructure');
495
+ });
496
+
497
+ it('without --mock, behavior is unchanged: the real (throwing) node executes and the run fails for real', async () => {
498
+ const root = scratch();
499
+ writeProjectConfigWithThrowingDbNode(root);
500
+ writeInfraOpUnmocked(root);
501
+ process.env.EMBERFLOW_PROJECT = root;
502
+
503
+ const code = await runCli(['test', 'infra-op-unmocked', '--json']);
504
+
505
+ expect(code).toBe(1);
506
+ const parsed = JSON.parse(logs.join('')) as Array<{ scenario: string; status: string; failures?: string[] }>;
507
+ expect(parsed.every((r) => r.status === 'failed')).toBe(true);
508
+ expect(parsed.map((r) => r.failures!.join(' ')).join(' ')).toContain('real DB touched');
509
+ });
510
+ });
511
+ });
512
+
513
+ describe('emberflow list-environments (CLI)', () => {
514
+ let logs: string[];
515
+ let restoreOutput: () => void = () => {};
516
+
517
+ beforeEach(() => {
518
+ logs = [];
519
+ const origOut = process.stdout.write.bind(process.stdout);
520
+ process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
521
+ logs.push(String(chunk));
522
+ return origOut(chunk as never, ...(rest as []));
523
+ }) as typeof process.stdout.write;
524
+ restoreOutput = () => {
525
+ process.stdout.write = origOut;
526
+ };
527
+ });
528
+ afterEach(() => {
529
+ restoreOutput();
530
+ });
531
+
532
+ it('falls back to an in-process offline load when the runner is unreachable', async () => {
533
+ const root = scratch();
534
+ writeFileSync(
535
+ join(root, 'emberflow.environments.json'),
536
+ JSON.stringify({
537
+ defaultEnvironment: 'dev',
538
+ environments: { dev: { vars: { API_URL: 'https://example.test' }, secrets: ['API_KEY'] } },
539
+ }),
540
+ );
541
+ writeFileSync(join(root, 'emberflow.secrets.json'), JSON.stringify({ dev: { API_KEY: 'k' } }), {
542
+ mode: 0o600,
543
+ });
544
+ process.env.EMBERFLOW_PROJECT = root;
545
+ // Nothing listens on this port — forces RunnerUnreachableError.
546
+ process.env.EMBERFLOW_RUNNER_URL = 'http://127.0.0.1:1';
547
+
548
+ const code = await runCli(['list-environments']);
549
+
550
+ expect(code).toBe(0);
551
+ const parsed = JSON.parse(logs.join('')) as {
552
+ source: string;
553
+ defaultEnvironment: string;
554
+ environments: Array<{ name: string; varKeys: string[]; secretKeys: string[] }>;
555
+ };
556
+ expect(parsed.source).toBe('offline');
557
+ expect(parsed.defaultEnvironment).toBe('dev');
558
+ expect(parsed.environments).toEqual([
559
+ { name: 'dev', protected: false, varKeys: ['API_URL'], secretKeys: ['API_KEY'] },
560
+ ]);
561
+ // Never leaks the secret value, only its key name.
562
+ expect(logs.join('')).not.toContain('"k"');
563
+
564
+ delete process.env.EMBERFLOW_RUNNER_URL;
565
+ });
566
+ });
567
+
568
+ describe('emberflow get-node (CLI)', () => {
569
+ let logs: string[];
570
+ let errs: string[];
571
+ let restoreOutput: () => void = () => {};
572
+
573
+ beforeEach(() => {
574
+ logs = [];
575
+ errs = [];
576
+ const origOut = process.stdout.write.bind(process.stdout);
577
+ const origErr = process.stderr.write.bind(process.stderr);
578
+ process.stdout.write = ((chunk: unknown, ...rest: unknown[]) => {
579
+ logs.push(String(chunk));
580
+ return origOut(chunk as never, ...(rest as []));
581
+ }) as typeof process.stdout.write;
582
+ process.stderr.write = ((chunk: unknown, ...rest: unknown[]) => {
583
+ errs.push(String(chunk));
584
+ return origErr(chunk as never, ...(rest as []));
585
+ }) as typeof process.stderr.write;
586
+ restoreOutput = () => {
587
+ process.stdout.write = origOut;
588
+ process.stderr.write = origErr;
589
+ };
590
+ // Force the runner-unreachable offline path so these run hermetically.
591
+ process.env.EMBERFLOW_RUNNER_URL = 'http://127.0.0.1:1';
592
+ });
593
+ afterEach(() => {
594
+ restoreOutput();
595
+ delete process.env.EMBERFLOW_RUNNER_URL;
596
+ });
597
+
598
+ /** A project registering a 'Lookup' infra node (traceKind db, mutation
599
+ * effects, typed input/output schemas) and a 'graph-op' wiring
600
+ * input → lookup → result with a couple of edges. */
601
+ function writeGraphProject(root: string): void {
602
+ writeFileSync(
603
+ join(root, 'emberflow.config.mjs'),
604
+ `export default {
605
+ registerNodes(registry) {
606
+ registry.register(
607
+ {
608
+ type: 'Lookup',
609
+ label: 'Lookup User',
610
+ traceKind: 'db',
611
+ effects: 'mutation',
612
+ inputSchema: { fields: [{ name: 'userId', type: 'string' }] },
613
+ outputSchema: { fields: [{ name: 'name', type: 'string' }, { name: 'email', type: 'string' }] },
614
+ },
615
+ async () => ({ name: 'x', email: 'y' }),
616
+ );
617
+ },
618
+ };\n`,
619
+ );
620
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
621
+ mkdirSync(apisDir, { recursive: true });
622
+ writeFileSync(
623
+ join(apisDir, 'graph-op.json'),
624
+ JSON.stringify({
625
+ id: 'graph-op',
626
+ name: 'Graph Op',
627
+ version: 1,
628
+ nodes: [
629
+ { id: 'input', type: 'Input', label: 'Input', position: { x: 0, y: 0 }, config: {} },
630
+ {
631
+ id: 'lookup',
632
+ type: 'Lookup',
633
+ label: 'Lookup',
634
+ position: { x: 200, y: 0 },
635
+ config: { table: 'users' },
636
+ inputMap: { userId: { sourceNodeId: 'input', sourceField: 'id' } },
637
+ retry: { maxTries: 3 },
638
+ optional: true,
639
+ },
640
+ { id: 'result', type: 'Result', label: 'Result', position: { x: 400, y: 0 }, config: {} },
641
+ ],
642
+ edges: [
643
+ { id: 'e1', source: 'input', target: 'lookup' },
644
+ { id: 'e2', source: 'lookup', target: 'result', sourceHandle: 'ok' },
645
+ ],
646
+ createdAt: '2026-01-01T00:00:00Z',
647
+ updatedAt: '2026-01-01T00:00:00Z',
648
+ }),
649
+ );
650
+ }
651
+
652
+ it('prints a node instance, its inbound + outbound edges, and the registered definition summary', async () => {
653
+ const root = scratch();
654
+ writeGraphProject(root);
655
+ process.env.EMBERFLOW_PROJECT = root;
656
+
657
+ const code = await runCli(['get-node', 'graph-op', 'lookup']);
658
+
659
+ expect(code).toBe(0);
660
+ const parsed = JSON.parse(logs.join('')) as {
661
+ operation: string;
662
+ node: { id: string; type: string; config: unknown; inputMap: unknown; retry?: unknown; optional?: boolean };
663
+ inbound: Array<{ source: string; sourceHandle?: string }>;
664
+ outbound: Array<{ target: string; sourceHandle?: string }>;
665
+ definition: { type: string; traceKind?: string; effects?: string; inputFields: string[]; outputFields: string[] } | null;
666
+ };
667
+ expect(parsed.operation).toBe('graph-op');
668
+ expect(parsed.node.id).toBe('lookup');
669
+ expect(parsed.node.type).toBe('Lookup');
670
+ expect(parsed.node.config).toEqual({ table: 'users' });
671
+ expect(parsed.node.inputMap).toEqual({ userId: { sourceNodeId: 'input', sourceField: 'id' } });
672
+ expect(parsed.node.retry).toEqual({ maxTries: 3 });
673
+ expect(parsed.node.optional).toBe(true);
674
+ expect(parsed.inbound).toEqual([{ source: 'input' }]);
675
+ expect(parsed.outbound).toEqual([{ target: 'result', sourceHandle: 'ok' }]);
676
+ expect(parsed.definition).toMatchObject({
677
+ type: 'Lookup',
678
+ traceKind: 'db',
679
+ effects: 'mutation',
680
+ inputFields: ['userId'],
681
+ outputFields: ['name', 'email'],
682
+ });
683
+ });
684
+
685
+ it('definition is null for a node whose type is not registered', async () => {
686
+ const root = scratch();
687
+ writeFileSync(join(root, 'emberflow.config.mjs'), 'export default {};\n');
688
+ const apisDir = join(root, 'emberflow', 'apis', 'default');
689
+ mkdirSync(apisDir, { recursive: true });
690
+ writeFileSync(
691
+ join(apisDir, 'bare.json'),
692
+ JSON.stringify({
693
+ id: 'bare',
694
+ name: 'Bare',
695
+ version: 1,
696
+ nodes: [{ id: 'mystery', type: 'NotRegistered', label: 'Mystery', position: { x: 0, y: 0 }, config: {} }],
697
+ edges: [],
698
+ createdAt: '2026-01-01T00:00:00Z',
699
+ updatedAt: '2026-01-01T00:00:00Z',
700
+ }),
701
+ );
702
+ process.env.EMBERFLOW_PROJECT = root;
703
+
704
+ const code = await runCli(['get-node', 'bare', 'mystery']);
705
+
706
+ expect(code).toBe(0);
707
+ const parsed = JSON.parse(logs.join('')) as { definition: unknown };
708
+ expect(parsed.definition).toBeNull();
709
+ });
710
+
711
+ it('unknown operation exits 1', async () => {
712
+ const root = scratch();
713
+ writeGraphProject(root);
714
+ process.env.EMBERFLOW_PROJECT = root;
715
+
716
+ const code = await runCli(['get-node', 'no-such-op', 'lookup']);
717
+ expect(code).toBe(1);
718
+ expect(errs.join('')).toContain('Unknown operation');
719
+ });
720
+
721
+ it('unknown node in a known operation exits 1', async () => {
722
+ const root = scratch();
723
+ writeGraphProject(root);
724
+ process.env.EMBERFLOW_PROJECT = root;
725
+
726
+ const code = await runCli(['get-node', 'graph-op', 'no-such-node']);
727
+ expect(code).toBe(1);
728
+ expect(errs.join('')).toContain('Unknown node');
729
+ });
730
+
731
+ it('missing args exit 2', async () => {
732
+ const code = await runCli(['get-node', 'graph-op']);
733
+ expect(code).toBe(2);
734
+ });
735
+ });