@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,22 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+
3
+ interface OpenDeps {
4
+ platform?: NodeJS.Platform;
5
+ spawn?: typeof nodeSpawn;
6
+ }
7
+
8
+ /** Open the default browser at `url`. Best-effort — never throws into the caller. */
9
+ export function openBrowser(url: string, deps: OpenDeps = {}): void {
10
+ const platform = deps.platform ?? process.platform;
11
+ const spawn = deps.spawn ?? nodeSpawn;
12
+ const [cmd, args] =
13
+ platform === 'darwin' ? ['open', [url]]
14
+ : platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
15
+ : ['xdg-open', [url]];
16
+ try {
17
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
18
+ child.unref?.();
19
+ } catch {
20
+ // headless / no browser — the URL is printed by the caller anyway
21
+ }
22
+ }
@@ -0,0 +1,67 @@
1
+ import { expect, it } from 'vitest';
2
+ import { extractResponse } from './operationResult';
3
+ import type { WorkflowDefinition } from '../src/engine';
4
+
5
+ const base = { version: 1, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' };
6
+
7
+ it('returns the Response node output when present', () => {
8
+ const flow = { ...base, id: 'f', name: 'F', edges: [],
9
+ nodes: [{ id: 'r', type: 'Response', label: 'R', position: { x: 0, y: 0 }, config: {} }] } as unknown as WorkflowDefinition;
10
+ const run = { nodeStates: { r: { status: 'succeeded', output: { status: 204, body: null } } } } as never;
11
+ expect(extractResponse(run, flow)).toEqual({ status: 204, body: null });
12
+ });
13
+
14
+ it('picks the Response node that actually succeeded, not the first by array order', () => {
15
+ // Two Response nodes: 'first' is earlier in the array but was skipped
16
+ // (nodeStates status !== 'succeeded', output undefined); 'second' ran and
17
+ // succeeded. The real result is the second node's output, not a fallback
18
+ // to 200 + Result.
19
+ const flow = {
20
+ ...base,
21
+ id: 'f',
22
+ name: 'F',
23
+ edges: [],
24
+ nodes: [
25
+ { id: 'first', type: 'Response', label: 'First', position: { x: 0, y: 0 }, config: {} },
26
+ { id: 'second', type: 'Response', label: 'Second', position: { x: 0, y: 0 }, config: {} },
27
+ ],
28
+ } as unknown as WorkflowDefinition;
29
+ const run = {
30
+ nodeStates: {
31
+ first: { status: 'skipped', output: undefined },
32
+ second: { status: 'succeeded', output: { status: 404, body: { error: 'nf' } } },
33
+ },
34
+ } as never;
35
+ expect(extractResponse(run, flow)).toEqual({ status: 404, body: { error: 'nf' } });
36
+ });
37
+
38
+ it('defaults to 200 + Result output when there is no Response node', () => {
39
+ const flow = { ...base, id: 'f', name: 'F', edges: [],
40
+ nodes: [{ id: 'res', type: 'Result', label: 'Res', position: { x: 0, y: 0 }, config: {} }] } as unknown as WorkflowDefinition;
41
+ const run = { status: 'succeeded', nodeStates: { res: { status: 'succeeded', output: { hello: 'world' } } } } as never;
42
+ expect(extractResponse(run, flow)).toEqual({ status: 200, body: { hello: 'world' } });
43
+ });
44
+
45
+ it('returns 500 with the failing node error when the run failed and there is no Response node', () => {
46
+ const flow = { ...base, id: 'f', name: 'F', edges: [],
47
+ nodes: [{ id: 'n', type: 'HttpRequest', label: 'N', position: { x: 0, y: 0 }, config: {} }] } as unknown as WorkflowDefinition;
48
+ const run = { status: 'failed', nodeStates: { n: { status: 'failed', error: 'boom: connection refused' } } } as never;
49
+ expect(extractResponse(run, flow)).toEqual({
50
+ status: 500,
51
+ body: { error: 'run failed', detail: 'boom: connection refused' },
52
+ });
53
+ });
54
+
55
+ it('returns 500 with a generic body when the run failed but no node error is available', () => {
56
+ const flow = { ...base, id: 'f', name: 'F', edges: [],
57
+ nodes: [{ id: 'n', type: 'HttpRequest', label: 'N', position: { x: 0, y: 0 }, config: {} }] } as unknown as WorkflowDefinition;
58
+ const run = { status: 'failed', nodeStates: { n: { status: 'failed' } } } as never;
59
+ expect(extractResponse(run, flow)).toEqual({ status: 500, body: { error: 'run failed' } });
60
+ });
61
+
62
+ it('a succeeded Response node still wins even if the overall run status looks failed', () => {
63
+ const flow = { ...base, id: 'f', name: 'F', edges: [],
64
+ nodes: [{ id: 'r', type: 'Response', label: 'R', position: { x: 0, y: 0 }, config: {} }] } as unknown as WorkflowDefinition;
65
+ const run = { status: 'failed', nodeStates: { r: { status: 'succeeded', output: { status: 201, body: { ok: true } } } } } as never;
66
+ expect(extractResponse(run, flow)).toEqual({ status: 201, body: { ok: true } });
67
+ });
@@ -0,0 +1,27 @@
1
+ import { runOutput, type WorkflowDefinition, type WorkflowRun } from '../src/engine';
2
+
3
+ /** Map a finished operation run to an HTTP response. A flow can have more than
4
+ * one `Response` node (e.g. a success branch and an error branch) — only one
5
+ * of them actually ran. Among the `Response`-type nodes, pick the one whose
6
+ * run state actually succeeded (first such node by array order if somehow
7
+ * more than one did); its output (already `{ status, body }`) wins.
8
+ * Otherwise — no Response node, or none of them succeeded — fall back to
9
+ * 200 + the run's Result output. */
10
+ export function extractResponse(run: WorkflowRun, flow: WorkflowDefinition): { status: number; body: unknown } {
11
+ const responseNodes = flow.nodes.filter((n) => n.type === 'Response');
12
+ const succeeded = responseNodes.find((n) => run.nodeStates[n.id]?.status === 'succeeded');
13
+ const out = succeeded ? run.nodeStates[succeeded.id]?.output : undefined;
14
+ if (out && typeof out === 'object') {
15
+ const o = out as { status?: unknown; body?: unknown };
16
+ return { status: typeof o.status === 'number' ? o.status : 200, body: o.body };
17
+ }
18
+ // No Response node succeeded. If the run itself failed, a 200 fallback would
19
+ // silently mask the failure (runOutput returns undefined → 200 + null). Surface
20
+ // it as a 500, preferring the first failed node's error string (a stable shape,
21
+ // no stack traces) so the caller sees why.
22
+ if (run.status === 'failed') {
23
+ const nodeError = Object.values(run.nodeStates).find((s) => s?.status === 'failed')?.error;
24
+ return { status: 500, body: nodeError ? { error: 'run failed', detail: nodeError } : { error: 'run failed' } };
25
+ }
26
+ return { status: 200, body: runOutput(run, flow) };
27
+ }
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isMountablePath } from './pathGuard';
3
+
4
+ describe('isMountablePath', () => {
5
+ it('rejects bare root, empty, missing, no-leading-slash, and double-slash paths', () => {
6
+ expect(isMountablePath('/')).toBe(false);
7
+ expect(isMountablePath('')).toBe(false);
8
+ expect(isMountablePath(undefined)).toBe(false);
9
+ expect(isMountablePath(null)).toBe(false);
10
+ expect(isMountablePath('no-leading-slash')).toBe(false);
11
+ expect(isMountablePath('//x')).toBe(false);
12
+ });
13
+
14
+ it('accepts real sub-paths', () => {
15
+ expect(isMountablePath('/things')).toBe(true);
16
+ expect(isMountablePath('/a/b')).toBe(true);
17
+ expect(isMountablePath('/things/:id')).toBe(true);
18
+ });
19
+ });
@@ -0,0 +1,12 @@
1
+ // A mountable operation `http.path` must be a real sub-path — never bare
2
+ // root ('/' or '') and never missing. Operation routes are registered
3
+ // before the studio SPA catch-all, so an op mounted at '/' would shadow the
4
+ // studio entirely (GET / would return the op's output, not the studio
5
+ // HTML). Roughly `^/[^/].*`.
6
+ export function isMountablePath(path: string | undefined | null): path is string {
7
+ if (!path) return false;
8
+ if (path.length < 2) return false; // '/' or ''
9
+ if (path[0] !== '/') return false;
10
+ if (path[1] === '/') return false; // '//x'
11
+ return true;
12
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { isPathWithin } from './pathSafety';
3
+
4
+ describe('isPathWithin', () => {
5
+ const base = '/project/apis';
6
+
7
+ it('allows simple nested relative paths', () => {
8
+ expect(isPathWithin(base, 'svc/thing')).toBe(true);
9
+ expect(isPathWithin(base, 'a/b/c')).toBe(true);
10
+ expect(isPathWithin(base, 'billing/charge')).toBe(true);
11
+ });
12
+
13
+ it('rejects parent traversal', () => {
14
+ expect(isPathWithin(base, '../x')).toBe(false);
15
+ expect(isPathWithin(base, 'a/../../b')).toBe(false);
16
+ expect(isPathWithin(base, 'a/../b')).toBe(false);
17
+ });
18
+
19
+ it('rejects absolute paths', () => {
20
+ expect(isPathWithin(base, '/etc/x')).toBe(false);
21
+ });
22
+
23
+ it('rejects empty strings and empty segments', () => {
24
+ expect(isPathWithin(base, '')).toBe(false);
25
+ expect(isPathWithin(base, 'a//b')).toBe(false);
26
+ });
27
+
28
+ it('rejects backslash segments (Windows separator smuggling a traversal)', () => {
29
+ expect(isPathWithin(base, 'a\\..\\b')).toBe(false);
30
+ expect(isPathWithin(base, 'a\\b')).toBe(false);
31
+ });
32
+
33
+ it('rejects a bare "." or ".." segment', () => {
34
+ expect(isPathWithin(base, '.')).toBe(false);
35
+ expect(isPathWithin(base, '..')).toBe(false);
36
+ expect(isPathWithin(base, 'a/.')).toBe(false);
37
+ });
38
+ });
@@ -0,0 +1,33 @@
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
+
3
+ /**
4
+ * True when `relPath` is a safe POSIX-style relative path that, once joined
5
+ * onto `baseDir`, resolves to somewhere *inside* `baseDir`.
6
+ *
7
+ * Rejects:
8
+ * - empty strings
9
+ * - a leading `/` (absolute)
10
+ * - a backslash anywhere (the Windows separator — letting it through would
11
+ * let a segment like `a\..\b` smuggle a traversal past the POSIX-only
12
+ * `'/'.split` checks below, and still resolve outside `baseDir` via
13
+ * `path.join`'s platform-specific behavior)
14
+ * - any `.`/`..`/empty segment (so `a//b`, `./x`, `../x` are all rejected)
15
+ * - anything that, after resolution, doesn't stay under `baseDir` (defense
16
+ * in depth beyond the segment checks above)
17
+ *
18
+ * Shared by `isSafeApiPath` (server/index.ts, POST /operations and future
19
+ * path-taking endpoints) and `isSafeFlowId` (server/agents/runManager.ts) —
20
+ * one robust implementation instead of two ad hoc string checks.
21
+ */
22
+ export function isPathWithin(baseDir: string, relPath: string): boolean {
23
+ if (typeof relPath !== 'string' || relPath.length === 0) return false;
24
+ if (relPath.includes('\\')) return false;
25
+ if (relPath.startsWith('/')) return false;
26
+ const segments = relPath.split('/');
27
+ if (segments.some((seg) => seg === '' || seg === '.' || seg === '..')) return false;
28
+
29
+ const resolvedBase = resolve(baseDir);
30
+ const resolved = resolve(baseDir, relPath);
31
+ const rel = relative(resolvedBase, resolved);
32
+ return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
33
+ }
@@ -0,0 +1,67 @@
1
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { loadProjectConfig } from './projectConfig';
6
+
7
+ const dirs: string[] = [];
8
+ function scratch(): string {
9
+ const d = mkdtempSync(join(tmpdir(), 'ef-proj-'));
10
+ dirs.push(d);
11
+ return d;
12
+ }
13
+ afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); });
14
+
15
+ describe('loadProjectConfig', () => {
16
+ it('returns null when no config file exists', async () => {
17
+ expect(await loadProjectConfig(scratch())).toBeNull();
18
+ });
19
+
20
+ it('loads emberflow.config.mjs, resolving flowsDir against the config dir', async () => {
21
+ const d = scratch();
22
+ mkdirSync(join(d, 'my-flows'));
23
+ writeFileSync(
24
+ join(d, 'emberflow.config.mjs'),
25
+ `export default { flowsDir: 'my-flows', registerNodes: (r) => { r.register({ type: 'X', label: 'X' }, async () => ({})); } };\n`,
26
+ );
27
+ const cfg = await loadProjectConfig(d);
28
+ expect(cfg).not.toBeNull();
29
+ expect(cfg!.root).toBe(d);
30
+ expect(cfg!.flowsDir).toBe(join(d, 'my-flows'));
31
+ expect(typeof cfg!.registerNodes).toBe('function');
32
+ });
33
+
34
+ it('defaults flowsDir to <root>/emberflow/flows and creates nothing', async () => {
35
+ const d = scratch();
36
+ writeFileSync(join(d, 'emberflow.config.mjs'), `export default {};\n`);
37
+ const cfg = await loadProjectConfig(d);
38
+ expect(cfg!.flowsDir).toBe(join(d, 'emberflow', 'flows'));
39
+ });
40
+
41
+ it('throws a readable error for an unparseable config', async () => {
42
+ const d = scratch();
43
+ writeFileSync(join(d, 'emberflow.config.mjs'), `export default {;\n`);
44
+ await expect(loadProjectConfig(d)).rejects.toThrow(/emberflow\.config/);
45
+ });
46
+
47
+ it('infers language "javascript" from a .mjs config when unset', async () => {
48
+ const d = scratch();
49
+ writeFileSync(join(d, 'emberflow.config.mjs'), `export default {};\n`);
50
+ const cfg = await loadProjectConfig(d);
51
+ expect(cfg!.language).toBe('javascript');
52
+ });
53
+
54
+ it('infers language "typescript" from a .ts config when unset', async () => {
55
+ const d = scratch();
56
+ writeFileSync(join(d, 'emberflow.config.ts'), `export default {};\n`);
57
+ const cfg = await loadProjectConfig(d);
58
+ expect(cfg!.language).toBe('typescript');
59
+ });
60
+
61
+ it('explicit language field wins over extension inference', async () => {
62
+ const d = scratch();
63
+ writeFileSync(join(d, 'emberflow.config.mjs'), `export default { language: 'typescript' };\n`);
64
+ const cfg = await loadProjectConfig(d);
65
+ expect(cfg!.language).toBe('typescript');
66
+ });
67
+ });
@@ -0,0 +1,101 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { extname, isAbsolute, join, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import type { NodeRegistry } from '../src/engine';
5
+ import type { VerifierRegistry } from './auth/verifiers';
6
+
7
+ /**
8
+ * Project mode: a consumer repo drops an emberflow.config file at its root
9
+ * and Emberflow serves THEIR flows and nodes. This loader finds and imports
10
+ * that config. The runner runs under tsx, so dynamic import handles .ts
11
+ * configs as well as .js/.mjs.
12
+ */
13
+
14
+ const CONFIG_BASENAMES = ['emberflow.config.mjs', 'emberflow.config.js', 'emberflow.config.ts'];
15
+
16
+ export interface EmberflowUserConfig {
17
+ /** Directory holding <flowId>.json flow files (+ <flowId>.scenarios.json sidecars). Relative to the config file. */
18
+ flowsDir?: string;
19
+ /** Register the project's own node types. */
20
+ registerNodes?: (registry: NodeRegistry) => void;
21
+ /** Register custom auth verifiers (e.g. real JWT) alongside the default bearer/apiKey ones. */
22
+ registerVerifiers?: (registry: VerifierRegistry) => void;
23
+ /** Op id to run when any run finishes with status 'failed' (best-effort; never blocks the failed run). */
24
+ errorOperation?: string;
25
+ /** Authoritative signal for agents/tooling about which language this project's
26
+ * APIs/nodes are authored in. When absent, it's inferred from the config file's
27
+ * own extension (`emberflow.config.ts` → 'typescript', else 'javascript'). */
28
+ language?: 'javascript' | 'typescript';
29
+ }
30
+
31
+ /** Identity helper so consumer configs get typing: `export default defineConfig({...})`. */
32
+ export function defineConfig(config: EmberflowUserConfig): EmberflowUserConfig {
33
+ return config;
34
+ }
35
+
36
+ export interface ProjectConfig {
37
+ root: string;
38
+ flowsDir: string;
39
+ language: 'javascript' | 'typescript';
40
+ /** Present only when the config's `language` field was EXPLICIT and
41
+ * disagrees with the config file's own extension (e.g. `language:
42
+ * 'javascript'` in an `emberflow.config.ts`). Inference makes language and
43
+ * extension agree by construction, so this is the only detectable drift —
44
+ * see src/engine/diagnostics.ts's `language-drift` check, which callers
45
+ * (server/index.ts's diagnostics route, `doctor`) feed this into. */
46
+ languageDrift?: { projectLanguage: 'javascript' | 'typescript'; configPathExtension: string };
47
+ registerNodes?: (registry: NodeRegistry) => void;
48
+ registerVerifiers?: (registry: VerifierRegistry) => void;
49
+ errorOperation?: string;
50
+ }
51
+
52
+ /** The resolved emberflow.config.* path for a project dir, or undefined. */
53
+ export function configPathFor(dir: string): string | undefined {
54
+ const root = resolve(dir);
55
+ return CONFIG_BASENAMES.map((b) => join(root, b)).find(existsSync);
56
+ }
57
+
58
+ /**
59
+ * Loads a project's config. `fresh: true` cache-busts the dynamic import so an
60
+ * edited config re-imports (the module loader caches by URL) — used by the
61
+ * in-process node hot-reload so editing registerNodes takes effect without a
62
+ * process restart (which would kill in-flight agent runs).
63
+ */
64
+ export async function loadProjectConfig(
65
+ dir: string,
66
+ opts: { fresh?: boolean } = {},
67
+ ): Promise<ProjectConfig | null> {
68
+ const root = resolve(dir);
69
+ const file = CONFIG_BASENAMES.map((b) => join(root, b)).find(existsSync);
70
+ if (!file) return null;
71
+ let raw: EmberflowUserConfig;
72
+ try {
73
+ const url = pathToFileURL(file).href + (opts.fresh ? `?t=${Date.now()}` : '');
74
+ const mod = (await import(url)) as { default?: EmberflowUserConfig };
75
+ raw = mod.default ?? {};
76
+ } catch (err) {
77
+ throw new Error(`Failed to load ${file}: ${err instanceof Error ? err.message : String(err)}`);
78
+ }
79
+ const flowsDir = raw.flowsDir
80
+ ? isAbsolute(raw.flowsDir) ? raw.flowsDir : join(root, raw.flowsDir)
81
+ : join(root, 'emberflow', 'flows');
82
+ // Explicit `language` wins; otherwise infer from the config file's own
83
+ // extension — a .ts config implies a TypeScript project, everything else JS.
84
+ const inferredLanguage: 'javascript' | 'typescript' = file.endsWith('.ts') ? 'typescript' : 'javascript';
85
+ const language: 'javascript' | 'typescript' = raw.language ?? inferredLanguage;
86
+ // Drift is only detectable when the field is EXPLICIT: inferred language
87
+ // agrees with the extension by construction, so there's nothing to flag.
88
+ const languageDrift =
89
+ raw.language && raw.language !== inferredLanguage
90
+ ? { projectLanguage: raw.language, configPathExtension: extname(file) }
91
+ : undefined;
92
+ return {
93
+ root,
94
+ flowsDir,
95
+ language,
96
+ ...(languageDrift ? { languageDrift } : {}),
97
+ ...(raw.registerNodes ? { registerNodes: raw.registerNodes } : {}),
98
+ ...(raw.registerVerifiers ? { registerVerifiers: raw.registerVerifiers } : {}),
99
+ ...(raw.errorOperation ? { errorOperation: raw.errorOperation } : {}),
100
+ };
101
+ }
@@ -0,0 +1,106 @@
1
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { loadProjectConfig } from './projectConfig';
6
+ import { buildRegistries, buildFlowStore, buildApiStore, requireProjectWhenExplicit } from './projectMode';
7
+ import { startRun } from '../src/engine';
8
+
9
+ const dirs: string[] = [];
10
+ function scratch(): string {
11
+ const d = mkdtempSync(join(tmpdir(), 'ef-mode-'));
12
+ dirs.push(d);
13
+ return d;
14
+ }
15
+ afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); });
16
+
17
+ function writeFixture(d: string): void {
18
+ mkdirSync(join(d, 'flows'), { recursive: true });
19
+ writeFileSync(join(d, 'emberflow.config.mjs'), `export default {
20
+ flowsDir: 'flows',
21
+ registerNodes(registry) {
22
+ registry.register(
23
+ { type: 'Shout', label: 'Shout', inputSchema: { fields: [{ name: 'text', type: 'string', required: true }] } },
24
+ async (ctx) => ({ loud: String(ctx.input.text).toUpperCase() + '!' }),
25
+ );
26
+ },
27
+ };\n`);
28
+ writeFileSync(join(d, 'flows', 'hello.json'), JSON.stringify({
29
+ id: 'hello', name: 'Hello', version: 1,
30
+ nodes: [
31
+ { id: 'in', type: 'Input', label: 'In', position: { x: 0, y: 0 }, config: { fields: [{ name: 'text', type: 'string', required: true }] } },
32
+ { id: 'shout', type: 'Shout', label: 'Shout', position: { x: 200, y: 0 }, config: {}, inputMap: { text: { sourceNodeId: 'in', sourceField: 'text' } } },
33
+ ],
34
+ edges: [{ id: 'e1', source: 'in', target: 'shout' }],
35
+ createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
36
+ }, null, 2));
37
+ writeFileSync(join(d, 'flows', 'hello.scenarios.json'), JSON.stringify([
38
+ { id: 's1', name: 'greet', input: { text: 'hi' } },
39
+ ]));
40
+ }
41
+
42
+ describe('project mode wiring', () => {
43
+ it('serves project flows with sidecar scenarios and runs consumer nodes', async () => {
44
+ const d = scratch();
45
+ writeFixture(d);
46
+ const project = await loadProjectConfig(d);
47
+ expect(project).not.toBeNull();
48
+ const store = buildFlowStore(project);
49
+ const flow = store.load('hello')!;
50
+ expect(flow.scenarios?.[0].name).toBe('greet');
51
+ const { execution } = buildRegistries(project);
52
+ const run = await startRun({ flow, registry: execution, input: flow.scenarios![0].input }).runToEnd();
53
+ expect(run.status).toBe('succeeded');
54
+ expect(run.nodeStates.shout.output).toEqual({ loud: 'HI!' });
55
+ });
56
+
57
+ it('buildApiStore migrates a project with a custom-named flowsDir (not literally "flows")', async () => {
58
+ const d = scratch();
59
+ mkdirSync(join(d, 'my-flows'), { recursive: true });
60
+ writeFileSync(
61
+ join(d, 'my-flows', 'onboarding.json'),
62
+ JSON.stringify({ id: 'onboarding', name: 'Onboarding', nodes: [], edges: [] }, null, 2),
63
+ );
64
+ writeFileSync(
65
+ join(d, 'emberflow.config.mjs'),
66
+ `export default { flowsDir: 'my-flows', registerNodes: () => {} };\n`,
67
+ );
68
+ const project = await loadProjectConfig(d);
69
+ expect(project).not.toBeNull();
70
+
71
+ const apiStore = buildApiStore(project);
72
+
73
+ // migration ran: apis/default now holds the flow, my-flows/ is gone.
74
+ expect(existsSync(join(d, 'apis', 'default', 'onboarding.json'))).toBe(true);
75
+ expect(existsSync(join(d, 'my-flows'))).toBe(false);
76
+ const tree = apiStore.tree();
77
+ const ids = tree.apis.flatMap((a) => a.operations.map((op) => op.id));
78
+ expect(ids).toContain('onboarding');
79
+ });
80
+
81
+ it('null project falls back to default store dir and default registry', () => {
82
+ const store = buildFlowStore(null);
83
+ expect(store.dir.endsWith('workflows')).toBe(true);
84
+ const { validation, execution } = buildRegistries(null);
85
+ expect(validation.has('Input')).toBe(true);
86
+ expect(execution.has('Input')).toBe(true);
87
+ expect(execution.has('Shout')).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe('requireProjectWhenExplicit', () => {
92
+ it('throws when EMBERFLOW_PROJECT is explicit but no config was found', () => {
93
+ expect(() => requireProjectWhenExplicit(null, '/tmp/typo-path', '/tmp/typo-path')).toThrow(
94
+ '/tmp/typo-path',
95
+ );
96
+ });
97
+
98
+ it('passes the project through when explicit and found', () => {
99
+ const project = { root: '/tmp/proj' } as never;
100
+ expect(requireProjectWhenExplicit(project, '/tmp/proj', '/tmp/proj')).toBe(project);
101
+ });
102
+
103
+ it('stays lenient when implicit (undefined) and no config was found', () => {
104
+ expect(requireProjectWhenExplicit(null, undefined, process.cwd())).toBeNull();
105
+ });
106
+ });
@@ -0,0 +1,59 @@
1
+ import { dirname, join, resolve } from 'node:path';
2
+ import { FlowStore } from './flowStore';
3
+ import { ApiStore } from './apiStore';
4
+ import { migrateFlowsToApis } from './migrateFlows';
5
+ import { createDefaultRegistry } from '../src/nodes';
6
+ import type { NodeRegistry } from '../src/engine';
7
+ import type { ProjectConfig } from './projectConfig';
8
+
9
+ /**
10
+ * Project-mode wiring, kept Express-free so tests exercise exactly what the
11
+ * runner boots with. Two registries because validation and execution are
12
+ * separate instances in the runner — the consumer's nodes must exist in both.
13
+ */
14
+ /**
15
+ * Boot-time guard: an explicit EMBERFLOW_PROJECT env var must fail loudly
16
+ * when no config was found there, mirroring the environments-file stance.
17
+ * Implicit cwd detection stays lenient (silent fallback to default mode).
18
+ */
19
+ export function requireProjectWhenExplicit(
20
+ project: ProjectConfig | null,
21
+ explicitPath: string | undefined,
22
+ resolvedDir: string,
23
+ ): ProjectConfig | null {
24
+ if (explicitPath !== undefined && project === null) {
25
+ throw new Error(
26
+ `EMBERFLOW_PROJECT points at ${resolvedDir} but no emberflow.config.(mjs|js|ts) was found there`,
27
+ );
28
+ }
29
+ return project;
30
+ }
31
+
32
+ export function buildFlowStore(project: ProjectConfig | null): FlowStore {
33
+ return project ? new FlowStore(project.flowsDir, { scenarioSidecars: true }) : new FlowStore();
34
+ }
35
+
36
+ /**
37
+ * The `apis/` tree replaces the flat `flows/` layout. Runs the one-time
38
+ * migration before constructing the store so a legacy project boots straight
39
+ * into the new layout.
40
+ */
41
+ export function buildApiStore(project: ProjectConfig | null): ApiStore {
42
+ const flowsDir = project ? project.flowsDir : resolve(process.cwd(), 'workflows', 'flows');
43
+ const apisDir = join(dirname(flowsDir), 'apis');
44
+ migrateFlowsToApis(flowsDir, apisDir);
45
+ return new ApiStore(apisDir);
46
+ }
47
+
48
+ export function buildRegistries(project: ProjectConfig | null): {
49
+ validation: NodeRegistry;
50
+ execution: NodeRegistry;
51
+ } {
52
+ const validation = createDefaultRegistry();
53
+ const execution = createDefaultRegistry();
54
+ if (project?.registerNodes) {
55
+ project.registerNodes(validation);
56
+ project.registerNodes(execution);
57
+ }
58
+ return { validation, execution };
59
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { redactSecrets } from './redact';
3
+
4
+ describe('redactSecrets', () => {
5
+ it('redacts a secret embedded inside a longer string', () => {
6
+ const payload = { line: 'cookie: session=SECRETVAL; Path=/' };
7
+ const result = redactSecrets(payload, { sessionCookie: 'SECRETVAL' });
8
+ expect(result).toEqual({ line: 'cookie: session=«secret:sessionCookie»; Path=/' });
9
+ });
10
+
11
+ it('redacts the URL-encoded form of a secret', () => {
12
+ const value = 'a b+c/d';
13
+ const encoded = encodeURIComponent(value);
14
+ const payload = { url: `https://x/y?token=${encoded}` };
15
+ const result = redactSecrets(payload, { apiToken: value });
16
+ expect(result).toEqual({ url: `https://x/y?token=«secret:apiToken»` });
17
+ });
18
+
19
+ it('redacts within nested objects and arrays', () => {
20
+ const payload = {
21
+ a: { b: ['prefix SECRETVAL suffix', { c: 'SECRETVAL again' }] },
22
+ };
23
+ const result = redactSecrets(payload, { key: 'SECRETVAL' });
24
+ expect(result).toEqual({
25
+ a: { b: ['prefix «secret:key» suffix', { c: '«secret:key» again' }] },
26
+ });
27
+ });
28
+
29
+ it('redacts multiple secrets in one payload', () => {
30
+ const payload = { line: 'user=USERSECRET pass=PASSSECRET' };
31
+ const result = redactSecrets(payload, { user: 'USERSECRET', pass: 'PASSSECRET' });
32
+ expect(result).toEqual({ line: 'user=«secret:user» pass=«secret:pass»' });
33
+ });
34
+
35
+ it('redacts multiple occurrences of one secret', () => {
36
+ const payload = { line: 'SECRETVAL and SECRETVAL again' };
37
+ const result = redactSecrets(payload, { key: 'SECRETVAL' });
38
+ expect(result).toEqual({ line: '«secret:key» and «secret:key» again' });
39
+ });
40
+
41
+ it('prefers the longer matching secret over one whose value is its substring', () => {
42
+ const payload = { line: 'value=XABCDEFY end' };
43
+ const result = redactSecrets(payload, { short: 'ABCDEF', long: 'XABCDEFY' });
44
+ expect(result).toEqual({ line: 'value=«secret:long» end' });
45
+ });
46
+
47
+ it('does not redact values shorter than 6 characters', () => {
48
+ const payload = { line: 'short=abcde end' };
49
+ const result = redactSecrets(payload, { short: 'abcde' });
50
+ expect(result).toEqual({ line: 'short=abcde end' });
51
+ });
52
+
53
+ it('leaves a payload with no strings unchanged', () => {
54
+ const payload = { n: 42, b: true, nil: null, list: [1, 2, 3] };
55
+ const result = redactSecrets(payload, { key: 'SECRETVAL' });
56
+ expect(result).toEqual(payload);
57
+ });
58
+
59
+ it('is the identity function when the secrets map is empty', () => {
60
+ const payload = { line: 'SECRETVAL stays', nested: { x: 'SECRETVAL' } };
61
+ const result = redactSecrets(payload, {});
62
+ expect(result).toEqual(payload);
63
+ });
64
+
65
+ it('does not mutate the input object', () => {
66
+ const payload = { line: 'SECRETVAL here', nested: { x: 'SECRETVAL' } };
67
+ const clone = JSON.parse(JSON.stringify(payload));
68
+ redactSecrets(payload, { key: 'SECRETVAL' });
69
+ expect(payload).toEqual(clone);
70
+ });
71
+
72
+ it('redacts the base64 form of a secret', () => {
73
+ const value = 'supersecret123';
74
+ const encoded = Buffer.from(value).toString('base64');
75
+ const payload = { line: `token=${encoded}` };
76
+ const result = redactSecrets(payload, { key: value });
77
+ expect(result).toEqual({ line: 'token=«secret:key»' });
78
+ });
79
+
80
+ it('redacts raw, url-encoded, and base64 forms of a secret in one payload', () => {
81
+ const value = 'a b+c/d';
82
+ const encoded = encodeURIComponent(value);
83
+ const b64 = Buffer.from(value).toString('base64');
84
+ const payload = { line: `raw=${value} enc=${encoded} b64=${b64}` };
85
+ const result = redactSecrets(payload, { key: value });
86
+ expect(result).toEqual({ line: 'raw=«secret:key» enc=«secret:key» b64=«secret:key»' });
87
+ });
88
+ });