@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.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/bin/commands.test.ts +185 -0
- package/bin/commands.ts +306 -0
- package/bin/emberflow.mjs +38 -0
- package/bin/init.test.ts +165 -0
- package/bin/init.ts +297 -0
- package/bin/runtime.mjs +105 -0
- package/bin/runtime.test.ts +167 -0
- package/bin/skillTargets.test.ts +108 -0
- package/bin/skillTargets.ts +36 -0
- package/bin/skillTemplates.test.ts +37 -0
- package/bin/tsconfig.json +23 -0
- package/dist/bin/commands.d.ts +24 -0
- package/dist/bin/commands.js +252 -0
- package/dist/bin/init.d.ts +17 -0
- package/dist/bin/init.js +241 -0
- package/dist/bin/runtime.d.mts +39 -0
- package/dist/bin/runtime.mjs +96 -0
- package/dist/bin/skillTargets.d.ts +13 -0
- package/dist/bin/skillTargets.js +22 -0
- package/dist/server/agents/claudeAdapter.d.ts +8 -0
- package/dist/server/agents/claudeAdapter.js +190 -0
- package/dist/server/agents/claudeParse.d.ts +2 -0
- package/dist/server/agents/claudeParse.js +162 -0
- package/dist/server/agents/codexAdapter.d.ts +13 -0
- package/dist/server/agents/codexAdapter.js +174 -0
- package/dist/server/agents/codexParse.d.ts +2 -0
- package/dist/server/agents/codexParse.js +74 -0
- package/dist/server/agents/detect.d.ts +23 -0
- package/dist/server/agents/detect.js +35 -0
- package/dist/server/agents/gitScope.d.ts +24 -0
- package/dist/server/agents/gitScope.js +95 -0
- package/dist/server/agents/modelRejectionHint.d.ts +8 -0
- package/dist/server/agents/modelRejectionHint.js +12 -0
- package/dist/server/agents/prompt.d.ts +79 -0
- package/dist/server/agents/prompt.js +364 -0
- package/dist/server/agents/runManager.d.ts +90 -0
- package/dist/server/agents/runManager.js +259 -0
- package/dist/server/agents/types.d.ts +15 -0
- package/dist/server/agents/types.js +1 -0
- package/dist/server/apiStore.d.ts +60 -0
- package/dist/server/apiStore.js +249 -0
- package/dist/server/auth/enforce.d.ts +1 -0
- package/dist/server/auth/enforce.js +6 -0
- package/dist/server/auth/types.d.ts +1 -0
- package/dist/server/auth/types.js +1 -0
- package/dist/server/auth/verifiers.d.ts +1 -0
- package/dist/server/auth/verifiers.js +4 -0
- package/dist/server/authAttach.d.ts +19 -0
- package/dist/server/authAttach.js +79 -0
- package/dist/server/cli.d.ts +7 -0
- package/dist/server/cli.js +804 -0
- package/dist/server/client.d.ts +130 -0
- package/dist/server/client.js +247 -0
- package/dist/server/environments.d.ts +103 -0
- package/dist/server/environments.js +486 -0
- package/dist/server/flowStore.d.ts +37 -0
- package/dist/server/flowStore.js +117 -0
- package/dist/server/httpOperations.d.ts +32 -0
- package/dist/server/httpOperations.js +62 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1022 -0
- package/dist/server/infrastructure.d.ts +51 -0
- package/dist/server/infrastructure.js +116 -0
- package/dist/server/login.d.ts +21 -0
- package/dist/server/login.js +44 -0
- package/dist/server/mcp.d.ts +1 -0
- package/dist/server/mcp.js +182 -0
- package/dist/server/migrateFlows.d.ts +8 -0
- package/dist/server/migrateFlows.js +25 -0
- package/dist/server/mockHandler.d.ts +15 -0
- package/dist/server/mockHandler.js +35 -0
- package/dist/server/nodesPayload.d.ts +14 -0
- package/dist/server/nodesPayload.js +14 -0
- package/dist/server/normalizeFlow.d.ts +21 -0
- package/dist/server/normalizeFlow.js +48 -0
- package/dist/server/openBrowser.d.ts +8 -0
- package/dist/server/openBrowser.js +16 -0
- package/dist/server/operationResult.d.ts +12 -0
- package/dist/server/operationResult.js +26 -0
- package/dist/server/pathGuard.d.ts +1 -0
- package/dist/server/pathGuard.js +16 -0
- package/dist/server/pathSafety.d.ts +20 -0
- package/dist/server/pathSafety.js +35 -0
- package/dist/server/projectConfig.d.ts +47 -0
- package/dist/server/projectConfig.js +61 -0
- package/dist/server/projectMode.d.ts +26 -0
- package/dist/server/projectMode.js +44 -0
- package/dist/server/redact.d.ts +6 -0
- package/dist/server/redact.js +37 -0
- package/dist/server/runRegistry.d.ts +84 -0
- package/dist/server/runRegistry.js +200 -0
- package/dist/server/runtime/RuntimeAdapter.d.ts +22 -0
- package/dist/server/runtime/RuntimeAdapter.js +1 -0
- package/dist/server/runtime/expressAdapter.d.ts +19 -0
- package/dist/server/runtime/expressAdapter.js +56 -0
- package/dist/server/scenarioTest.d.ts +14 -0
- package/dist/server/scenarioTest.js +66 -0
- package/dist/server/subflowRunner.d.ts +32 -0
- package/dist/server/subflowRunner.js +72 -0
- package/dist/server/testRunner.d.ts +70 -0
- package/dist/server/testRunner.js +151 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +2 -0
- package/dist/src/engine/authVerify.d.ts +33 -0
- package/dist/src/engine/authVerify.js +63 -0
- package/dist/src/engine/diagnostics.d.ts +62 -0
- package/dist/src/engine/diagnostics.js +148 -0
- package/dist/src/engine/executor.d.ts +151 -0
- package/dist/src/engine/executor.js +684 -0
- package/dist/src/engine/httpError.d.ts +7 -0
- package/dist/src/engine/httpError.js +14 -0
- package/dist/src/engine/index.d.ts +11 -0
- package/dist/src/engine/index.js +11 -0
- package/dist/src/engine/output.d.ts +11 -0
- package/dist/src/engine/output.js +20 -0
- package/dist/src/engine/publish.d.ts +30 -0
- package/dist/src/engine/publish.js +95 -0
- package/dist/src/engine/registry.d.ts +34 -0
- package/dist/src/engine/registry.js +67 -0
- package/dist/src/engine/scenarios.d.ts +12 -0
- package/dist/src/engine/scenarios.js +19 -0
- package/dist/src/engine/schemaCheck.d.ts +4 -0
- package/dist/src/engine/schemaCheck.js +34 -0
- package/dist/src/engine/trace.d.ts +12 -0
- package/dist/src/engine/trace.js +12 -0
- package/dist/src/engine/types.d.ts +308 -0
- package/dist/src/engine/types.js +1 -0
- package/dist/src/engine/validation.d.ts +26 -0
- package/dist/src/engine/validation.js +318 -0
- package/dist/src/flows/anomaly-flows.d.ts +23 -0
- package/dist/src/flows/anomaly-flows.js +151 -0
- package/dist/src/flows/city-sweep-flow.d.ts +10 -0
- package/dist/src/flows/city-sweep-flow.js +127 -0
- package/dist/src/flows/login-flow.d.ts +13 -0
- package/dist/src/flows/login-flow.js +145 -0
- package/dist/src/flows/pradar-flows.d.ts +5 -0
- package/dist/src/flows/pradar-flows.js +375 -0
- package/dist/src/flows/weather-flow.d.ts +12 -0
- package/dist/src/flows/weather-flow.js +150 -0
- package/dist/src/lib/pathParams.d.ts +3 -0
- package/dist/src/lib/pathParams.js +6 -0
- package/dist/src/nodes/anomaly.d.ts +2 -0
- package/dist/src/nodes/anomaly.js +556 -0
- package/dist/src/nodes/flow-control.d.ts +7 -0
- package/dist/src/nodes/flow-control.js +270 -0
- package/dist/src/nodes/index.d.ts +3 -0
- package/dist/src/nodes/index.js +20 -0
- package/dist/src/nodes/login.d.ts +7 -0
- package/dist/src/nodes/login.js +158 -0
- package/dist/src/nodes/loops.d.ts +11 -0
- package/dist/src/nodes/loops.js +60 -0
- package/dist/src/nodes/pradar/index.d.ts +7 -0
- package/dist/src/nodes/pradar/index.js +9 -0
- package/dist/src/nodes/pradar/logic.d.ts +189 -0
- package/dist/src/nodes/pradar/logic.js +370 -0
- package/dist/src/nodes/pradar/nodes.d.ts +42 -0
- package/dist/src/nodes/pradar/nodes.js +801 -0
- package/dist/src/nodes/requireAuth.d.ts +20 -0
- package/dist/src/nodes/requireAuth.js +48 -0
- package/dist/src/nodes/response.d.ts +8 -0
- package/dist/src/nodes/response.js +33 -0
- package/dist/src/nodes/weather.d.ts +33 -0
- package/dist/src/nodes/weather.js +260 -0
- package/package.json +92 -0
- package/server/agentRoute.test.ts +157 -0
- package/server/agents/__fixtures__/fake-claude-writer.mjs +23 -0
- package/server/agents/__fixtures__/fake-claude.mjs +55 -0
- package/server/agents/__fixtures__/fake-codex-writer.mjs +27 -0
- package/server/agents/__fixtures__/fake-codex.mjs +52 -0
- package/server/agents/__fixtures__/fake-version-empty.mjs +3 -0
- package/server/agents/__fixtures__/fake-version-fail.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-garbage.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-good.mjs +4 -0
- package/server/agents/__fixtures__/fake-version-hang.mjs +3 -0
- package/server/agents/claudeAdapter.test.ts +127 -0
- package/server/agents/claudeAdapter.ts +212 -0
- package/server/agents/claudeParse.ts +193 -0
- package/server/agents/codexAdapter.test.ts +125 -0
- package/server/agents/codexAdapter.ts +198 -0
- package/server/agents/codexParse.test.ts +58 -0
- package/server/agents/codexParse.ts +126 -0
- package/server/agents/detect.test.ts +65 -0
- package/server/agents/detect.ts +43 -0
- package/server/agents/gitScope.test.ts +129 -0
- package/server/agents/gitScope.ts +110 -0
- package/server/agents/modelRejectionHint.test.ts +30 -0
- package/server/agents/modelRejectionHint.ts +14 -0
- package/server/agents/prompt.test.ts +613 -0
- package/server/agents/prompt.ts +527 -0
- package/server/agents/runManager.test.ts +251 -0
- package/server/agents/runManager.ts +286 -0
- package/server/agents/types.ts +17 -0
- package/server/apiMount.test.ts +591 -0
- package/server/apiStore.test.ts +324 -0
- package/server/apiStore.ts +241 -0
- package/server/auth/enforce.test.ts +61 -0
- package/server/auth/enforce.ts +6 -0
- package/server/auth/types.ts +5 -0
- package/server/auth/verifiers.test.ts +37 -0
- package/server/auth/verifiers.ts +9 -0
- package/server/authAttach.test.ts +175 -0
- package/server/authAttach.ts +99 -0
- package/server/cli.ts +847 -0
- package/server/cliAuth.test.ts +229 -0
- package/server/cliCreate.test.ts +131 -0
- package/server/cliDoctor.test.ts +326 -0
- package/server/cliTest.test.ts +735 -0
- package/server/client.ts +314 -0
- package/server/demoProject.test.ts +29 -0
- package/server/diagnosticsRoute.test.ts +169 -0
- package/server/environments.test.ts +620 -0
- package/server/environments.ts +603 -0
- package/server/environmentsRoute.test.ts +385 -0
- package/server/errorWorkflow.test.ts +205 -0
- package/server/flowStore.test.ts +84 -0
- package/server/flowStore.ts +125 -0
- package/server/httpOperations.test.ts +377 -0
- package/server/httpOperations.ts +96 -0
- package/server/index.ts +1111 -0
- package/server/infrastructure.test.ts +154 -0
- package/server/infrastructure.ts +161 -0
- package/server/infrastructureRoute.test.ts +105 -0
- package/server/login.test.ts +153 -0
- package/server/login.ts +67 -0
- package/server/mcp.ts +278 -0
- package/server/migrateFlows.test.ts +55 -0
- package/server/migrateFlows.ts +23 -0
- package/server/mockHandler.test.ts +138 -0
- package/server/mockHandler.ts +52 -0
- package/server/mockRun.test.ts +367 -0
- package/server/mockServe.test.ts +467 -0
- package/server/nodesPayload.test.ts +29 -0
- package/server/nodesPayload.ts +26 -0
- package/server/normalizeFlow.test.ts +80 -0
- package/server/normalizeFlow.ts +59 -0
- package/server/openBrowser.test.ts +20 -0
- package/server/openBrowser.ts +22 -0
- package/server/operationResult.test.ts +67 -0
- package/server/operationResult.ts +27 -0
- package/server/pathGuard.test.ts +19 -0
- package/server/pathGuard.ts +12 -0
- package/server/pathSafety.test.ts +38 -0
- package/server/pathSafety.ts +33 -0
- package/server/projectConfig.test.ts +67 -0
- package/server/projectConfig.ts +101 -0
- package/server/projectMode.test.ts +106 -0
- package/server/projectMode.ts +59 -0
- package/server/redact.test.ts +88 -0
- package/server/redact.ts +35 -0
- package/server/redactEgress.test.ts +281 -0
- package/server/runRegistry.ts +270 -0
- package/server/runsAuth.test.ts +119 -0
- package/server/runtime/RuntimeAdapter.ts +24 -0
- package/server/runtime/expressAdapter.test.ts +38 -0
- package/server/runtime/expressAdapter.ts +70 -0
- package/server/scenarioTest.test.ts +148 -0
- package/server/scenarioTest.ts +88 -0
- package/server/setupStatus.test.ts +127 -0
- package/server/subflowRunner.ts +104 -0
- package/server/testRunner.ts +216 -0
- package/server/tsconfig.json +20 -0
- package/server/workflowTestRoute.test.ts +297 -0
- package/src/App.tsx +233 -0
- package/src/components/AgentConsole.tsx +461 -0
- package/src/components/BranchRulesEditor.tsx +156 -0
- package/src/components/CreateModal.tsx +275 -0
- package/src/components/Dock.tsx +487 -0
- package/src/components/EnvironmentDialog.tsx +569 -0
- package/src/components/EnvironmentPicker.tsx +357 -0
- package/src/components/EnvironmentsDialog.tsx +144 -0
- package/src/components/ExecutionPager.tsx +67 -0
- package/src/components/InfraPanel.test.tsx +103 -0
- package/src/components/InfraPanel.tsx +171 -0
- package/src/components/Inspector.tsx +1182 -0
- package/src/components/Json.tsx +50 -0
- package/src/components/JsonModal.tsx +52 -0
- package/src/components/LogMessage.tsx +61 -0
- package/src/components/NodeRunModal.tsx +213 -0
- package/src/components/RunLogPanel.tsx +397 -0
- package/src/components/RunbookView.tsx +1085 -0
- package/src/components/Scenarios.tsx +476 -0
- package/src/components/SettingsDialog.tsx +229 -0
- package/src/components/Sidebar.tsx +436 -0
- package/src/components/StatusBar.tsx +302 -0
- package/src/components/Toolbar.tsx +663 -0
- package/src/components/WelcomeDialog.test.tsx +107 -0
- package/src/components/WelcomeDialog.tsx +412 -0
- package/src/components/agentMarkdown.test.tsx +91 -0
- package/src/components/agentMarkdown.tsx +158 -0
- package/src/components/ui/badge.tsx +31 -0
- package/src/components/ui/button.tsx +46 -0
- package/src/components/ui/combobox.tsx +114 -0
- package/src/components/ui/command.tsx +82 -0
- package/src/components/ui/dialog.tsx +73 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/popover.tsx +32 -0
- package/src/components/ui/resizable.tsx +44 -0
- package/src/components/ui/tabs.tsx +36 -0
- package/src/config.ts +4 -0
- package/src/engine/authVerify.ts +81 -0
- package/src/engine/diagnostics.test.ts +437 -0
- package/src/engine/diagnostics.ts +211 -0
- package/src/engine/executor.test.ts +1339 -0
- package/src/engine/executor.ts +840 -0
- package/src/engine/httpError.test.ts +12 -0
- package/src/engine/httpError.ts +16 -0
- package/src/engine/index.ts +11 -0
- package/src/engine/output.ts +21 -0
- package/src/engine/publish.test.ts +176 -0
- package/src/engine/publish.ts +127 -0
- package/src/engine/registry.test.ts +64 -0
- package/src/engine/registry.ts +80 -0
- package/src/engine/scenarios.test.ts +55 -0
- package/src/engine/scenarios.ts +29 -0
- package/src/engine/schemaCheck.test.ts +21 -0
- package/src/engine/schemaCheck.ts +35 -0
- package/src/engine/trace.test.ts +19 -0
- package/src/engine/trace.ts +23 -0
- package/src/engine/types.test.ts +22 -0
- package/src/engine/types.ts +316 -0
- package/src/engine/validation.test.ts +314 -0
- package/src/engine/validation.ts +354 -0
- package/src/flows/anomaly-flows.test.ts +81 -0
- package/src/flows/anomaly-flows.ts +202 -0
- package/src/flows/city-sweep-flow.ts +131 -0
- package/src/flows/flows.test.ts +30 -0
- package/src/flows/login-flow.ts +150 -0
- package/src/flows/pradar-flows.test.ts +204 -0
- package/src/flows/pradar-flows.ts +419 -0
- package/src/flows/weather-flow.ts +154 -0
- package/src/globals.css +147 -0
- package/src/lib/pathParams.test.ts +24 -0
- package/src/lib/pathParams.ts +6 -0
- package/src/lib/registerLens.test.ts +79 -0
- package/src/lib/registerLens.ts +59 -0
- package/src/lib/runbookModel.test.ts +61 -0
- package/src/lib/runbookModel.ts +290 -0
- package/src/lib/runbookProjection.test.ts +417 -0
- package/src/lib/runbookProjection.ts +254 -0
- package/src/lib/useTailAnchor.ts +53 -0
- package/src/lib/utils.ts +7 -0
- package/src/main.tsx +13 -0
- package/src/nodes/anomaly.test.ts +98 -0
- package/src/nodes/anomaly.ts +637 -0
- package/src/nodes/effects-scan.test.ts +44 -0
- package/src/nodes/flow-control.test.ts +280 -0
- package/src/nodes/flow-control.ts +305 -0
- package/src/nodes/index.ts +22 -0
- package/src/nodes/login.test.ts +81 -0
- package/src/nodes/login.ts +186 -0
- package/src/nodes/loops.ts +69 -0
- package/src/nodes/pradar/index.ts +11 -0
- package/src/nodes/pradar/logic.test.ts +288 -0
- package/src/nodes/pradar/logic.ts +582 -0
- package/src/nodes/pradar/nodes.test.ts +146 -0
- package/src/nodes/pradar/nodes.ts +955 -0
- package/src/nodes/requireAuth.test.ts +75 -0
- package/src/nodes/requireAuth.ts +55 -0
- package/src/nodes/response.test.ts +33 -0
- package/src/nodes/response.ts +39 -0
- package/src/nodes/traceDetail.test.ts +39 -0
- package/src/nodes/weather.test.ts +65 -0
- package/src/nodes/weather.ts +306 -0
- package/src/store/agentClient.test.ts +103 -0
- package/src/store/agentClient.ts +131 -0
- package/src/store/apiTree.test.ts +17 -0
- package/src/store/apiTree.ts +73 -0
- package/src/store/builderStore.test.ts +1234 -0
- package/src/store/builderStore.ts +1879 -0
- package/src/store/infraClient.ts +63 -0
- package/src/store/nodeMeta.test.ts +24 -0
- package/src/store/nodeMeta.ts +21 -0
- package/src/store/persistence.test.ts +15 -0
- package/src/store/persistence.ts +54 -0
- package/src/store/serverRunner.ts +381 -0
- package/src/store/setupClient.ts +57 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/studio-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/studio-dist/assets/index-DNJwW-hM.css +1 -0
- package/studio-dist/assets/index-O26dKRjW.js +65 -0
- package/studio-dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/studio-dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/studio-dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/studio-dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/studio-dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/studio-dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/studio-dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/studio-dist/favicon.svg +1 -0
- package/studio-dist/icons.svg +24 -0
- package/studio-dist/index.html +14 -0
- package/templates/skills/emberflow-basics/SKILL.md +316 -0
- package/templates/skills/emberflow-model-process/SKILL.md +209 -0
- package/templates/skills/emberflow-new-workflow/SKILL.md +211 -0
- 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
|
+
});
|