@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
package/dist/bin/init.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { detectHarnesses, resolveSkillDirs } from './skillTargets.js';
|
|
7
|
+
const MCP_SNIPPET = {
|
|
8
|
+
mcpServers: {
|
|
9
|
+
emberflow: { command: 'npx', args: ['emberflow', 'mcp'] },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
function copySkillsRecursive(src, dest, log) {
|
|
13
|
+
mkdirSync(dest, { recursive: true });
|
|
14
|
+
for (const entry of readdirSync(src)) {
|
|
15
|
+
const srcPath = join(src, entry);
|
|
16
|
+
const destPath = join(dest, entry);
|
|
17
|
+
if (statSync(srcPath).isDirectory()) {
|
|
18
|
+
copySkillsRecursive(srcPath, destPath, log);
|
|
19
|
+
}
|
|
20
|
+
else if (!existsSync(destPath)) {
|
|
21
|
+
copyFileSync(srcPath, destPath);
|
|
22
|
+
log.push(destPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const JS_CONFIG_CONTENT = "import { defineConfig } from '@xdelivered/emberflow';\n\n" +
|
|
27
|
+
'export default defineConfig({\n' +
|
|
28
|
+
" language: 'javascript',\n" +
|
|
29
|
+
' // Anchor only: this path is never created and holds no files. It just tells\n' +
|
|
30
|
+
' // Emberflow where your project root is — operations always live in the\n' +
|
|
31
|
+
' // SIBLING apis/ tree next to it:\n' +
|
|
32
|
+
' // emberflow/apis/<api>/<folder…>/<op>.json — each op id = its path under apis/.\n' +
|
|
33
|
+
" flowsDir: 'emberflow/flows',\n" +
|
|
34
|
+
' // Author your nodes here: registry.register(definition, async (ctx) => output).\n' +
|
|
35
|
+
" /** @param {import('@xdelivered/emberflow').NodeRegistry} registry */\n" +
|
|
36
|
+
' registerNodes(registry) {\n' +
|
|
37
|
+
' /* register your nodes */\n' +
|
|
38
|
+
' },\n' +
|
|
39
|
+
'});\n';
|
|
40
|
+
const TS_CONFIG_CONTENT = "import { defineConfig } from '@xdelivered/emberflow';\n" +
|
|
41
|
+
"import type { NodeRegistry } from '@xdelivered/emberflow';\n\n" +
|
|
42
|
+
'export default defineConfig({\n' +
|
|
43
|
+
" language: 'typescript',\n" +
|
|
44
|
+
' // Anchor only: this path is never created and holds no files. It just tells\n' +
|
|
45
|
+
' // Emberflow where your project root is — operations always live in the\n' +
|
|
46
|
+
' // SIBLING apis/ tree next to it:\n' +
|
|
47
|
+
' // emberflow/apis/<api>/<folder…>/<op>.json — each op id = its path under apis/.\n' +
|
|
48
|
+
" flowsDir: 'emberflow/flows',\n" +
|
|
49
|
+
' // Author your nodes here: registry.register(definition, async (ctx) => output).\n' +
|
|
50
|
+
' registerNodes(registry: NodeRegistry) {\n' +
|
|
51
|
+
' /* register your nodes */\n' +
|
|
52
|
+
' },\n' +
|
|
53
|
+
'});\n';
|
|
54
|
+
// Minimal — the consumer only needs their config + nodes typechecked, not a full
|
|
55
|
+
// project tsconfig. `noEmit` because emberflow.config.ts runs via tsx, never tsc.
|
|
56
|
+
const TSCONFIG_CONTENT = JSON.stringify({
|
|
57
|
+
compilerOptions: {
|
|
58
|
+
target: 'es2022',
|
|
59
|
+
module: 'nodenext',
|
|
60
|
+
moduleResolution: 'nodenext',
|
|
61
|
+
strict: true,
|
|
62
|
+
skipLibCheck: true,
|
|
63
|
+
esModuleInterop: true,
|
|
64
|
+
resolveJsonModule: true,
|
|
65
|
+
noEmit: true,
|
|
66
|
+
},
|
|
67
|
+
include: ['emberflow.config.ts', 'emberflow/**/*'],
|
|
68
|
+
}, null, 2) + '\n';
|
|
69
|
+
/** Whether `tsx` resolves from the project's own node_modules (peer dep — optional).
|
|
70
|
+
* Exported so bin/commands.ts can reuse it when deciding whether `init --ts` can
|
|
71
|
+
* launch dev immediately after scaffolding, or must defer to the user. */
|
|
72
|
+
export function tsxResolvable(cwd) {
|
|
73
|
+
try {
|
|
74
|
+
createRequire(join(cwd, 'package.json')).resolve('tsx');
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// The example HTTP operation: GET /hello, Input({params,query,body,headers}) →
|
|
82
|
+
// Response({status,body}). Its `id` equals its path under the apis dir.
|
|
83
|
+
const HELLO_OP = {
|
|
84
|
+
id: 'default/hello',
|
|
85
|
+
name: 'Hello',
|
|
86
|
+
version: 1,
|
|
87
|
+
http: { method: 'GET', path: '/hello' },
|
|
88
|
+
nodes: [
|
|
89
|
+
{
|
|
90
|
+
id: 'input',
|
|
91
|
+
type: 'Input',
|
|
92
|
+
label: 'Request',
|
|
93
|
+
position: { x: 0, y: 0 },
|
|
94
|
+
config: {
|
|
95
|
+
fields: [
|
|
96
|
+
{ name: 'params', type: 'object' },
|
|
97
|
+
{ name: 'query', type: 'object' },
|
|
98
|
+
{ name: 'body', type: 'object' },
|
|
99
|
+
{ name: 'headers', type: 'object' },
|
|
100
|
+
],
|
|
101
|
+
defaults: { query: { name: 'world' } },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'response',
|
|
106
|
+
type: 'Response',
|
|
107
|
+
label: 'Response',
|
|
108
|
+
position: { x: 300, y: 0 },
|
|
109
|
+
config: {},
|
|
110
|
+
inputMap: {
|
|
111
|
+
body: { sourceNodeId: 'input', sourceField: 'query' },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
edges: [{ id: 'e0', source: 'input', target: 'response', targetHandle: 'body' }],
|
|
116
|
+
createdAt: '2026-07-05T00:00:00Z',
|
|
117
|
+
updatedAt: '2026-07-05T00:00:00Z',
|
|
118
|
+
};
|
|
119
|
+
const HELLO_OP_SCENARIOS = [{ id: 's-hi', name: 'greet', input: { query: { name: 'Ember' } } }];
|
|
120
|
+
// Emberflow infra that is machine-local, generated, or secret — never committed.
|
|
121
|
+
// NOTE: emberflow.config.mjs and emberflow/ (apis + scenarios) are deliberately
|
|
122
|
+
// NOT here — those are the value and SHOULD be checked in.
|
|
123
|
+
const GITIGNORE_HEADER = '# Emberflow (local infra — emberflow.config.mjs and emberflow/apis ARE committed)';
|
|
124
|
+
const GITIGNORE_LINES = [
|
|
125
|
+
'node_modules/',
|
|
126
|
+
'studio-dist/',
|
|
127
|
+
'emberflow.secrets.json',
|
|
128
|
+
'emberflow.environments.json',
|
|
129
|
+
];
|
|
130
|
+
function writeFileIfAbsent(path, content, log) {
|
|
131
|
+
if (existsSync(path))
|
|
132
|
+
return;
|
|
133
|
+
writeFileSync(path, content);
|
|
134
|
+
log.push(path);
|
|
135
|
+
}
|
|
136
|
+
export async function runInit(cwd, opts) {
|
|
137
|
+
const written = [];
|
|
138
|
+
const language = opts?.language ?? 'javascript';
|
|
139
|
+
// Refuse to scaffold a SECOND config in another extension: the loader probes
|
|
140
|
+
// .mjs before .js before .ts, so a new config of a different flavor would sit
|
|
141
|
+
// dead on disk while init claims success. Language switching is a manual
|
|
142
|
+
// migration (rename/port the existing config), not a re-init.
|
|
143
|
+
const existingOther = ['emberflow.config.mjs', 'emberflow.config.js', 'emberflow.config.ts']
|
|
144
|
+
.filter((name) => (language === 'typescript' ? !name.endsWith('.ts') : name.endsWith('.ts')))
|
|
145
|
+
.find((name) => existsSync(join(cwd, name)));
|
|
146
|
+
if (existingOther) {
|
|
147
|
+
console.error(`[emberflow] this project already has ${existingOther} — refusing to scaffold a ${language} config alongside it. ` +
|
|
148
|
+
`To switch languages, port ${existingOther} manually and delete it.`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
if (language === 'typescript') {
|
|
152
|
+
writeFileIfAbsent(join(cwd, 'emberflow.config.ts'), TS_CONFIG_CONTENT, written);
|
|
153
|
+
writeFileIfAbsent(join(cwd, 'tsconfig.json'), TSCONFIG_CONTENT, written);
|
|
154
|
+
if (!tsxResolvable(cwd)) {
|
|
155
|
+
console.log('[emberflow] TypeScript config needs tsx to run this project.');
|
|
156
|
+
console.log(' npm i -D tsx typescript');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
writeFileIfAbsent(join(cwd, 'emberflow.config.mjs'), JS_CONFIG_CONTENT, written);
|
|
161
|
+
}
|
|
162
|
+
// Scaffold the apis/ tree with an example HTTP operation so the API host has a
|
|
163
|
+
// routed endpoint the moment the project boots. The operation's `id`
|
|
164
|
+
// ("default/hello") equals its path under the apis dir.
|
|
165
|
+
const apisDefaultDir = join(cwd, 'emberflow', 'apis', 'default');
|
|
166
|
+
const apisDefaultDirExisted = existsSync(apisDefaultDir);
|
|
167
|
+
mkdirSync(apisDefaultDir, { recursive: true });
|
|
168
|
+
if (!apisDefaultDirExisted)
|
|
169
|
+
written.push(apisDefaultDir + '/');
|
|
170
|
+
writeFileIfAbsent(join(apisDefaultDir, 'hello.json'), JSON.stringify(HELLO_OP, null, 2) + '\n', written);
|
|
171
|
+
writeFileIfAbsent(join(apisDefaultDir, 'hello.scenarios.json'), JSON.stringify(HELLO_OP_SCENARIOS, null, 2) + '\n', written);
|
|
172
|
+
const pkgPath = join(cwd, 'package.json');
|
|
173
|
+
if (existsSync(pkgPath)) {
|
|
174
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
175
|
+
let pkgDirty = false;
|
|
176
|
+
if (!pkg.scripts)
|
|
177
|
+
pkg.scripts = {};
|
|
178
|
+
if (!pkg.scripts.emberflow) {
|
|
179
|
+
pkg.scripts.emberflow = 'emberflow dev';
|
|
180
|
+
pkgDirty = true;
|
|
181
|
+
}
|
|
182
|
+
// A TypeScript project MUST load as ESM: `emberflow` is an ESM-only package,
|
|
183
|
+
// and under a CommonJS package.json tsx transpiles emberflow.config.ts to CJS
|
|
184
|
+
// and then require()s the ESM `emberflow` — which Node rejects with
|
|
185
|
+
// ERR_REQUIRE_CYCLE_MODULE. Stamp "type":"module" when the project hasn't
|
|
186
|
+
// declared one. (JS scaffolds a .mjs config, which is ESM regardless.)
|
|
187
|
+
if (language === 'typescript') {
|
|
188
|
+
if (pkg.type === undefined) {
|
|
189
|
+
pkg.type = 'module';
|
|
190
|
+
pkgDirty = true;
|
|
191
|
+
console.warn('[emberflow] set "type": "module" in package.json — required for emberflow.config.ts; ' +
|
|
192
|
+
'if this project has existing CommonJS .js files, audit them (they are now parsed as ESM)');
|
|
193
|
+
}
|
|
194
|
+
else if (pkg.type !== 'module') {
|
|
195
|
+
console.warn(`[emberflow] warning: package.json has "type": "${pkg.type}", but a TypeScript ` +
|
|
196
|
+
'emberflow.config.ts must load as ESM. Set "type": "module" or the runner will fail ' +
|
|
197
|
+
'to load your config.');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (pkgDirty) {
|
|
201
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
202
|
+
written.push(pkgPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
206
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : '';
|
|
207
|
+
const missing = GITIGNORE_LINES.filter((line) => !existing.includes(line));
|
|
208
|
+
if (missing.length > 0) {
|
|
209
|
+
// Create .gitignore if absent, or append only the lines not already present.
|
|
210
|
+
const header = existing.includes(GITIGNORE_HEADER) ? '' : `${GITIGNORE_HEADER}\n`;
|
|
211
|
+
const sep = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
212
|
+
const block = (existing.length > 0 ? '\n' : '') + header + missing.join('\n') + '\n';
|
|
213
|
+
writeFileSync(gitignorePath, existing + sep + block);
|
|
214
|
+
written.push(gitignorePath);
|
|
215
|
+
}
|
|
216
|
+
for (const path of written) {
|
|
217
|
+
console.log(`[emberflow] created ${path}`);
|
|
218
|
+
}
|
|
219
|
+
const skillsOpt = opts?.skills === undefined ? { scope: 'repo', home: homedir() } : opts.skills;
|
|
220
|
+
if (skillsOpt !== false) {
|
|
221
|
+
const pkgRoot = opts?.packageRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
222
|
+
const skillsSrc = join(pkgRoot, 'templates', 'skills');
|
|
223
|
+
if (existsSync(skillsSrc)) {
|
|
224
|
+
const presence = detectHarnesses(cwd, skillsOpt.home);
|
|
225
|
+
const skillDirs = resolveSkillDirs(presence, skillsOpt.scope, cwd, skillsOpt.home);
|
|
226
|
+
const skillsWritten = [];
|
|
227
|
+
for (const dir of skillDirs) {
|
|
228
|
+
copySkillsRecursive(skillsSrc, dir, skillsWritten);
|
|
229
|
+
}
|
|
230
|
+
for (const path of skillsWritten) {
|
|
231
|
+
console.log(`[emberflow] created ${path}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log('[emberflow] To let your agent drive Emberflow directly, register the emberflow MCP server:');
|
|
236
|
+
console.log(" - Claude Code: add the snippet below to .mcp.json at your project root.");
|
|
237
|
+
console.log(" - Other agents: add it to your agent's MCP server config (name/location varies by tool).");
|
|
238
|
+
console.log(JSON.stringify(MCP_SNIPPET, null, 2));
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The emberflow.config.* basename present in `dir`, or undefined. Local probe
|
|
2
|
+
* of the same basenames server/projectConfig.ts#configPathFor checks. */
|
|
3
|
+
export function configBasenameFor(dir: any, fsExists?: typeof existsSync): string | undefined;
|
|
4
|
+
/** Resolve the project directory the same way the commands do, in the binding
|
|
5
|
+
* order EMBERFLOW_PROJECT → --project → cwd. Values are resolved against cwd. */
|
|
6
|
+
export function resolveProjectDir(argv: any, env: any, cwd: any): any;
|
|
7
|
+
/**
|
|
8
|
+
* Decide how to run.
|
|
9
|
+
*
|
|
10
|
+
* @param packageRoot absolute path to the package root (dir above bin/).
|
|
11
|
+
* @param projectDir resolved project dir (see resolveProjectDir).
|
|
12
|
+
* @param env process.env-shaped object.
|
|
13
|
+
* @param fsExists existsSync-shaped probe (injectable for tests).
|
|
14
|
+
*
|
|
15
|
+
* Returns:
|
|
16
|
+
* needsTsx – register tsx and spawn the runner under tsx.
|
|
17
|
+
* runnerMode – 'node' (spawn `node dist/server/*.js`) or 'tsx' (spawn tsx source).
|
|
18
|
+
* useDist – import the compiled `dist/bin/commands.js` rather than source.
|
|
19
|
+
* sourceMode – repo checkout (dev loop) rather than an installed package.
|
|
20
|
+
* configBasename / reason – for the EMBERFLOW_DEBUG_RUNTIME trace.
|
|
21
|
+
*/
|
|
22
|
+
export function decideRuntime({ packageRoot, projectDir, env, fsExists }: {
|
|
23
|
+
packageRoot: any;
|
|
24
|
+
projectDir: any;
|
|
25
|
+
env?: {} | undefined;
|
|
26
|
+
fsExists?: typeof existsSync | undefined;
|
|
27
|
+
}): {
|
|
28
|
+
needsTsx: boolean;
|
|
29
|
+
runnerMode: string;
|
|
30
|
+
useDist: boolean;
|
|
31
|
+
sourceMode: boolean;
|
|
32
|
+
hasDist: boolean;
|
|
33
|
+
configBasename: string | undefined;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
/** Actionable message shown when a `.ts` config / source mode needs tsx but it
|
|
37
|
+
* isn't installed. Exported so both the bin and its tests reference one string. */
|
|
38
|
+
export const TSX_MISSING_MESSAGE: string;
|
|
39
|
+
import { existsSync } from 'node:fs';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Runtime-mode decision for the Emberflow bin.
|
|
2
|
+
//
|
|
3
|
+
// The bin must decide — BEFORE loading any TypeScript — whether this invocation
|
|
4
|
+
// needs the tsx loader (source checkout, a `.ts` project config, or an explicit
|
|
5
|
+
// override) or can run on plain Node against the compiled `dist/`. A JS consumer
|
|
6
|
+
// with a `.mjs`/`.js` config runs entirely on `node` with NO tsx: no tsx child
|
|
7
|
+
// IPC, so the agent in-process CLI works inside the codex sandbox.
|
|
8
|
+
//
|
|
9
|
+
// This module is plain JS on purpose — it imports NO server/TS code, so probing
|
|
10
|
+
// it never triggers the tsx requirement it is trying to decide about. Everything
|
|
11
|
+
// here is pure and injectable (fsExists/env) so the decision matrix is unit
|
|
12
|
+
// testable without touching a real filesystem.
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
15
|
+
// Mirrors server/projectConfig.ts CONFIG_BASENAMES. Kept as a tiny local copy
|
|
16
|
+
// (not imported) because that module is TypeScript and pulls in server code.
|
|
17
|
+
const CONFIG_BASENAMES = ['emberflow.config.mjs', 'emberflow.config.js', 'emberflow.config.ts'];
|
|
18
|
+
/** Actionable message shown when a `.ts` config / source mode needs tsx but it
|
|
19
|
+
* isn't installed. Exported so both the bin and its tests reference one string. */
|
|
20
|
+
export const TSX_MISSING_MESSAGE = '[emberflow] This project needs tsx to run, but tsx is not installed.\n' +
|
|
21
|
+
' Either a TypeScript config (emberflow.config.ts) or source mode requires the tsx loader.\n' +
|
|
22
|
+
' Fix: install it as a dev dependency — npm i -D tsx\n' +
|
|
23
|
+
' Or: rename emberflow.config.ts to emberflow.config.mjs (plain JS) so Emberflow runs on plain Node.';
|
|
24
|
+
/** The emberflow.config.* basename present in `dir`, or undefined. Local probe
|
|
25
|
+
* of the same basenames server/projectConfig.ts#configPathFor checks. */
|
|
26
|
+
export function configBasenameFor(dir, fsExists = existsSync) {
|
|
27
|
+
const root = resolve(dir);
|
|
28
|
+
return CONFIG_BASENAMES.find((b) => fsExists(join(root, b)));
|
|
29
|
+
}
|
|
30
|
+
/** Resolve the project directory the same way the commands do, in the binding
|
|
31
|
+
* order EMBERFLOW_PROJECT → --project → cwd. Values are resolved against cwd. */
|
|
32
|
+
export function resolveProjectDir(argv, env, cwd) {
|
|
33
|
+
const fromEnv = env.EMBERFLOW_PROJECT;
|
|
34
|
+
if (fromEnv)
|
|
35
|
+
return isAbsolute(fromEnv) ? fromEnv : resolve(cwd, fromEnv);
|
|
36
|
+
const i = argv.indexOf('--project');
|
|
37
|
+
if (i !== -1 && argv[i + 1])
|
|
38
|
+
return resolve(cwd, argv[i + 1]);
|
|
39
|
+
return cwd;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Decide how to run.
|
|
43
|
+
*
|
|
44
|
+
* @param packageRoot absolute path to the package root (dir above bin/).
|
|
45
|
+
* @param projectDir resolved project dir (see resolveProjectDir).
|
|
46
|
+
* @param env process.env-shaped object.
|
|
47
|
+
* @param fsExists existsSync-shaped probe (injectable for tests).
|
|
48
|
+
*
|
|
49
|
+
* Returns:
|
|
50
|
+
* needsTsx – register tsx and spawn the runner under tsx.
|
|
51
|
+
* runnerMode – 'node' (spawn `node dist/server/*.js`) or 'tsx' (spawn tsx source).
|
|
52
|
+
* useDist – import the compiled `dist/bin/commands.js` rather than source.
|
|
53
|
+
* sourceMode – repo checkout (dev loop) rather than an installed package.
|
|
54
|
+
* configBasename / reason – for the EMBERFLOW_DEBUG_RUNTIME trace.
|
|
55
|
+
*/
|
|
56
|
+
export function decideRuntime({ packageRoot, projectDir, env = {}, fsExists = existsSync }) {
|
|
57
|
+
const hasDist = fsExists(join(packageRoot, 'dist'));
|
|
58
|
+
const hasSrc = fsExists(join(packageRoot, 'src'));
|
|
59
|
+
const underNodeModules = packageRoot.split(sep).includes('node_modules');
|
|
60
|
+
// source-mode = repo checkout, where we always run the TypeScript sources under
|
|
61
|
+
// tsx (the dev loop). `dist/` may EXIST in the repo after `build:lib`, so its
|
|
62
|
+
// presence can't be the signal. Heuristic: src/ exists AND we're not installed
|
|
63
|
+
// under node_modules. EMBERFLOW_SOURCE is an explicit escape hatch either way.
|
|
64
|
+
let sourceMode;
|
|
65
|
+
if (env.EMBERFLOW_SOURCE === '1')
|
|
66
|
+
sourceMode = true;
|
|
67
|
+
else if (env.EMBERFLOW_SOURCE === '0')
|
|
68
|
+
sourceMode = false;
|
|
69
|
+
else
|
|
70
|
+
sourceMode = hasSrc && !underNodeModules;
|
|
71
|
+
const configBasename = configBasenameFor(projectDir, fsExists);
|
|
72
|
+
const tsConfig = configBasename?.endsWith('.ts') ?? false;
|
|
73
|
+
const forced = env.EMBERFLOW_FORCE_TSX === '1';
|
|
74
|
+
// Import compiled commands only when we have a dist AND we're not in the source
|
|
75
|
+
// dev loop. Otherwise fall back to the TypeScript source (which requires tsx —
|
|
76
|
+
// and needsTsx is already true whenever sourceMode is).
|
|
77
|
+
const useDist = hasDist && !sourceMode;
|
|
78
|
+
// INVARIANT: importing TypeScript source requires tsx. When there is no dist
|
|
79
|
+
// to fall back on (`!useDist`), the launcher will import ./commands.ts, so tsx
|
|
80
|
+
// is required even if nothing else demanded it — otherwise plain node dies
|
|
81
|
+
// with a cryptic ERR_UNKNOWN_FILE_EXTENSION instead of TSX_MISSING_MESSAGE.
|
|
82
|
+
const needsTsx = sourceMode || forced || tsConfig || !useDist;
|
|
83
|
+
// A node-mode runner needs the compiled server; if there's no dist we can't run
|
|
84
|
+
// plain node, so fall back to tsx even when nothing else demanded it.
|
|
85
|
+
const runnerMode = !needsTsx && hasDist ? 'node' : 'tsx';
|
|
86
|
+
const reason = sourceMode
|
|
87
|
+
? 'source-mode (repo checkout)'
|
|
88
|
+
: forced
|
|
89
|
+
? 'EMBERFLOW_FORCE_TSX=1'
|
|
90
|
+
: tsConfig
|
|
91
|
+
? `ts-config (${configBasename})`
|
|
92
|
+
: runnerMode === 'node'
|
|
93
|
+
? 'js-consumer (plain node)'
|
|
94
|
+
: 'no-dist fallback';
|
|
95
|
+
return { needsTsx, runnerMode, useDist, sourceMode, hasDist, configBasename, reason };
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Harness = 'claude' | 'codex';
|
|
2
|
+
export interface HarnessPresence {
|
|
3
|
+
claude: boolean;
|
|
4
|
+
codex: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Detect which harnesses the project/user uses: a harness counts as present if
|
|
7
|
+
* its repo dir (<cwd>/.claude or /.codex) OR its home dir (<home>/.claude or /.codex) exists. */
|
|
8
|
+
export declare function detectHarnesses(cwd: string, home: string): HarnessPresence;
|
|
9
|
+
/** The skills destination dirs to install into. For each PRESENT harness, choose
|
|
10
|
+
* the repo root (<cwd>/.<harness>/skills) when scope='repo', else the home root
|
|
11
|
+
* (<home>/.<harness>/skills). If NEITHER harness is present, default to Claude Code
|
|
12
|
+
* at the chosen scope (so a fresh project still gets skills). Returns absolute dirs. */
|
|
13
|
+
export declare function resolveSkillDirs(presence: HarnessPresence, scope: 'repo' | 'global', cwd: string, home: string): string[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const HARNESSES = ['claude', 'codex'];
|
|
4
|
+
/** Detect which harnesses the project/user uses: a harness counts as present if
|
|
5
|
+
* its repo dir (<cwd>/.claude or /.codex) OR its home dir (<home>/.claude or /.codex) exists. */
|
|
6
|
+
export function detectHarnesses(cwd, home) {
|
|
7
|
+
const presence = {};
|
|
8
|
+
for (const h of HARNESSES) {
|
|
9
|
+
presence[h] = existsSync(join(cwd, `.${h}`)) || existsSync(join(home, `.${h}`));
|
|
10
|
+
}
|
|
11
|
+
return presence;
|
|
12
|
+
}
|
|
13
|
+
/** The skills destination dirs to install into. For each PRESENT harness, choose
|
|
14
|
+
* the repo root (<cwd>/.<harness>/skills) when scope='repo', else the home root
|
|
15
|
+
* (<home>/.<harness>/skills). If NEITHER harness is present, default to Claude Code
|
|
16
|
+
* at the chosen scope (so a fresh project still gets skills). Returns absolute dirs. */
|
|
17
|
+
export function resolveSkillDirs(presence, scope, cwd, home) {
|
|
18
|
+
const root = scope === 'repo' ? cwd : home;
|
|
19
|
+
const present = HARNESSES.filter((h) => presence[h]);
|
|
20
|
+
const targets = present.length > 0 ? present : ['claude'];
|
|
21
|
+
return targets.map((h) => join(root, `.${h}`, 'skills'));
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SpawnedAgent } from './codexAdapter.js';
|
|
2
|
+
export interface ClaudeOptions {
|
|
3
|
+
model?: string;
|
|
4
|
+
bin?: string;
|
|
5
|
+
/** Grace period (ms) between SIGTERM and the SIGKILL escalation in cancel(). Test hook. */
|
|
6
|
+
killGraceMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function spawnClaude(prompt: string, projectDir: string, opts?: ClaudeOptions): SpawnedAgent;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { parseClaudeLine } from './claudeParse.js';
|
|
4
|
+
import { modelRejectionHint } from './modelRejectionHint.js';
|
|
5
|
+
const STDERR_TAIL_BYTES = 2048;
|
|
6
|
+
export function spawnClaude(prompt, projectDir, opts = {}) {
|
|
7
|
+
const bin = opts.bin ?? 'claude';
|
|
8
|
+
const args = [
|
|
9
|
+
'-p',
|
|
10
|
+
prompt,
|
|
11
|
+
'--output-format',
|
|
12
|
+
'stream-json',
|
|
13
|
+
'--verbose',
|
|
14
|
+
// Never ask for per-call approval — the studio runs claude non-interactively
|
|
15
|
+
// (-p + SSE panel), so an approval prompt has no one to answer it and the
|
|
16
|
+
// command simply fails ("This command requires approval"), which broke even
|
|
17
|
+
// our own CLI calls (list-environments). Mirrors the codex adapter's
|
|
18
|
+
// never-ask contract; the --add-dir working-directory sandbox still fences
|
|
19
|
+
// file reads/edits to the project.
|
|
20
|
+
'--permission-mode',
|
|
21
|
+
'bypassPermissions',
|
|
22
|
+
'--add-dir',
|
|
23
|
+
projectDir,
|
|
24
|
+
...(opts.model ? ['--model', opts.model] : []),
|
|
25
|
+
];
|
|
26
|
+
// detached: give claude its own process group so cancel() can tree-kill any
|
|
27
|
+
// subprocesses it spawns, not just the top-level process.
|
|
28
|
+
// stdio ['ignore','pipe','pipe'] → stdin is null, stdout/stderr are Readable.
|
|
29
|
+
const child = bin.endsWith('.mjs')
|
|
30
|
+
? spawn(process.execPath, [bin, ...args], { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], detached: true })
|
|
31
|
+
: spawn(bin, args, { cwd: projectDir, stdio: ['ignore', 'pipe', 'pipe'], detached: true });
|
|
32
|
+
// Buffer only the last ~2KB of stderr — non-fatal in general, but useful as
|
|
33
|
+
// a diagnostic tail when the process dies without a terminal event (see
|
|
34
|
+
// maybeFinalize below).
|
|
35
|
+
let stderrTail = '';
|
|
36
|
+
child.stderr.on('data', (chunk) => {
|
|
37
|
+
stderrTail = (stderrTail + chunk.toString('utf8')).slice(-STDERR_TAIL_BYTES);
|
|
38
|
+
});
|
|
39
|
+
const queue = [];
|
|
40
|
+
const resolvers = [];
|
|
41
|
+
let done = false;
|
|
42
|
+
let sawTerminal = false;
|
|
43
|
+
function push(event) {
|
|
44
|
+
queue.push(event);
|
|
45
|
+
const resolve = resolvers.shift();
|
|
46
|
+
if (resolve)
|
|
47
|
+
resolve();
|
|
48
|
+
}
|
|
49
|
+
function finish() {
|
|
50
|
+
done = true;
|
|
51
|
+
while (resolvers.length) {
|
|
52
|
+
const resolve = resolvers.shift();
|
|
53
|
+
if (resolve)
|
|
54
|
+
resolve();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Tracks in-flight Bash commands by their tool_use id, so that when the
|
|
58
|
+
// matching tool_result (a separate 'user' stream-json line) arrives we can
|
|
59
|
+
// recover the original command text — the tool_result line only carries the
|
|
60
|
+
// id + output, not the command. AgentConsole dedups an in_progress command
|
|
61
|
+
// event with a later completed one by matching `command` text, so the
|
|
62
|
+
// completed event we emit here must reuse the same text.
|
|
63
|
+
const pendingCommands = new Map();
|
|
64
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
65
|
+
rl.on('line', (line) => {
|
|
66
|
+
for (const event of parseClaudeLine(line)) {
|
|
67
|
+
if (event.type === 'done' || event.type === 'error')
|
|
68
|
+
sawTerminal = true;
|
|
69
|
+
if (event.type === 'command' && event.commandStatus === 'in_progress' && event.toolUseId && event.command) {
|
|
70
|
+
pendingCommands.set(event.toolUseId, event.command);
|
|
71
|
+
push(event);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (event.type === 'command' && event.commandStatus !== 'in_progress' && event.toolUseId) {
|
|
75
|
+
// A tool_result — only surface it if it corresponds to a Bash command
|
|
76
|
+
// we're tracking; skip gracefully otherwise (e.g. a non-Bash tool).
|
|
77
|
+
const command = pendingCommands.get(event.toolUseId);
|
|
78
|
+
if (!command)
|
|
79
|
+
continue;
|
|
80
|
+
pendingCommands.delete(event.toolUseId);
|
|
81
|
+
push({ ...event, command });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
push(event);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Finalize only once BOTH the stdout stream is fully drained (readline 'close')
|
|
88
|
+
// AND the process has exited ('close', which fires after all stdio is flushed —
|
|
89
|
+
// unlike 'exit', which can fire while trailing stdout lines are still buffered).
|
|
90
|
+
let rlClosed = false;
|
|
91
|
+
let procClosed = false;
|
|
92
|
+
let exitCode = null;
|
|
93
|
+
let escalateTimer;
|
|
94
|
+
function clearEscalation() {
|
|
95
|
+
if (escalateTimer) {
|
|
96
|
+
clearTimeout(escalateTimer);
|
|
97
|
+
escalateTimer = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function maybeFinalize() {
|
|
101
|
+
if (!rlClosed || !procClosed)
|
|
102
|
+
return;
|
|
103
|
+
clearEscalation();
|
|
104
|
+
if (!sawTerminal) {
|
|
105
|
+
if (exitCode === 0) {
|
|
106
|
+
// Clean exit but the model never emitted a result event — synthesize a done.
|
|
107
|
+
push({ type: 'done' });
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const hint = modelRejectionHint('claude', stderrTail);
|
|
111
|
+
const text = hint ? `claude exited with code ${exitCode} (${hint})` : `claude exited with code ${exitCode}`;
|
|
112
|
+
push({ type: 'error', text });
|
|
113
|
+
}
|
|
114
|
+
sawTerminal = true;
|
|
115
|
+
}
|
|
116
|
+
finish();
|
|
117
|
+
}
|
|
118
|
+
rl.on('close', () => {
|
|
119
|
+
rlClosed = true;
|
|
120
|
+
maybeFinalize();
|
|
121
|
+
});
|
|
122
|
+
child.on('close', (code) => {
|
|
123
|
+
procClosed = true;
|
|
124
|
+
exitCode = code;
|
|
125
|
+
maybeFinalize();
|
|
126
|
+
});
|
|
127
|
+
// A failed spawn (missing binary, bad EMBERFLOW_*_BIN, permissions, ...) emits
|
|
128
|
+
// an 'error' event on the child instead of ever exiting/closing normally. Left
|
|
129
|
+
// unhandled, Node rethrows it and crashes the whole runner process — so we
|
|
130
|
+
// must handle it and finalize the stream ourselves. 'close' still fires after
|
|
131
|
+
// a spawn error (with code null), so guard against double-finalizing there.
|
|
132
|
+
let spawnFailed = false;
|
|
133
|
+
child.on('error', (err) => {
|
|
134
|
+
if (spawnFailed)
|
|
135
|
+
return;
|
|
136
|
+
spawnFailed = true;
|
|
137
|
+
clearEscalation();
|
|
138
|
+
push({ type: 'error', text: `failed to start claude: ${err.message}` });
|
|
139
|
+
sawTerminal = true;
|
|
140
|
+
rlClosed = true;
|
|
141
|
+
procClosed = true;
|
|
142
|
+
finish();
|
|
143
|
+
});
|
|
144
|
+
async function* generate() {
|
|
145
|
+
while (true) {
|
|
146
|
+
if (queue.length > 0) {
|
|
147
|
+
yield queue.shift();
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (done)
|
|
151
|
+
return;
|
|
152
|
+
await new Promise((resolve) => resolvers.push(resolve));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Kill the whole process group (negative pid) so claude's own subprocesses die
|
|
156
|
+
// too; fall back to a plain kill if the group signal fails (e.g. already gone
|
|
157
|
+
// or no group). Used by both the initial SIGTERM and the SIGKILL escalation.
|
|
158
|
+
function killGroup(signal) {
|
|
159
|
+
try {
|
|
160
|
+
if (child.pid != null) {
|
|
161
|
+
process.kill(-child.pid, signal);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// group kill failed — fall through to a direct kill
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
child.kill(signal);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// process already gone — nothing to do
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
events: { [Symbol.asyncIterator]: generate },
|
|
177
|
+
cancel() {
|
|
178
|
+
killGroup('SIGTERM');
|
|
179
|
+
// If the process (or a stubborn grandchild/MCP server) ignores SIGTERM,
|
|
180
|
+
// escalate to SIGKILL after a grace period so a hung agent can't
|
|
181
|
+
// permanently lock the run slot. Cleared once close/error finalizes.
|
|
182
|
+
clearEscalation();
|
|
183
|
+
escalateTimer = setTimeout(() => {
|
|
184
|
+
escalateTimer = undefined;
|
|
185
|
+
killGroup('SIGKILL');
|
|
186
|
+
}, opts.killGraceMs ?? 3000);
|
|
187
|
+
escalateTimer.unref?.();
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|