@synergenius/flow-weaver 0.6.0 → 0.8.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/dist/annotation-generator.js +11 -9
- package/dist/api/generate-in-place.js +28 -20
- package/dist/api/patterns.js +12 -15
- package/dist/ast/types.d.ts +2 -0
- package/dist/built-in-nodes/invoke-workflow.js +3 -3
- package/dist/built-in-nodes/mock-types.d.ts +20 -0
- package/dist/built-in-nodes/mock-types.js +30 -0
- package/dist/built-in-nodes/wait-for-agent.js +5 -4
- package/dist/built-in-nodes/wait-for-event.js +3 -3
- package/dist/chevrotain-parser/node-parser.d.ts +4 -0
- package/dist/chevrotain-parser/node-parser.js +24 -1
- package/dist/chevrotain-parser/tokens.d.ts +1 -0
- package/dist/chevrotain-parser/tokens.js +5 -0
- package/dist/cli/commands/compile.js +6 -0
- package/dist/cli/commands/init.js +2 -0
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +38 -0
- package/dist/cli/commands/validate.js +12 -0
- package/dist/cli/flow-weaver.mjs +208 -122
- package/dist/cli/templates/workflows/aggregator.js +3 -6
- package/dist/cli/templates/workflows/ai-agent-durable.js +4 -8
- package/dist/cli/templates/workflows/ai-agent.js +3 -6
- package/dist/cli/templates/workflows/ai-chat.js +3 -6
- package/dist/cli/templates/workflows/ai-pipeline-durable.js +4 -8
- package/dist/cli/templates/workflows/ai-rag.js +2 -4
- package/dist/cli/templates/workflows/ai-react.js +3 -6
- package/dist/cli/templates/workflows/conditional.js +3 -6
- package/dist/cli/templates/workflows/error-handler.js +2 -4
- package/dist/cli/templates/workflows/foreach.js +3 -6
- package/dist/cli/templates/workflows/sequential.js +7 -8
- package/dist/cli/templates/workflows/webhook.js +3 -6
- package/dist/doc-metadata/extractors/annotations.js +9 -6
- package/dist/editor-completions/jsDocAnnotations.js +5 -3
- package/dist/jsdoc-parser.d.ts +2 -0
- package/dist/jsdoc-parser.js +6 -1
- package/dist/mcp/tools-editor.js +20 -1
- package/dist/mcp/workflow-executor.js +7 -2
- package/dist/parser.js +1 -0
- package/dist/validator.js +19 -0
- package/docs/reference/concepts.md +52 -11
- package/docs/reference/debugging.md +33 -0
- package/docs/reference/iterative-development.md +2 -3
- package/docs/reference/jsdoc-grammar.md +8 -3
- package/docs/reference/patterns.md +2 -4
- package/docs/reference/tutorial.md +8 -14
- package/package.json +1 -1
|
@@ -40,9 +40,11 @@ export class AnnotationGenerator {
|
|
|
40
40
|
}
|
|
41
41
|
// Generate JSDoc comment block (only when no functionText)
|
|
42
42
|
lines.push("/**");
|
|
43
|
-
// Add description if present
|
|
43
|
+
// Add description if present (handle multi-line descriptions)
|
|
44
44
|
if (includeComments && nodeType.description) {
|
|
45
|
-
|
|
45
|
+
for (const descLine of nodeType.description.split('\n')) {
|
|
46
|
+
lines.push(` * ${descLine}`);
|
|
47
|
+
}
|
|
46
48
|
lines.push(` *`);
|
|
47
49
|
}
|
|
48
50
|
// @flowWeaver nodeType marker
|
|
@@ -279,12 +281,7 @@ export class AnnotationGenerator {
|
|
|
279
281
|
if (workflow.ui?.startNode?.x !== undefined && workflow.ui?.startNode?.y !== undefined) {
|
|
280
282
|
lines.push(` * @position Start ${Math.round(workflow.ui.startNode.x)} ${Math.round(workflow.ui.startNode.y)}`);
|
|
281
283
|
}
|
|
282
|
-
//
|
|
283
|
-
workflow.instances.forEach((instance) => {
|
|
284
|
-
if (instance.config?.x !== undefined && instance.config?.y !== undefined) {
|
|
285
|
-
lines.push(` * @position ${instance.id} ${Math.round(instance.config.x)} ${Math.round(instance.config.y)}`);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
284
|
+
// Instance positions are now emitted as [position: x y] on @node lines
|
|
288
285
|
// Add Exit node position if present
|
|
289
286
|
if (workflow.ui?.exitNode?.x !== undefined && workflow.ui?.exitNode?.y !== undefined) {
|
|
290
287
|
lines.push(` * @position Exit ${Math.round(workflow.ui.exitNode.x)} ${Math.round(workflow.ui.exitNode.y)}`);
|
|
@@ -610,7 +607,12 @@ export function generateNodeInstanceTag(instance) {
|
|
|
610
607
|
if (instance.config?.width !== undefined && instance.config?.height !== undefined) {
|
|
611
608
|
sizeAttr = ` [size: ${Math.round(instance.config.width)} ${Math.round(instance.config.height)}]`;
|
|
612
609
|
}
|
|
613
|
-
|
|
610
|
+
// Generate [position: x y] attribute if present
|
|
611
|
+
let positionAttr = '';
|
|
612
|
+
if (instance.config?.x !== undefined && instance.config?.y !== undefined) {
|
|
613
|
+
positionAttr = ` [position: ${Math.round(instance.config.x)} ${Math.round(instance.config.y)}]`;
|
|
614
|
+
}
|
|
615
|
+
return ` * @node ${instance.id} ${instance.nodeType}${parent}${labelAttr}${portOrderAttr}${portLabelAttr}${exprAttr}${pullExecutionAttr}${minimizedAttr}${colorAttr}${iconAttr}${tagsAttr}${sizeAttr}${positionAttr}`;
|
|
614
616
|
}
|
|
615
617
|
export const annotationGenerator = new AnnotationGenerator();
|
|
616
618
|
//# sourceMappingURL=annotation-generator.js.map
|
|
@@ -408,6 +408,9 @@ function replaceWorkflowFunctionBody(source, functionName, newBody) {
|
|
|
408
408
|
}
|
|
409
409
|
// Find the closing brace
|
|
410
410
|
const closeBraceIdx = functionNode.body.end - 1;
|
|
411
|
+
if (closeBraceIdx <= openBraceIdx) {
|
|
412
|
+
return { code: source, changed: false };
|
|
413
|
+
}
|
|
411
414
|
const before = source.slice(0, openBraceIdx + 1);
|
|
412
415
|
const after = source.slice(closeBraceIdx);
|
|
413
416
|
const newBodyWithMarkers = [
|
|
@@ -1116,17 +1119,36 @@ function generateWorkflowJSDoc(ast, options = {}) {
|
|
|
1116
1119
|
}
|
|
1117
1120
|
lines.push(` * @fwImport ${npmType.name} ${actualFunctionName} from "${npmType.importSource}"`);
|
|
1118
1121
|
}
|
|
1119
|
-
//
|
|
1122
|
+
// Auto-position: compute default positions for nodes without explicit positions.
|
|
1123
|
+
// Must happen before instance tags are generated so [position:] can be emitted.
|
|
1124
|
+
const autoPositions = computeAutoPositions(ast);
|
|
1125
|
+
// Add node instances — skip synthetic MAP_ITERATOR instances, strip parent from macro children.
|
|
1126
|
+
// Merge auto-computed positions into instance config (without mutating the AST).
|
|
1120
1127
|
for (const instance of ast.instances) {
|
|
1121
1128
|
if (macroInstanceIds.has(instance.id))
|
|
1122
1129
|
continue;
|
|
1123
|
-
if
|
|
1130
|
+
// Merge auto-position into config if not already set
|
|
1131
|
+
let inst = instance;
|
|
1132
|
+
if (inst.config?.x === undefined || inst.config?.y === undefined) {
|
|
1133
|
+
const autoPos = autoPositions.get(inst.id);
|
|
1134
|
+
if (autoPos) {
|
|
1135
|
+
inst = {
|
|
1136
|
+
...inst,
|
|
1137
|
+
config: {
|
|
1138
|
+
...inst.config,
|
|
1139
|
+
x: inst.config?.x ?? autoPos.x,
|
|
1140
|
+
y: inst.config?.y ?? autoPos.y,
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (macroChildIds.has(inst.id) && inst.parent) {
|
|
1124
1146
|
// Write child @node without parent scope — @map handles it
|
|
1125
|
-
const stripped = { ...
|
|
1147
|
+
const stripped = { ...inst, parent: undefined };
|
|
1126
1148
|
lines.push(generateNodeInstanceTag(stripped));
|
|
1127
1149
|
}
|
|
1128
1150
|
else {
|
|
1129
|
-
lines.push(generateNodeInstanceTag(
|
|
1151
|
+
lines.push(generateNodeInstanceTag(inst));
|
|
1130
1152
|
}
|
|
1131
1153
|
}
|
|
1132
1154
|
// Filter stale macros (e.g. paths whose connections were deleted)
|
|
@@ -1165,27 +1187,13 @@ function generateWorkflowJSDoc(ast, options = {}) {
|
|
|
1165
1187
|
}
|
|
1166
1188
|
}
|
|
1167
1189
|
}
|
|
1168
|
-
//
|
|
1169
|
-
// Uses a left-to-right layout with topological ordering when connections are available.
|
|
1170
|
-
const autoPositions = computeAutoPositions(ast);
|
|
1171
|
-
// Add positions - Start node
|
|
1190
|
+
// Add positions - Start node (virtual, standalone @position)
|
|
1172
1191
|
const startX = ast.ui?.startNode?.x ?? autoPositions.get('Start')?.x;
|
|
1173
1192
|
const startY = ast.ui?.startNode?.y ?? autoPositions.get('Start')?.y;
|
|
1174
1193
|
if (startX !== undefined && startY !== undefined) {
|
|
1175
1194
|
lines.push(` * @position Start ${Math.round(startX)} ${Math.round(startY)}`);
|
|
1176
1195
|
}
|
|
1177
|
-
// Add positions -
|
|
1178
|
-
for (const instance of ast.instances) {
|
|
1179
|
-
const explicitX = instance.config?.x;
|
|
1180
|
-
const explicitY = instance.config?.y;
|
|
1181
|
-
const autoPos = autoPositions.get(instance.id);
|
|
1182
|
-
const x = explicitX ?? autoPos?.x;
|
|
1183
|
-
const y = explicitY ?? autoPos?.y;
|
|
1184
|
-
if (x !== undefined && y !== undefined) {
|
|
1185
|
-
lines.push(` * @position ${instance.id} ${Math.round(x)} ${Math.round(y)}`);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
// Add positions - Exit node
|
|
1196
|
+
// Add positions - Exit node (virtual, standalone @position)
|
|
1189
1197
|
const exitX = ast.ui?.exitNode?.x ?? autoPositions.get('Exit')?.x;
|
|
1190
1198
|
const exitY = ast.ui?.exitNode?.y ?? autoPositions.get('Exit')?.y;
|
|
1191
1199
|
if (exitX !== undefined && exitY !== undefined) {
|
package/dist/api/patterns.js
CHANGED
|
@@ -48,8 +48,13 @@ export function applyPattern(options) {
|
|
|
48
48
|
conflicts.push(nodeType.name);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
// ── Build @node declarations
|
|
52
|
-
const nodeDeclarations = pattern.instances.map((inst) =>
|
|
51
|
+
// ── Build @node declarations (with inline [position:] when present) ──
|
|
52
|
+
const nodeDeclarations = pattern.instances.map((inst) => {
|
|
53
|
+
const posAttr = inst.config?.x !== undefined && inst.config?.y !== undefined
|
|
54
|
+
? ` [position: ${inst.config.x} ${inst.config.y}]`
|
|
55
|
+
: '';
|
|
56
|
+
return ` * @node ${nodePrefix}${inst.id} ${inst.nodeType}${posAttr}`;
|
|
57
|
+
});
|
|
53
58
|
// ── Build @connect declarations + wiring instructions ───────────────
|
|
54
59
|
const connectDeclarations = [];
|
|
55
60
|
const wiringInstructions = [];
|
|
@@ -85,10 +90,6 @@ export function applyPattern(options) {
|
|
|
85
90
|
connectDeclarations.push(` * @connect ${fromNode}.${conn.from.port} -> ${toNode}.${conn.to.port}`);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
|
-
// ── Build @position declarations ────────────────────────────────────
|
|
89
|
-
const positionDeclarations = pattern.instances
|
|
90
|
-
.filter((inst) => inst.config?.x !== undefined && inst.config?.y !== undefined)
|
|
91
|
-
.map((inst) => ` * @position ${nodePrefix}${inst.id} ${inst.config.x} ${inst.config.y}`);
|
|
92
93
|
// ── Generate node type functions (only non-conflicting) ─────────────
|
|
93
94
|
const nodeTypesAdded = [];
|
|
94
95
|
const nodeTypeFunctions = [];
|
|
@@ -103,7 +104,6 @@ export function applyPattern(options) {
|
|
|
103
104
|
`// --- Pattern: ${pattern.name} ${prefix ? `(prefix: ${prefix})` : ''} ---`,
|
|
104
105
|
...nodeDeclarations,
|
|
105
106
|
...connectDeclarations,
|
|
106
|
-
...positionDeclarations,
|
|
107
107
|
];
|
|
108
108
|
// ── Insert into target content ──────────────────────────────────────
|
|
109
109
|
const workflowMatch = targetContent.match(/\/\*\*[\s\S]*?@flowWeaver\s+workflow[\s\S]*?\*\//);
|
|
@@ -250,9 +250,12 @@ export function extractPattern(options) {
|
|
|
250
250
|
lines.push('/**');
|
|
251
251
|
lines.push(` * @flowWeaver pattern`);
|
|
252
252
|
lines.push(` * @name ${patternName}`);
|
|
253
|
-
// Node declarations
|
|
253
|
+
// Node declarations (with inline [position:] when present)
|
|
254
254
|
for (const inst of extractedInstances) {
|
|
255
|
-
|
|
255
|
+
const posAttr = inst.config?.x !== undefined && inst.config?.y !== undefined
|
|
256
|
+
? ` [position: ${inst.config.x} ${inst.config.y}]`
|
|
257
|
+
: '';
|
|
258
|
+
lines.push(` * @node ${inst.id} ${inst.nodeType}${posAttr}`);
|
|
256
259
|
}
|
|
257
260
|
// Internal connections
|
|
258
261
|
for (const conn of internalConnections) {
|
|
@@ -280,12 +283,6 @@ export function extractPattern(options) {
|
|
|
280
283
|
for (const port of [...new Set(outputPorts)]) {
|
|
281
284
|
lines.push(` * @port OUT.${port}`);
|
|
282
285
|
}
|
|
283
|
-
// Positions
|
|
284
|
-
for (const inst of extractedInstances) {
|
|
285
|
-
if (inst.config?.x !== undefined && inst.config?.y !== undefined) {
|
|
286
|
-
lines.push(` * @position ${inst.id} ${inst.config.x} ${inst.config.y}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
286
|
lines.push(' */');
|
|
290
287
|
lines.push('function patternPlaceholder() {}');
|
|
291
288
|
// Add node type functions
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -637,6 +637,8 @@ export type TValidationError = {
|
|
|
637
637
|
node?: string;
|
|
638
638
|
connection?: TConnectionAST;
|
|
639
639
|
location?: TSourceLocation;
|
|
640
|
+
/** Reference to documentation explaining this error and how to fix it. */
|
|
641
|
+
docUrl?: string;
|
|
640
642
|
};
|
|
641
643
|
export type TAnalysisResult = {
|
|
642
644
|
controlFlowGraph: TControlFlowGraph;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input functionId - Inngest function ID (e.g. "my-service/sub-workflow")
|
|
@@ -11,8 +11,8 @@ export async function invokeWorkflow(execute, functionId, payload, timeout) {
|
|
|
11
11
|
return { onSuccess: false, onFailure: false, result: {} };
|
|
12
12
|
const mocks = getMockConfig();
|
|
13
13
|
if (mocks) {
|
|
14
|
-
// Mock mode
|
|
15
|
-
const mockResult = mocks.invocations
|
|
14
|
+
// Mock mode — look up result by functionId (supports instance-qualified keys)
|
|
15
|
+
const mockResult = lookupMock(mocks.invocations, functionId);
|
|
16
16
|
if (mockResult !== undefined) {
|
|
17
17
|
return { onSuccess: true, onFailure: false, result: mockResult };
|
|
18
18
|
}
|
|
@@ -17,4 +17,24 @@ export interface FwMockConfig {
|
|
|
17
17
|
* Read the mock config from globalThis, returning undefined if not set.
|
|
18
18
|
*/
|
|
19
19
|
export declare function getMockConfig(): FwMockConfig | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Look up a mock value from a section, supporting instance-qualified keys.
|
|
22
|
+
*
|
|
23
|
+
* Checks "instanceId:key" first (for per-node targeting), then falls back
|
|
24
|
+
* to plain "key". The instance ID comes from __fw_current_node_id__ which
|
|
25
|
+
* the generated code sets before each node invocation.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```json
|
|
29
|
+
* {
|
|
30
|
+
* "invocations": {
|
|
31
|
+
* "retryCall:api/process": { "status": "ok" },
|
|
32
|
+
* "api/process": { "status": "default" }
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
* When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
|
|
37
|
+
* Any other node invoking "api/process" gets `{ status: "default" }`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function lookupMock<T>(section: Record<string, T> | undefined, key: string): T | undefined;
|
|
20
40
|
//# sourceMappingURL=mock-types.d.ts.map
|
|
@@ -9,4 +9,34 @@
|
|
|
9
9
|
export function getMockConfig() {
|
|
10
10
|
return globalThis.__fw_mocks__;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Look up a mock value from a section, supporting instance-qualified keys.
|
|
14
|
+
*
|
|
15
|
+
* Checks "instanceId:key" first (for per-node targeting), then falls back
|
|
16
|
+
* to plain "key". The instance ID comes from __fw_current_node_id__ which
|
|
17
|
+
* the generated code sets before each node invocation.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "invocations": {
|
|
23
|
+
* "retryCall:api/process": { "status": "ok" },
|
|
24
|
+
* "api/process": { "status": "default" }
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
* When the node "retryCall" invokes "api/process", it gets `{ status: "ok" }`.
|
|
29
|
+
* Any other node invoking "api/process" gets `{ status: "default" }`.
|
|
30
|
+
*/
|
|
31
|
+
export function lookupMock(section, key) {
|
|
32
|
+
if (!section)
|
|
33
|
+
return undefined;
|
|
34
|
+
const nodeId = globalThis.__fw_current_node_id__;
|
|
35
|
+
if (nodeId) {
|
|
36
|
+
const qualified = section[`${nodeId}:${key}`];
|
|
37
|
+
if (qualified !== undefined)
|
|
38
|
+
return qualified;
|
|
39
|
+
}
|
|
40
|
+
return section[key];
|
|
41
|
+
}
|
|
12
42
|
//# sourceMappingURL=mock-types.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input agentId - Agent/task identifier
|
|
@@ -9,10 +9,11 @@ import { getMockConfig } from './mock-types.js';
|
|
|
9
9
|
export async function waitForAgent(execute, agentId, context, prompt) {
|
|
10
10
|
if (!execute)
|
|
11
11
|
return { onSuccess: false, onFailure: false, agentResult: {} };
|
|
12
|
-
// 1. Check mocks first
|
|
12
|
+
// 1. Check mocks first (supports instance-qualified keys)
|
|
13
13
|
const mocks = getMockConfig();
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const mockResult = lookupMock(mocks?.agents, agentId);
|
|
15
|
+
if (mockResult !== undefined) {
|
|
16
|
+
return { onSuccess: true, onFailure: false, agentResult: mockResult };
|
|
16
17
|
}
|
|
17
18
|
// 2. Check agent channel (set by executor for pause/resume)
|
|
18
19
|
const channel = globalThis.__fw_agent_channel__;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMockConfig } from './mock-types.js';
|
|
1
|
+
import { getMockConfig, lookupMock } from './mock-types.js';
|
|
2
2
|
/**
|
|
3
3
|
* @flowWeaver nodeType
|
|
4
4
|
* @input eventName - Event name to wait for (e.g. "app/approval.received")
|
|
@@ -11,8 +11,8 @@ export async function waitForEvent(execute, eventName, match, timeout) {
|
|
|
11
11
|
return { onSuccess: false, onFailure: false, eventData: {} };
|
|
12
12
|
const mocks = getMockConfig();
|
|
13
13
|
if (mocks) {
|
|
14
|
-
// Mock mode
|
|
15
|
-
const mockData = mocks.events
|
|
14
|
+
// Mock mode — look up event data by name (supports instance-qualified keys)
|
|
15
|
+
const mockData = lookupMock(mocks.events, eventName);
|
|
16
16
|
if (mockData !== undefined) {
|
|
17
17
|
return { onSuccess: true, onFailure: false, eventData: mockData };
|
|
18
18
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Parser for @node declarations using Chevrotain.
|
|
5
5
|
*/
|
|
6
6
|
import { CstParser } from 'chevrotain';
|
|
7
|
-
import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, ColorPrefix, IconPrefix, TagsPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
|
|
7
|
+
import { JSDocLexer, NodeTag, Identifier, Dot, Integer, LabelPrefix, ExprPrefix, PortOrderPrefix, PortLabelPrefix, MinimizedKeyword, PullExecutionPrefix, SizePrefix, PositionPrefix, ColorPrefix, IconPrefix, TagsPrefix, StringLiteral, LBracket, RBracket, Comma, Equals, EventEq, CronEq, MatchEq, TimeoutEq, LimitEq, PeriodEq, allTokens, } from './tokens.js';
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Parser Definition
|
|
10
10
|
// =============================================================================
|
|
@@ -48,6 +48,7 @@ class NodeParser extends CstParser {
|
|
|
48
48
|
{ ALT: () => this.SUBRULE(this.minimizedAttr) },
|
|
49
49
|
{ ALT: () => this.SUBRULE(this.pullExecutionAttr) },
|
|
50
50
|
{ ALT: () => this.SUBRULE(this.sizeAttr) },
|
|
51
|
+
{ ALT: () => this.SUBRULE(this.positionAttr) },
|
|
51
52
|
{ ALT: () => this.SUBRULE(this.colorAttr) },
|
|
52
53
|
{ ALT: () => this.SUBRULE(this.iconAttr) },
|
|
53
54
|
{ ALT: () => this.SUBRULE(this.tagsAttr) },
|
|
@@ -140,6 +141,12 @@ class NodeParser extends CstParser {
|
|
|
140
141
|
this.CONSUME(Integer, { LABEL: 'widthValue' });
|
|
141
142
|
this.CONSUME2(Integer, { LABEL: 'heightValue' });
|
|
142
143
|
});
|
|
144
|
+
// position: x y
|
|
145
|
+
positionAttr = this.RULE('positionAttr', () => {
|
|
146
|
+
this.CONSUME(PositionPrefix);
|
|
147
|
+
this.CONSUME(Integer, { LABEL: 'xValue' });
|
|
148
|
+
this.CONSUME2(Integer, { LABEL: 'yValue' });
|
|
149
|
+
});
|
|
143
150
|
// color: "value"
|
|
144
151
|
colorAttr = this.RULE('colorAttr', () => {
|
|
145
152
|
this.CONSUME(ColorPrefix);
|
|
@@ -192,6 +199,7 @@ class NodeVisitor extends BaseVisitor {
|
|
|
192
199
|
let minimized;
|
|
193
200
|
let pullExecution;
|
|
194
201
|
let size;
|
|
202
|
+
let position;
|
|
195
203
|
let color;
|
|
196
204
|
let icon;
|
|
197
205
|
let tags;
|
|
@@ -215,6 +223,8 @@ class NodeVisitor extends BaseVisitor {
|
|
|
215
223
|
pullExecution = attrs.pullExecution;
|
|
216
224
|
if (attrs.size)
|
|
217
225
|
size = attrs.size;
|
|
226
|
+
if (attrs.position)
|
|
227
|
+
position = attrs.position;
|
|
218
228
|
if (attrs.color)
|
|
219
229
|
color = attrs.color;
|
|
220
230
|
if (attrs.icon)
|
|
@@ -234,6 +244,7 @@ class NodeVisitor extends BaseVisitor {
|
|
|
234
244
|
...(minimized && { minimized }),
|
|
235
245
|
...(pullExecution && { pullExecution }),
|
|
236
246
|
...(size && { size }),
|
|
247
|
+
...(position && { position }),
|
|
237
248
|
...(color && { color }),
|
|
238
249
|
...(icon && { icon }),
|
|
239
250
|
...(tags && { tags }),
|
|
@@ -252,6 +263,7 @@ class NodeVisitor extends BaseVisitor {
|
|
|
252
263
|
let minimized;
|
|
253
264
|
let pullExecution;
|
|
254
265
|
let size;
|
|
266
|
+
let position;
|
|
255
267
|
let color;
|
|
256
268
|
let icon;
|
|
257
269
|
let tags;
|
|
@@ -291,6 +303,11 @@ class NodeVisitor extends BaseVisitor {
|
|
|
291
303
|
size = this.visit(attr);
|
|
292
304
|
}
|
|
293
305
|
}
|
|
306
|
+
if (ctx.positionAttr) {
|
|
307
|
+
for (const attr of ctx.positionAttr) {
|
|
308
|
+
position = this.visit(attr);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
294
311
|
if (ctx.colorAttr) {
|
|
295
312
|
for (const attr of ctx.colorAttr) {
|
|
296
313
|
color = this.visit(attr);
|
|
@@ -315,6 +332,7 @@ class NodeVisitor extends BaseVisitor {
|
|
|
315
332
|
minimized,
|
|
316
333
|
pullExecution,
|
|
317
334
|
size,
|
|
335
|
+
position,
|
|
318
336
|
color,
|
|
319
337
|
icon,
|
|
320
338
|
tags,
|
|
@@ -397,6 +415,11 @@ class NodeVisitor extends BaseVisitor {
|
|
|
397
415
|
const height = parseInt(ctx.heightValue[0].image, 10);
|
|
398
416
|
return { width, height };
|
|
399
417
|
}
|
|
418
|
+
positionAttr(ctx) {
|
|
419
|
+
const x = parseInt(ctx.xValue[0].image, 10);
|
|
420
|
+
const y = parseInt(ctx.yValue[0].image, 10);
|
|
421
|
+
return { x, y };
|
|
422
|
+
}
|
|
400
423
|
colorAttr(ctx) {
|
|
401
424
|
return this.unescapeString(ctx.colorValue[0].image);
|
|
402
425
|
}
|
|
@@ -44,6 +44,7 @@ export declare const MergeStrategyPrefix: import("chevrotain").TokenType;
|
|
|
44
44
|
export declare const PullExecutionPrefix: import("chevrotain").TokenType;
|
|
45
45
|
export declare const MinimizedKeyword: import("chevrotain").TokenType;
|
|
46
46
|
export declare const SizePrefix: import("chevrotain").TokenType;
|
|
47
|
+
export declare const PositionPrefix: import("chevrotain").TokenType;
|
|
47
48
|
export declare const ColorPrefix: import("chevrotain").TokenType;
|
|
48
49
|
export declare const IconPrefix: import("chevrotain").TokenType;
|
|
49
50
|
export declare const TagsPrefix: import("chevrotain").TokenType;
|
|
@@ -172,6 +172,10 @@ export const SizePrefix = createToken({
|
|
|
172
172
|
name: 'SizePrefix',
|
|
173
173
|
pattern: /size:/,
|
|
174
174
|
});
|
|
175
|
+
export const PositionPrefix = createToken({
|
|
176
|
+
name: 'PositionPrefix',
|
|
177
|
+
pattern: /position:/,
|
|
178
|
+
});
|
|
175
179
|
export const ColorPrefix = createToken({
|
|
176
180
|
name: 'ColorPrefix',
|
|
177
181
|
pattern: /color:/,
|
|
@@ -356,6 +360,7 @@ export const allTokens = [
|
|
|
356
360
|
MergeStrategyPrefix,
|
|
357
361
|
PullExecutionPrefix,
|
|
358
362
|
SizePrefix,
|
|
363
|
+
PositionPrefix,
|
|
359
364
|
ColorPrefix,
|
|
360
365
|
IconPrefix,
|
|
361
366
|
TagsPrefix,
|
|
@@ -116,6 +116,9 @@ export async function compileCommand(input, options = {}) {
|
|
|
116
116
|
const loc = err.location ? `[line ${err.location.line}] ` : '';
|
|
117
117
|
logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
|
|
118
118
|
logger.info(` How to fix: ${friendly.fix}`);
|
|
119
|
+
if (err.docUrl) {
|
|
120
|
+
logger.info(` See: ${err.docUrl}`);
|
|
121
|
+
}
|
|
119
122
|
}
|
|
120
123
|
else {
|
|
121
124
|
let msg = ` - ${err.message}`;
|
|
@@ -123,6 +126,9 @@ export async function compileCommand(input, options = {}) {
|
|
|
123
126
|
msg += ` (node: ${err.node})`;
|
|
124
127
|
}
|
|
125
128
|
logger.error(msg);
|
|
129
|
+
if (err.docUrl) {
|
|
130
|
+
logger.info(` See: ${err.docUrl}`);
|
|
131
|
+
}
|
|
126
132
|
}
|
|
127
133
|
});
|
|
128
134
|
errorCount++;
|
|
@@ -273,12 +273,14 @@ export function generateProjectFiles(projectName, template, format = 'esm') {
|
|
|
273
273
|
].join('\n');
|
|
274
274
|
}
|
|
275
275
|
const gitignore = `node_modules/\ndist/\n.tsbuildinfo\n`;
|
|
276
|
+
const configYaml = `defaultFileType: ts\n`;
|
|
276
277
|
return {
|
|
277
278
|
'package.json': packageJson,
|
|
278
279
|
'tsconfig.json': tsconfigJson,
|
|
279
280
|
[`src/${workflowFile}`]: workflowCode,
|
|
280
281
|
'src/main.ts': mainTs,
|
|
281
282
|
'.gitignore': gitignore,
|
|
283
|
+
'.flowweaver/config.yaml': configYaml,
|
|
282
284
|
};
|
|
283
285
|
}
|
|
284
286
|
// ── Filesystem writer ────────────────────────────────────────────────────────
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Run command - execute a workflow file directly from the CLI
|
|
3
3
|
*/
|
|
4
|
+
import type { FwMockConfig } from '../../built-in-nodes/mock-types.js';
|
|
4
5
|
export interface RunOptions {
|
|
5
6
|
/** Specific workflow name to run (if file contains multiple workflows) */
|
|
6
7
|
workflow?: string;
|
|
@@ -48,4 +49,5 @@ export interface RunOptions {
|
|
|
48
49
|
* ```
|
|
49
50
|
*/
|
|
50
51
|
export declare function runCommand(input: string, options: RunOptions): Promise<void>;
|
|
52
|
+
export declare function validateMockConfig(mocks: FwMockConfig, filePath: string, workflowName?: string): Promise<void>;
|
|
51
53
|
//# sourceMappingURL=run.d.ts.map
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -9,6 +9,7 @@ import { AgentChannel } from '../../mcp/agent-channel.js';
|
|
|
9
9
|
import { logger } from '../utils/logger.js';
|
|
10
10
|
import { getFriendlyError } from '../../friendly-errors.js';
|
|
11
11
|
import { getErrorMessage } from '../../utils/error-utils.js';
|
|
12
|
+
import { parseWorkflow } from '../../api/index.js';
|
|
12
13
|
/**
|
|
13
14
|
* Execute a workflow file and output the result.
|
|
14
15
|
*
|
|
@@ -85,6 +86,10 @@ export async function runCommand(input, options) {
|
|
|
85
86
|
throw new Error(`Failed to parse mocks file: ${options.mocksFile}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
// Validate mock config against workflow when mocks are provided
|
|
90
|
+
if (mocks && !options.json) {
|
|
91
|
+
await validateMockConfig(mocks, filePath, options.workflow);
|
|
92
|
+
}
|
|
88
93
|
// Set up timeout if specified
|
|
89
94
|
let timeoutId;
|
|
90
95
|
let timedOut = false;
|
|
@@ -263,6 +268,39 @@ export async function runCommand(input, options) {
|
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
}
|
|
271
|
+
const VALID_MOCK_KEYS = new Set(['events', 'invocations', 'agents', 'fast']);
|
|
272
|
+
const BUILT_IN_NODE_TYPES = new Set(['delay', 'waitForEvent', 'invokeWorkflow', 'waitForAgent']);
|
|
273
|
+
const MOCK_SECTION_TO_NODE = {
|
|
274
|
+
events: 'waitForEvent',
|
|
275
|
+
invocations: 'invokeWorkflow',
|
|
276
|
+
agents: 'waitForAgent',
|
|
277
|
+
};
|
|
278
|
+
export async function validateMockConfig(mocks, filePath, workflowName) {
|
|
279
|
+
// Check for unknown top-level keys (catches typos like "invocation" instead of "invocations")
|
|
280
|
+
for (const key of Object.keys(mocks)) {
|
|
281
|
+
if (!VALID_MOCK_KEYS.has(key)) {
|
|
282
|
+
logger.warn(`Mock config has unknown key "${key}". Valid keys: ${[...VALID_MOCK_KEYS].join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Quick-parse the workflow to check which built-in node types are used
|
|
286
|
+
try {
|
|
287
|
+
const result = await parseWorkflow(filePath, { workflowName });
|
|
288
|
+
if (result.errors.length > 0 || !result.ast?.instances)
|
|
289
|
+
return;
|
|
290
|
+
const usedNodeTypes = new Set(result.ast.instances.map((i) => i.nodeType));
|
|
291
|
+
for (const [section, nodeType] of Object.entries(MOCK_SECTION_TO_NODE)) {
|
|
292
|
+
const mockSection = mocks[section];
|
|
293
|
+
if (mockSection && typeof mockSection === 'object' && Object.keys(mockSection).length > 0) {
|
|
294
|
+
if (!usedNodeTypes.has(nodeType)) {
|
|
295
|
+
logger.warn(`Mock config has "${section}" entries but workflow has no ${nodeType} nodes`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
// Parsing failed — skip validation, the execution will report the real error
|
|
302
|
+
}
|
|
303
|
+
}
|
|
266
304
|
function promptForInput(question) {
|
|
267
305
|
return new Promise((resolve) => {
|
|
268
306
|
const rl = readline.createInterface({
|
|
@@ -140,6 +140,9 @@ export async function validateCommand(input, options = {}) {
|
|
|
140
140
|
const loc = err.location ? `[line ${err.location.line}] ` : '';
|
|
141
141
|
logger.error(` ${loc}${friendly.title}: ${friendly.explanation}`);
|
|
142
142
|
logger.info(` How to fix: ${friendly.fix}`);
|
|
143
|
+
if (err.docUrl) {
|
|
144
|
+
logger.info(` See: ${err.docUrl}`);
|
|
145
|
+
}
|
|
143
146
|
}
|
|
144
147
|
else {
|
|
145
148
|
let msg = ` - ${err.message}`;
|
|
@@ -153,6 +156,9 @@ export async function validateCommand(input, options = {}) {
|
|
|
153
156
|
msg += ` (connection: ${err.connection.from.node}:${err.connection.from.port} -> ${err.connection.to.node}:${err.connection.to.port})`;
|
|
154
157
|
}
|
|
155
158
|
logger.error(msg);
|
|
159
|
+
if (err.docUrl) {
|
|
160
|
+
logger.info(` See: ${err.docUrl}`);
|
|
161
|
+
}
|
|
156
162
|
}
|
|
157
163
|
});
|
|
158
164
|
}
|
|
@@ -167,6 +173,9 @@ export async function validateCommand(input, options = {}) {
|
|
|
167
173
|
const loc = warn.location ? `[line ${warn.location.line}] ` : '';
|
|
168
174
|
logger.warn(` ${loc}${friendly.title}: ${friendly.explanation}`);
|
|
169
175
|
logger.info(` How to fix: ${friendly.fix}`);
|
|
176
|
+
if (warn.docUrl) {
|
|
177
|
+
logger.info(` See: ${warn.docUrl}`);
|
|
178
|
+
}
|
|
170
179
|
}
|
|
171
180
|
else {
|
|
172
181
|
let msg = ` - ${warn.message}`;
|
|
@@ -177,6 +186,9 @@ export async function validateCommand(input, options = {}) {
|
|
|
177
186
|
msg += ` (node: ${warn.node})`;
|
|
178
187
|
}
|
|
179
188
|
logger.warn(msg);
|
|
189
|
+
if (warn.docUrl) {
|
|
190
|
+
logger.info(` See: ${warn.docUrl}`);
|
|
191
|
+
}
|
|
180
192
|
}
|
|
181
193
|
});
|
|
182
194
|
}
|