@xdelivered/emberflow 0.2.0 → 0.5.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/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +208 -16
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +28 -7
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +213 -15
- package/server/nodeRun.test.ts +189 -0
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +385 -314
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-BbzppDpt.js +65 -0
- package/studio-dist/assets/index-CLf6blcA.css +1 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +92 -9
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
package/dist/server/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, watch } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
4
|
import { dirname, join, resolve } from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -15,10 +16,12 @@ import { loadInfrastructure } from './infrastructure.js';
|
|
|
15
16
|
import { configPathFor, loadProjectConfig } from './projectConfig.js';
|
|
16
17
|
import { buildApiStore, buildRegistries, requireProjectWhenExplicit } from './projectMode.js';
|
|
17
18
|
import { nodesPayload } from './nodesPayload.js';
|
|
19
|
+
import { getSourceFile } from './sourceNav.js';
|
|
18
20
|
import { openBrowser } from './openBrowser.js';
|
|
19
21
|
import { AgentRunManager, AgentStartError } from './agents/runManager.js';
|
|
22
|
+
import { isGitRepo } from './agents/gitScope.js';
|
|
20
23
|
import { isMountablePath } from './pathGuard.js';
|
|
21
|
-
import {
|
|
24
|
+
import { detectAgentsCached } from './agents/detect.js';
|
|
22
25
|
import { ExpressAdapter } from './runtime/expressAdapter.js';
|
|
23
26
|
import { makeOperationHandler } from './httpOperations.js';
|
|
24
27
|
import { mockResponseFor } from './mockHandler.js';
|
|
@@ -27,6 +30,7 @@ import { isPathWithin } from './pathSafety.js';
|
|
|
27
30
|
import { attachCredential } from './authAttach.js';
|
|
28
31
|
import { redactSecrets } from './redact.js';
|
|
29
32
|
import { runScenarioSuiteFor, ScenarioTestUsageError } from './testRunner.js';
|
|
33
|
+
import { checkForUpdate, isLinkedInstall, ownVersion, runNpmInstall } from './updateCheck.js';
|
|
30
34
|
import { diagnoseOperation } from '../src/engine/diagnostics.js';
|
|
31
35
|
import { seedParamDefaults } from './normalizeFlow.js';
|
|
32
36
|
const PORT = Number(process.env.EMBERFLOW_RUNNER_PORT ?? 8092);
|
|
@@ -124,10 +128,32 @@ if (!project) {
|
|
|
124
128
|
apiStore.save(flow, flow.id.includes('/') ? flow.id : `default/${flow.id}`);
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
|
-
const agentRuns = new AgentRunManager(project ? project.root : projectDir, apiStore.dir, apiStore.pathOf.bind(apiStore), () => nodesPayload(validationRegistry).nodes.map(({ type, label, description }) => ({ type, label, description })), project?.language ?? 'typescript',
|
|
131
|
+
const agentRuns = new AgentRunManager(project ? project.root : projectDir, apiStore.dir, apiStore.pathOf.bind(apiStore), () => nodesPayload(validationRegistry, project ? project.root : process.cwd()).nodes.map(({ type, label, description }) => ({ type, label, description })), project?.language ?? 'typescript',
|
|
128
132
|
// Fresh per run: re-read emberflow/infrastructure.json so a scout that ran
|
|
129
133
|
// earlier this session primes later prompts. Malformed → null (agent guesses).
|
|
130
|
-
() => loadInfrastructure(project ? project.root : projectDir)
|
|
134
|
+
() => loadInfrastructure(project ? project.root : projectDir),
|
|
135
|
+
// Runner-verified setup snapshot for guided-setup prompts: the same facts
|
|
136
|
+
// /setup-status reports, computed fresh at run start, so the agent starts
|
|
137
|
+
// from KNOWN state instead of probing for it.
|
|
138
|
+
() => {
|
|
139
|
+
const root = project ? project.root : process.cwd();
|
|
140
|
+
let envs = environmentsFile;
|
|
141
|
+
try {
|
|
142
|
+
envs = loadEnvironments(root);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* keep last good */
|
|
146
|
+
}
|
|
147
|
+
const ops = apiStore.list();
|
|
148
|
+
return {
|
|
149
|
+
gitRepo: isGitRepo(root),
|
|
150
|
+
skillsInstalled: skillInstalled(root, 'claude') || skillInstalled(root, 'codex'),
|
|
151
|
+
environmentsConfigured: envs.configured,
|
|
152
|
+
infrastructurePresent: readInfrastructure() !== null,
|
|
153
|
+
opCount: ops.length,
|
|
154
|
+
onlyHello: ops.length === 1 && ops[0]?.id === 'default/hello',
|
|
155
|
+
};
|
|
156
|
+
});
|
|
131
157
|
// Hot-reload agent-authored nodes: watch the project's config and rebuild the
|
|
132
158
|
// node registries IN-PROCESS (no process restart), so registering a node in
|
|
133
159
|
// `registerNodes` takes effect live. Critically, this survives an in-flight
|
|
@@ -187,7 +213,27 @@ api.post('/serving', (req, res) => {
|
|
|
187
213
|
res.status(204).end();
|
|
188
214
|
});
|
|
189
215
|
api.get('/nodes', (_req, res) => {
|
|
190
|
-
res.json(nodesPayload(validationRegistry));
|
|
216
|
+
res.json(nodesPayload(validationRegistry, project ? project.root : process.cwd()));
|
|
217
|
+
});
|
|
218
|
+
// Whole-file source view + symbol table for the studio's source navigator.
|
|
219
|
+
// Works in project mode AND no-project mode (root = cwd) — path guards in
|
|
220
|
+
// sourceNav (isPathWithin, node_modules, secret basenames) bound what is
|
|
221
|
+
// servable either way. 400s stay generic: the requested path is never echoed.
|
|
222
|
+
api.get('/source-file', (req, res) => {
|
|
223
|
+
void (async () => {
|
|
224
|
+
const path = req.query.path;
|
|
225
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
226
|
+
res.status(400).json({ error: 'Query param path is required' });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const root = project ? project.root : process.cwd();
|
|
230
|
+
const result = await getSourceFile(root, path);
|
|
231
|
+
if (!result.ok) {
|
|
232
|
+
res.status(result.status).json({ error: result.error });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
res.json(result.payload);
|
|
236
|
+
})();
|
|
191
237
|
});
|
|
192
238
|
// Validate a flow against the runner's LIVE registry (built-ins + project
|
|
193
239
|
// nodes) so the CLI `validate` command doesn't false-reject ops that use
|
|
@@ -660,6 +706,8 @@ api.post('/runs', async (req, res) => {
|
|
|
660
706
|
input: effectiveInput,
|
|
661
707
|
mockRun,
|
|
662
708
|
mocks: effectiveMocks,
|
|
709
|
+
// Step-mode runs get drill-in subflow stepping (see RunRegistry.step).
|
|
710
|
+
stepped: mode === 'step',
|
|
663
711
|
}));
|
|
664
712
|
}
|
|
665
713
|
catch (err) {
|
|
@@ -673,22 +721,98 @@ api.post('/runs', async (req, res) => {
|
|
|
673
721
|
}
|
|
674
722
|
res.status(201).json({ runId });
|
|
675
723
|
});
|
|
724
|
+
// Run a single node in isolation (studio's "Run node" affordance). Executes the
|
|
725
|
+
// node's implementation in-process against the resolved environment's
|
|
726
|
+
// secrets/vars, honouring safe mode via resolveRunSafety, and redacts the
|
|
727
|
+
// output through the same choke point runs use. Unknown type → 404. The studio
|
|
728
|
+
// bundles no implementations, so this is the ONLY path to a lone node run.
|
|
729
|
+
api.post('/node-run', async (req, res) => {
|
|
730
|
+
const { type, input, config, environment, safeMode, confirm } = req.body ?? {};
|
|
731
|
+
if (typeof type !== 'string' || type.length === 0) {
|
|
732
|
+
res.status(400).json({ error: 'Body must include a node "type"' });
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (input !== undefined && (typeof input !== 'object' || input === null || Array.isArray(input))) {
|
|
736
|
+
res.status(400).json({ error: 'input must be a plain object' });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (config !== undefined && (typeof config !== 'object' || config === null || Array.isArray(config))) {
|
|
740
|
+
res.status(400).json({ error: 'config must be a plain object' });
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (environment !== undefined && typeof environment !== 'string') {
|
|
744
|
+
res.status(400).json({ error: 'environment must be a string' });
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (safeMode !== undefined && typeof safeMode !== 'boolean') {
|
|
748
|
+
res.status(400).json({ error: 'safeMode must be a boolean' });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (!runs.executionRegistry.has(type)) {
|
|
752
|
+
res.status(404).json({ error: `Unknown node type: ${type}` });
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const environmentName = environment ?? environmentsFile.defaultEnvironment;
|
|
756
|
+
const environmentDef = environmentsFile.environments[environmentName];
|
|
757
|
+
if (!environmentDef) {
|
|
758
|
+
res.status(400).json({ error: `Unknown environment: '${environmentName}'` });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const safety = resolveRunSafety(environmentName, environmentDef, { safeMode, confirm });
|
|
762
|
+
if (!safety.ok) {
|
|
763
|
+
res.status(400).json({ error: safety.error });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const { implementation } = runs.executionRegistry.get(type);
|
|
767
|
+
const runId = `node-run-${randomUUID().slice(0, 8)}`;
|
|
768
|
+
const logs = [];
|
|
769
|
+
const log = (level, message) => {
|
|
770
|
+
logs.push({ timestamp: new Date().toISOString(), level, runId, message });
|
|
771
|
+
};
|
|
772
|
+
const nodeInput = (input ?? {});
|
|
773
|
+
let result;
|
|
774
|
+
try {
|
|
775
|
+
const output = await implementation({
|
|
776
|
+
input: nodeInput,
|
|
777
|
+
config: (config ?? {}),
|
|
778
|
+
secrets: environmentDef.secrets,
|
|
779
|
+
vars: environmentDef.vars,
|
|
780
|
+
environment: environmentName,
|
|
781
|
+
safeMode: safety.safeMode,
|
|
782
|
+
runInput: nodeInput,
|
|
783
|
+
log,
|
|
784
|
+
// A node run in isolation has no flow context to resolve child workflows.
|
|
785
|
+
runSubflow: async () => ({
|
|
786
|
+
status: 'failed',
|
|
787
|
+
error: 'Subflow nodes cannot be run in isolation — run the whole operation instead',
|
|
788
|
+
}),
|
|
789
|
+
});
|
|
790
|
+
result = { output, logs };
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
result = { error: err instanceof Error ? err.message : String(err), logs };
|
|
794
|
+
}
|
|
795
|
+
// Redact secret values from the output AND captured logs before responding.
|
|
796
|
+
res.json(redactSecrets(result, environmentDef.secrets));
|
|
797
|
+
});
|
|
676
798
|
api.post('/runs/:id/step', async (req, res) => {
|
|
677
|
-
|
|
678
|
-
|
|
799
|
+
// Drill-aware composite step: stepping INTO a Subflow reports
|
|
800
|
+
// `entered: { workflowId, nodeId }`, completing the child reports
|
|
801
|
+
// `exited: true` (see RunRegistry.step). Plain steps report just `done`.
|
|
802
|
+
const result = await runs.step(req.params.id);
|
|
803
|
+
if (!result) {
|
|
679
804
|
res.status(404).json({ error: `Unknown run: ${req.params.id}` });
|
|
680
805
|
return;
|
|
681
806
|
}
|
|
682
|
-
|
|
683
|
-
res.json({ done: !more });
|
|
807
|
+
res.json(result);
|
|
684
808
|
});
|
|
685
809
|
api.post('/runs/:id/cancel', (req, res) => {
|
|
686
|
-
|
|
687
|
-
|
|
810
|
+
// Drill-aware: a stepped run drilled into subflows also cancels every
|
|
811
|
+
// stacked child and unblocks the parent executions (see RunRegistry.cancel).
|
|
812
|
+
if (!runs.cancel(req.params.id)) {
|
|
688
813
|
res.status(404).json({ error: `Unknown run: ${req.params.id}` });
|
|
689
814
|
return;
|
|
690
815
|
}
|
|
691
|
-
handle.cancel();
|
|
692
816
|
res.status(204).end();
|
|
693
817
|
});
|
|
694
818
|
api.get('/runs/:id/events', (req, res) => {
|
|
@@ -709,7 +833,7 @@ api.get('/runs/:id/events', (req, res) => {
|
|
|
709
833
|
unsubscribe = runs.subscribe(runId, listener);
|
|
710
834
|
});
|
|
711
835
|
api.get('/agent/available', (_req, res) => {
|
|
712
|
-
res.json({ agents:
|
|
836
|
+
res.json({ agents: detectAgentsCached() });
|
|
713
837
|
});
|
|
714
838
|
// First-run onboarding aggregate for the Welcome checklist (studio's
|
|
715
839
|
// WelcomeDialog). Recomputed per request (cheap; mirrors /environments'
|
|
@@ -732,7 +856,10 @@ api.get('/setup-status', (_req, res) => {
|
|
|
732
856
|
// A pristine project ships exactly the single `default/hello` example op.
|
|
733
857
|
const onlyHello = ops.length === 1 && ops[0]?.id === 'default/hello';
|
|
734
858
|
res.json({
|
|
735
|
-
agents:
|
|
859
|
+
agents: detectAgentsCached(),
|
|
860
|
+
// Agent features snapshot changes with git (server/agents/runManager.ts);
|
|
861
|
+
// the checklist gates them on this. Same notion gitScope's isGitRepo uses.
|
|
862
|
+
git: { repo: isGitRepo(root) },
|
|
736
863
|
environments: {
|
|
737
864
|
configured: envs.configured,
|
|
738
865
|
// The synthesized "local" fallback is "no environments yet" — report 0.
|
|
@@ -758,6 +885,52 @@ api.get('/setup-status', (_req, res) => {
|
|
|
758
885
|
function skillInstalled(root, harness) {
|
|
759
886
|
return [root, homedir()].some((base) => existsSync(join(base, `.${harness}`, 'skills', 'emberflow-basics', 'SKILL.md')));
|
|
760
887
|
}
|
|
888
|
+
// Package update notifier: the studio asks once per page load; checkForUpdate
|
|
889
|
+
// caches registry hits for an hour and fails silent — an unreachable registry
|
|
890
|
+
// (or a pre-publish package) just reports "no update".
|
|
891
|
+
api.get('/update-status', async (_req, res) => {
|
|
892
|
+
const current = ownVersion() ?? '0.0.0';
|
|
893
|
+
const check = await checkForUpdate(current);
|
|
894
|
+
if (!check) {
|
|
895
|
+
res.json({ current, updateAvailable: false });
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
res.json(check);
|
|
899
|
+
});
|
|
900
|
+
// One-click updater: npm-installs the latest package into the consumer
|
|
901
|
+
// project. Single-flight — a second click while npm runs gets a 409, not a
|
|
902
|
+
// second npm process fighting over node_modules.
|
|
903
|
+
let updateInFlight = false;
|
|
904
|
+
api.post('/update', async (_req, res) => {
|
|
905
|
+
if (!project) {
|
|
906
|
+
res.status(400).json({ ok: false, error: 'not a consumer project — no project is loaded to update' });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (isLinkedInstall(project.root)) {
|
|
910
|
+
res.status(400).json({
|
|
911
|
+
ok: false,
|
|
912
|
+
error: 'node_modules/@xdelivered/emberflow is a symlink (file:/npm-link dev setup) — refusing to npm install over it',
|
|
913
|
+
});
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (updateInFlight) {
|
|
917
|
+
res.status(409).json({ ok: false, error: 'update already running' });
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
updateInFlight = true;
|
|
921
|
+
try {
|
|
922
|
+
const result = await runNpmInstall(project.root);
|
|
923
|
+
if (result.ok) {
|
|
924
|
+
res.json({ ok: true, restartRequired: true });
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
res.status(500).json({ ok: false, error: result.error });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
updateInFlight = false;
|
|
932
|
+
}
|
|
933
|
+
});
|
|
761
934
|
/** The /setup-status `infrastructure` field: presence + a shallow summary. */
|
|
762
935
|
function infraStatus() {
|
|
763
936
|
const manifest = readInfrastructure();
|
|
@@ -786,10 +959,11 @@ api.post('/agent', (req, res) => {
|
|
|
786
959
|
intent.action !== 'setup-auth' &&
|
|
787
960
|
intent.action !== 'setup-environments' &&
|
|
788
961
|
intent.action !== 'scout-infrastructure' &&
|
|
962
|
+
intent.action !== 'guided-setup' &&
|
|
789
963
|
intent.action !== 'cover-operation' &&
|
|
790
964
|
intent.action !== 'ask') {
|
|
791
965
|
res.status(400).json({
|
|
792
|
-
error: `Unsupported intent.action: ${intent.action}. Must be one of new-scenario, edit-node, edit-flow, new-operation, setup-auth, setup-environments, scout-infrastructure, cover-operation, ask.`,
|
|
966
|
+
error: `Unsupported intent.action: ${intent.action}. Must be one of new-scenario, edit-node, edit-flow, new-operation, setup-auth, setup-environments, scout-infrastructure, guided-setup, cover-operation, ask.`,
|
|
793
967
|
});
|
|
794
968
|
return;
|
|
795
969
|
}
|
|
@@ -805,7 +979,9 @@ api.post('/agent', (req, res) => {
|
|
|
805
979
|
return;
|
|
806
980
|
}
|
|
807
981
|
}
|
|
808
|
-
else if (intent.action === 'setup-environments' ||
|
|
982
|
+
else if (intent.action === 'setup-environments' ||
|
|
983
|
+
intent.action === 'scout-infrastructure' ||
|
|
984
|
+
intent.action === 'guided-setup') {
|
|
809
985
|
// instruction alone is required, already validated above; no flowId/environment needed.
|
|
810
986
|
}
|
|
811
987
|
else if (intent.action === 'ask') {
|
|
@@ -899,7 +1075,7 @@ const operationRunEnv = {
|
|
|
899
1075
|
// operation whose http.path collides with one of these would silently never
|
|
900
1076
|
// be reached (the internal route, registered first, always wins) — guarded
|
|
901
1077
|
// against below rather than left as a silent trap.
|
|
902
|
-
const RESERVED_ROOT_PATHS = ['/healthz', '/nodes', '/samples', '/environments', '/workflows', '/runs', '/agent', '/serving'];
|
|
1078
|
+
const RESERVED_ROOT_PATHS = ['/healthz', '/nodes', '/samples', '/environments', '/workflows', '/runs', '/node-run', '/agent', '/serving'];
|
|
903
1079
|
function collidesWithReservedPath(path) {
|
|
904
1080
|
return RESERVED_ROOT_PATHS.some((reserved) => path === reserved || path.startsWith(`${reserved}/`));
|
|
905
1081
|
}
|
|
@@ -1007,6 +1183,22 @@ if (serveStudio) {
|
|
|
1007
1183
|
console.warn(`[runner] EMBERFLOW_SERVE_STUDIO=1 but no studio-dist found near ${here} — run \`npm run build:studio\``);
|
|
1008
1184
|
}
|
|
1009
1185
|
}
|
|
1186
|
+
// Agent CLIs and node executions must not outlive the runner: agents are
|
|
1187
|
+
// spawned detached (their own process group), so without this a killed runner
|
|
1188
|
+
// leaves an orphaned agent editing the project — and possibly writing files
|
|
1189
|
+
// long after the studio is gone (observed in the wild). Cancel everything,
|
|
1190
|
+
// then exit.
|
|
1191
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
1192
|
+
process.on(signal, () => {
|
|
1193
|
+
try {
|
|
1194
|
+
agentRuns.shutdown();
|
|
1195
|
+
runs.shutdown();
|
|
1196
|
+
}
|
|
1197
|
+
finally {
|
|
1198
|
+
process.exit(0);
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1010
1202
|
app.listen(PORT, HOST, () => {
|
|
1011
1203
|
console.log(`[emberflow-runner] listening on http://${HOST}:${PORT} — ${runs.nodeCount} nodes registered`);
|
|
1012
1204
|
if (serveStudio) {
|
|
@@ -5,10 +5,23 @@ import type { NodeDefinition, NodeRegistry } from '../src/engine/index.js';
|
|
|
5
5
|
* implementation's toString() so the Inspector can show a node's code without
|
|
6
6
|
* the browser bundling the implementation — the key to consumer nodes
|
|
7
7
|
* appearing in the palette without a per-project browser build.
|
|
8
|
+
*
|
|
9
|
+
* `sourceRef` (repo-relative file + line of the register() call) is present
|
|
10
|
+
* only when registration provenance was captured AND the file lives inside
|
|
11
|
+
* the project root — the studio uses it to open the real source via
|
|
12
|
+
* GET /source-file. A captured ref OUTSIDE the root (Emberflow's own
|
|
13
|
+
* built-ins registered from the package) is flagged `builtin: true` instead:
|
|
14
|
+
* those files are never served (node_modules exposure rule), so the
|
|
15
|
+
* Inspector keeps the toString() view for them.
|
|
8
16
|
*/
|
|
9
17
|
export interface NodeMetaPayload {
|
|
10
18
|
nodes: Array<NodeDefinition & {
|
|
11
19
|
source?: string;
|
|
20
|
+
sourceRef?: {
|
|
21
|
+
file: string;
|
|
22
|
+
line?: number;
|
|
23
|
+
};
|
|
24
|
+
builtin?: boolean;
|
|
12
25
|
}>;
|
|
13
26
|
}
|
|
14
|
-
export declare function nodesPayload(registry: NodeRegistry): NodeMetaPayload;
|
|
27
|
+
export declare function nodesPayload(registry: NodeRegistry, projectRoot?: string): NodeMetaPayload;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
export function nodesPayload(registry, projectRoot) {
|
|
3
|
+
const root = resolve(projectRoot ?? process.cwd());
|
|
2
4
|
return {
|
|
3
5
|
nodes: registry.list().map((definition) => {
|
|
4
6
|
let source;
|
|
@@ -8,7 +10,18 @@ export function nodesPayload(registry) {
|
|
|
8
10
|
catch {
|
|
9
11
|
source = undefined;
|
|
10
12
|
}
|
|
11
|
-
|
|
13
|
+
const ref = registry.getSourceRef(definition.type);
|
|
14
|
+
if (!ref)
|
|
15
|
+
return { ...definition, source };
|
|
16
|
+
const rel = relative(root, resolve(root, ref.file));
|
|
17
|
+
const inside = rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
18
|
+
if (!inside)
|
|
19
|
+
return { ...definition, source, builtin: true };
|
|
20
|
+
return {
|
|
21
|
+
...definition,
|
|
22
|
+
source,
|
|
23
|
+
sourceRef: ref.line !== undefined ? { file: rel, line: ref.line } : { file: rel },
|
|
24
|
+
};
|
|
12
25
|
}),
|
|
13
26
|
};
|
|
14
27
|
}
|
|
@@ -34,8 +34,11 @@ export function buildApiStore(project) {
|
|
|
34
34
|
return new ApiStore(apisDir);
|
|
35
35
|
}
|
|
36
36
|
export function buildRegistries(project) {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Server-side registries capture registration provenance (file:line of each
|
|
38
|
+
// register() call) so the studio can navigate to a node's real source.
|
|
39
|
+
// Browser registries never enable this — see createDefaultRegistry.
|
|
40
|
+
const validation = createDefaultRegistry(undefined, { captureSourceRefs: true });
|
|
41
|
+
const execution = createDefaultRegistry(undefined, { captureSourceRefs: true });
|
|
39
42
|
if (project?.registerNodes) {
|
|
40
43
|
project.registerNodes(validation);
|
|
41
44
|
project.registerNodes(execution);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { LogLine, NodeExecutionSample, NodeRunState, WorkflowDefinition, WorkflowRun } from '../src/engine/index.js';
|
|
2
2
|
import { InMemoryTraceSink, type FlowRun, type NodeRegistry } from '../src/engine/index.js';
|
|
3
3
|
/** SSE-shaped events, mirroring ExecutorEvents 1:1. */
|
|
4
|
-
export type RunEvent =
|
|
4
|
+
export type RunEvent =
|
|
5
|
+
/** `workflowId` is the flow the node belongs to: the root flow for the
|
|
6
|
+
* run's own nodes, the CHILD flow's id for a stepped subflow's nodes. */
|
|
7
|
+
{
|
|
5
8
|
type: 'nodeState';
|
|
9
|
+
workflowId: string;
|
|
6
10
|
nodeId: string;
|
|
7
11
|
state: NodeRunState;
|
|
8
12
|
} | {
|
|
@@ -18,6 +22,17 @@ export type RunEvent = {
|
|
|
18
22
|
};
|
|
19
23
|
};
|
|
20
24
|
type Listener = (event: RunEvent) => void;
|
|
25
|
+
/** Result of one composite step on a stepped run (see RunRegistry.step). */
|
|
26
|
+
export interface StepResult {
|
|
27
|
+
done: boolean;
|
|
28
|
+
/** Set when this step drove execution INTO a subflow child. */
|
|
29
|
+
entered?: {
|
|
30
|
+
workflowId: string;
|
|
31
|
+
nodeId: string;
|
|
32
|
+
};
|
|
33
|
+
/** Set when this step completed the deepest child and popped back out. */
|
|
34
|
+
exited?: true;
|
|
35
|
+
}
|
|
21
36
|
/** Manages live runs: buffers engine events per run and fans them out to SSE subscribers. */
|
|
22
37
|
export declare class RunRegistry {
|
|
23
38
|
private readonly loadFlow?;
|
|
@@ -62,10 +77,46 @@ export declare class RunRegistry {
|
|
|
62
77
|
* error-handler run — stamped onto this run's `finished` event as
|
|
63
78
|
* `errorHandler: { firedBy }`. Only meaningful alongside isErrorHandler. */
|
|
64
79
|
errorHandlerFiredBy?: string;
|
|
80
|
+
/** When true (a step-mode run), Subflow children become step-drivable:
|
|
81
|
+
* entering one pauses inside it instead of running it to completion in
|
|
82
|
+
* a single opaque step. Drive the run via `step(runId)`. */
|
|
83
|
+
stepped?: boolean;
|
|
65
84
|
}): {
|
|
66
85
|
runId: string;
|
|
67
86
|
handle: FlowRun;
|
|
68
87
|
};
|
|
88
|
+
/**
|
|
89
|
+
* One composite step of a run, drill-aware. For stepped runs a Subflow node
|
|
90
|
+
* is no longer one opaque step: entering it pauses inside the child
|
|
91
|
+
* (`entered`), subsequent steps drive the child's nodes, and completing the
|
|
92
|
+
* child pops back out (`exited`). Non-stepped runs (no drill state) fall
|
|
93
|
+
* back to a plain executor step. Unknown run → undefined.
|
|
94
|
+
*/
|
|
95
|
+
step(runId: string): Promise<StepResult | undefined>;
|
|
96
|
+
/**
|
|
97
|
+
* One composite step, drill-aware. Structured as a loop over levels: each
|
|
98
|
+
* pass drives the current deepest level (a pending stashed step, or a fresh
|
|
99
|
+
* executor step) while racing "a new child started". Popping a finished
|
|
100
|
+
* child continues the loop on its parent's stashed step WITH the race still
|
|
101
|
+
* armed — the parent's still-pending Subflow node execution may call
|
|
102
|
+
* runSubflow again (retry, or a custom node making sequential calls), and
|
|
103
|
+
* that re-entry must surface as `entered`, not deadlock the fold.
|
|
104
|
+
*/
|
|
105
|
+
private stepLevels;
|
|
106
|
+
/**
|
|
107
|
+
* Cancel a run, drill-aware. For a stepped run drilled into subflows this
|
|
108
|
+
* cancels every stacked child deepest-first and rejects the promise each
|
|
109
|
+
* parent's Subflow node execution is blocked on — those pending executions
|
|
110
|
+
* then unwind in the background exactly like an in-flight node after a
|
|
111
|
+
* plain root cancel today (recorded, but the run is already finished).
|
|
112
|
+
* step() after cancel reports { done: true } without executing anything.
|
|
113
|
+
* Returns false for an unknown run.
|
|
114
|
+
*/
|
|
115
|
+
/** Cancels every live (non-finished) run — the server's shutdown path, so
|
|
116
|
+
* in-flight executions (and their drilled subflow children) don't keep
|
|
117
|
+
* running headless after the process is asked to die. */
|
|
118
|
+
shutdown(): void;
|
|
119
|
+
cancel(runId: string): boolean;
|
|
69
120
|
/**
|
|
70
121
|
* Best-effort: fires `errorOperation` (resolved via the same `loadFlow`
|
|
71
122
|
* callback Subflow nodes use) as a new run when `failedRun` finished with
|