devglide 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/bin/claude-md-template.js +94 -0
  4. package/bin/devglide.js +387 -0
  5. package/package.json +85 -0
  6. package/pnpm-workspace.yaml +3 -0
  7. package/src/apps/coder/.turbo/turbo-lint.log +5 -0
  8. package/src/apps/coder/package.json +16 -0
  9. package/src/apps/coder/public/favicon.svg +7 -0
  10. package/src/apps/coder/public/page.css +275 -0
  11. package/src/apps/coder/public/page.js +528 -0
  12. package/src/apps/coder/server.js +3 -0
  13. package/src/apps/documentation/public/page.css +597 -0
  14. package/src/apps/documentation/public/page.js +609 -0
  15. package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
  16. package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
  17. package/src/apps/kanban/package.json +32 -0
  18. package/src/apps/kanban/public/favicon.svg +7 -0
  19. package/src/apps/kanban/public/page.css +1010 -0
  20. package/src/apps/kanban/public/page.js +1730 -0
  21. package/src/apps/kanban/public/vendor/marked.min.js +6 -0
  22. package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
  23. package/src/apps/kanban/src/db.ts +319 -0
  24. package/src/apps/kanban/src/index.ts +14 -0
  25. package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
  26. package/src/apps/kanban/src/mcp-helpers.ts +60 -0
  27. package/src/apps/kanban/src/mcp.ts +59 -0
  28. package/src/apps/kanban/src/routes/attachments.ts +161 -0
  29. package/src/apps/kanban/src/routes/features.ts +233 -0
  30. package/src/apps/kanban/src/routes/issues.ts +373 -0
  31. package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
  32. package/src/apps/kanban/src/tools/item-tools.ts +307 -0
  33. package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
  34. package/src/apps/kanban/tsconfig.check.json +9 -0
  35. package/src/apps/kanban/tsconfig.json +9 -0
  36. package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
  37. package/src/apps/keymap/package.json +16 -0
  38. package/src/apps/keymap/public/page.css +275 -0
  39. package/src/apps/keymap/public/page.js +294 -0
  40. package/src/apps/keymap/server.js +25 -0
  41. package/src/apps/log/.turbo/turbo-build.log +5 -0
  42. package/src/apps/log/.turbo/turbo-lint.log +45 -0
  43. package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
  44. package/src/apps/log/node_modules/.bin/tsc +21 -0
  45. package/src/apps/log/node_modules/.bin/tsserver +21 -0
  46. package/src/apps/log/node_modules/.bin/tsx +21 -0
  47. package/src/apps/log/package.json +36 -0
  48. package/src/apps/log/public/console-sniffer.js +221 -0
  49. package/src/apps/log/public/favicon.svg +7 -0
  50. package/src/apps/log/public/page.css +322 -0
  51. package/src/apps/log/public/page.js +463 -0
  52. package/src/apps/log/src/index.ts +9 -0
  53. package/src/apps/log/src/mcp.ts +122 -0
  54. package/src/apps/log/src/routes/log.ts +333 -0
  55. package/src/apps/log/src/routes/status.ts +25 -0
  56. package/src/apps/log/src/server-sniffer.ts +118 -0
  57. package/src/apps/log/src/services/file-patterns.ts +39 -0
  58. package/src/apps/log/src/services/file-tailer.ts +228 -0
  59. package/src/apps/log/src/services/line-parser.ts +94 -0
  60. package/src/apps/log/src/services/log-writer.ts +39 -0
  61. package/src/apps/log/tsconfig.json +8 -0
  62. package/src/apps/prompts/.turbo/turbo-build.log +5 -0
  63. package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
  64. package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
  65. package/src/apps/prompts/mcp.ts +175 -0
  66. package/src/apps/prompts/node_modules/.bin/tsc +21 -0
  67. package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
  68. package/src/apps/prompts/node_modules/.bin/tsx +21 -0
  69. package/src/apps/prompts/package.json +25 -0
  70. package/src/apps/prompts/public/page.css +315 -0
  71. package/src/apps/prompts/public/page.js +541 -0
  72. package/src/apps/prompts/services/prompt-store.ts +212 -0
  73. package/src/apps/prompts/src/index.ts +9 -0
  74. package/src/apps/prompts/tsconfig.json +8 -0
  75. package/src/apps/prompts/types.ts +27 -0
  76. package/src/apps/shell/.turbo/turbo-build.log +5 -0
  77. package/src/apps/shell/.turbo/turbo-lint.log +34 -0
  78. package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
  79. package/src/apps/shell/package.json +35 -0
  80. package/src/apps/shell/public/favicon.svg +7 -0
  81. package/src/apps/shell/public/page.css +407 -0
  82. package/src/apps/shell/public/page.js +1577 -0
  83. package/src/apps/shell/src/index.ts +150 -0
  84. package/src/apps/shell/src/mcp.ts +398 -0
  85. package/src/apps/shell/src/shell-types.ts +41 -0
  86. package/src/apps/shell/tsconfig.json +8 -0
  87. package/src/apps/test/.turbo/turbo-build.log +5 -0
  88. package/src/apps/test/.turbo/turbo-lint.log +27 -0
  89. package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
  90. package/src/apps/test/node_modules/.bin/tsc +21 -0
  91. package/src/apps/test/node_modules/.bin/tsserver +21 -0
  92. package/src/apps/test/node_modules/.bin/tsx +21 -0
  93. package/src/apps/test/node_modules/.bin/uuid +21 -0
  94. package/src/apps/test/package.json +35 -0
  95. package/src/apps/test/public/favicon.svg +7 -0
  96. package/src/apps/test/public/page.css +499 -0
  97. package/src/apps/test/public/page.js +417 -0
  98. package/src/apps/test/public/scenario-runner.js +450 -0
  99. package/src/apps/test/src/index.ts +9 -0
  100. package/src/apps/test/src/mcp.ts +192 -0
  101. package/src/apps/test/src/routes/trigger.ts +285 -0
  102. package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
  103. package/src/apps/test/src/services/scenario-manager.ts +361 -0
  104. package/src/apps/test/src/services/scenario-store.ts +145 -0
  105. package/src/apps/test/tsconfig.json +8 -0
  106. package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
  107. package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
  108. package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
  109. package/src/apps/vocabulary/mcp.ts +173 -0
  110. package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
  111. package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
  112. package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
  113. package/src/apps/vocabulary/package.json +25 -0
  114. package/src/apps/vocabulary/public/page.css +247 -0
  115. package/src/apps/vocabulary/public/page.js +444 -0
  116. package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
  117. package/src/apps/vocabulary/src/index.ts +10 -0
  118. package/src/apps/vocabulary/tsconfig.json +8 -0
  119. package/src/apps/vocabulary/types.ts +22 -0
  120. package/src/apps/voice/.turbo/turbo-build.log +5 -0
  121. package/src/apps/voice/.turbo/turbo-lint.log +43 -0
  122. package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
  123. package/src/apps/voice/node_modules/.bin/openai +21 -0
  124. package/src/apps/voice/node_modules/.bin/tsc +21 -0
  125. package/src/apps/voice/node_modules/.bin/tsserver +21 -0
  126. package/src/apps/voice/node_modules/.bin/tsx +21 -0
  127. package/src/apps/voice/package.json +35 -0
  128. package/src/apps/voice/public/favicon.svg +7 -0
  129. package/src/apps/voice/public/page.css +388 -0
  130. package/src/apps/voice/public/page.js +718 -0
  131. package/src/apps/voice/src/index.ts +10 -0
  132. package/src/apps/voice/src/mcp.ts +70 -0
  133. package/src/apps/voice/src/providers/index.ts +85 -0
  134. package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
  135. package/src/apps/voice/src/providers/types.ts +27 -0
  136. package/src/apps/voice/src/routes/config.ts +118 -0
  137. package/src/apps/voice/src/routes/transcribe.ts +90 -0
  138. package/src/apps/voice/src/services/config-store.ts +129 -0
  139. package/src/apps/voice/src/services/stats.ts +108 -0
  140. package/src/apps/voice/src/transcribe.ts +11 -0
  141. package/src/apps/voice/src/utils/mime.ts +16 -0
  142. package/src/apps/voice/tsconfig.json +8 -0
  143. package/src/apps/workflow/.turbo/turbo-build.log +5 -0
  144. package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
  145. package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
  146. package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
  147. package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
  148. package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
  149. package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
  150. package/src/apps/workflow/engine/executors/index.ts +28 -0
  151. package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
  152. package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
  153. package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
  154. package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
  155. package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
  156. package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
  157. package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
  158. package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
  159. package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
  160. package/src/apps/workflow/engine/graph-runner.ts +438 -0
  161. package/src/apps/workflow/engine/node-executor.ts +104 -0
  162. package/src/apps/workflow/engine/node-registry.ts +15 -0
  163. package/src/apps/workflow/engine/variable-resolver.ts +109 -0
  164. package/src/apps/workflow/mcp.ts +223 -0
  165. package/src/apps/workflow/node_modules/.bin/tsc +21 -0
  166. package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
  167. package/src/apps/workflow/node_modules/.bin/tsx +21 -0
  168. package/src/apps/workflow/package.json +25 -0
  169. package/src/apps/workflow/public/editor/canvas.js +366 -0
  170. package/src/apps/workflow/public/editor/drag-manager.js +326 -0
  171. package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
  172. package/src/apps/workflow/public/editor/history-manager.js +147 -0
  173. package/src/apps/workflow/public/editor/layout-engine.js +159 -0
  174. package/src/apps/workflow/public/editor/node-renderer.js +199 -0
  175. package/src/apps/workflow/public/editor/selection-manager.js +193 -0
  176. package/src/apps/workflow/public/favicon.svg +7 -0
  177. package/src/apps/workflow/public/models/node-types.js +300 -0
  178. package/src/apps/workflow/public/models/workflow-model.js +257 -0
  179. package/src/apps/workflow/public/page.css +406 -0
  180. package/src/apps/workflow/public/page.js +658 -0
  181. package/src/apps/workflow/public/panels/inspector.js +360 -0
  182. package/src/apps/workflow/public/panels/palette.js +106 -0
  183. package/src/apps/workflow/public/panels/run-view.js +275 -0
  184. package/src/apps/workflow/public/panels/toolbar.js +232 -0
  185. package/src/apps/workflow/public/panels/workflow-list.js +237 -0
  186. package/src/apps/workflow/public/state/store.js +47 -0
  187. package/src/apps/workflow/services/custom-node-loader.ts +48 -0
  188. package/src/apps/workflow/services/legacy-converter.ts +72 -0
  189. package/src/apps/workflow/services/run-manager.ts +190 -0
  190. package/src/apps/workflow/services/workflow-store.ts +424 -0
  191. package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
  192. package/src/apps/workflow/services/workflow-validator.ts +98 -0
  193. package/src/apps/workflow/src/index.ts +10 -0
  194. package/src/apps/workflow/templates/ci-pipeline.json +18 -0
  195. package/src/apps/workflow/templates/code-review.json +22 -0
  196. package/src/apps/workflow/templates/kanban-testing.json +24 -0
  197. package/src/apps/workflow/tsconfig.json +8 -0
  198. package/src/apps/workflow/types.ts +268 -0
  199. package/src/packages/auth-middleware.ts +14 -0
  200. package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
  201. package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
  202. package/src/packages/design-tokens/build.js +413 -0
  203. package/src/packages/design-tokens/demo/index.html +1367 -0
  204. package/src/packages/design-tokens/demo/proposition-a.html +717 -0
  205. package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
  206. package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
  207. package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
  208. package/src/packages/design-tokens/dist/tokens.css +345 -0
  209. package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
  210. package/src/packages/design-tokens/dist/tokens.js +386 -0
  211. package/src/packages/design-tokens/package.json +25 -0
  212. package/src/packages/design-tokens/tokens.json +228 -0
  213. package/src/packages/devtools-middleware.ts +22 -0
  214. package/src/packages/eslint-config/index.js +63 -0
  215. package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
  216. package/src/packages/eslint-config/package.json +18 -0
  217. package/src/packages/json-file-store.ts +232 -0
  218. package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
  219. package/src/packages/mcp-utils/dist/index.d.ts +33 -0
  220. package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
  221. package/src/packages/mcp-utils/dist/index.js +126 -0
  222. package/src/packages/mcp-utils/dist/index.js.map +1 -0
  223. package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
  224. package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
  225. package/src/packages/mcp-utils/package.json +32 -0
  226. package/src/packages/mcp-utils/src/index.ts +171 -0
  227. package/src/packages/mcp-utils/tsconfig.json +9 -0
  228. package/src/packages/paths.ts +18 -0
  229. package/src/packages/project-context/index.js +55 -0
  230. package/src/packages/project-context/package.json +13 -0
  231. package/src/packages/project-store.ts +127 -0
  232. package/src/packages/server-sniffer.ts +132 -0
  233. package/src/packages/shared-assets/favicon.svg +7 -0
  234. package/src/packages/shared-assets/keymap-registry.js +512 -0
  235. package/src/packages/shared-assets/logo.svg +6 -0
  236. package/src/packages/shared-assets/package.json +11 -0
  237. package/src/packages/shared-assets/ui-utils.js +48 -0
  238. package/src/packages/shared-assets/voice-widget.d.ts +37 -0
  239. package/src/packages/shared-assets/voice-widget.js +695 -0
  240. package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
  241. package/src/packages/shared-types/dist/index.d.ts +39 -0
  242. package/src/packages/shared-types/dist/index.d.ts.map +1 -0
  243. package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
  244. package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
  245. package/src/packages/shared-types/package.json +25 -0
  246. package/src/packages/shared-types/src/index.ts +41 -0
  247. package/src/packages/shared-types/tsconfig.json +11 -0
  248. package/src/packages/tsconfig/base.json +15 -0
  249. package/src/packages/tsconfig/next.json +14 -0
  250. package/src/packages/tsconfig/node.json +11 -0
  251. package/src/packages/tsconfig/package.json +10 -0
  252. package/turbo.json +25 -0
@@ -0,0 +1,438 @@
1
+ import type {
2
+ Workflow,
3
+ WorkflowNode,
4
+ WorkflowEdge,
5
+ ExecutionContext,
6
+ ExecutorServices,
7
+ SSEEmitter,
8
+ RunStatus,
9
+ LoopConfig,
10
+ } from '../types.js';
11
+ import { executeNode } from './node-executor.js';
12
+ import { evaluate } from './expression-evaluator.js';
13
+ import { getActiveProject } from '../../../project-context.js';
14
+
15
+ const MAX_LOOP_ITERATIONS = 1000;
16
+
17
+ export async function runWorkflow(
18
+ workflow: Workflow,
19
+ emit: SSEEmitter,
20
+ triggerPayload?: unknown,
21
+ initialVariables?: Map<string, unknown>,
22
+ services?: ExecutorServices,
23
+ ): Promise<ExecutionContext> {
24
+ const nodeMap = new Map<string, WorkflowNode>();
25
+ for (const node of workflow.nodes) nodeMap.set(node.id, node);
26
+
27
+ const outgoing = new Map<string, WorkflowEdge[]>();
28
+ const incoming = new Map<string, WorkflowEdge[]>();
29
+ for (const node of workflow.nodes) {
30
+ outgoing.set(node.id, []);
31
+ incoming.set(node.id, []);
32
+ }
33
+ for (const edge of workflow.edges) {
34
+ outgoing.get(edge.source)?.push(edge);
35
+ incoming.get(edge.target)?.push(edge);
36
+ }
37
+
38
+ const startNodes = workflow.nodes.filter(
39
+ (n) => n.type === 'trigger' || (incoming.get(n.id)?.length ?? 0) === 0,
40
+ );
41
+
42
+ // Snapshot active project at workflow start — executors use this instead of the
43
+ // mutable global, so mid-run project switches don't affect execution.
44
+ const ap = getActiveProject();
45
+ const projectSnapshot = ap
46
+ ? { id: ap.id, name: ap.name, path: ap.path }
47
+ : undefined;
48
+
49
+ const context: ExecutionContext = {
50
+ runId: crypto.randomUUID(),
51
+ workflowId: workflow.id,
52
+ variables: new Map<string, unknown>(),
53
+ nodeStates: new Map(),
54
+ status: 'running',
55
+ startedAt: new Date().toISOString(),
56
+ cancelled: false,
57
+ project: projectSnapshot,
58
+ services: services ?? {},
59
+ };
60
+
61
+ for (const v of workflow.variables) {
62
+ if (v.defaultValue !== undefined) {
63
+ context.variables.set(v.name, coerceVariable(v.defaultValue, v.type));
64
+ }
65
+ }
66
+
67
+ if (initialVariables) {
68
+ for (const [key, value] of initialVariables) {
69
+ context.variables.set(key, value);
70
+ }
71
+ }
72
+
73
+ if (triggerPayload !== undefined) {
74
+ context.variables.set('__triggerPayload', triggerPayload);
75
+ }
76
+
77
+ const queue: string[] = startNodes.map((n) => n.id);
78
+ const queueSet = new Set<string>(queue);
79
+ const loopCounters = new Map<string, number>();
80
+ const requeueCounts = new Map<string, number>();
81
+ const maxRequeues = workflow.nodes.length * 2;
82
+
83
+ while (queue.length > 0 && !context.cancelled) {
84
+ const nodeId = queue.shift()!;
85
+ queueSet.delete(nodeId);
86
+ const node = nodeMap.get(nodeId);
87
+ if (!node) continue;
88
+
89
+ if (!allPredecessorsComplete(nodeId, incoming, context)) {
90
+ const count = (requeueCounts.get(nodeId) ?? 0) + 1;
91
+ if (count > maxRequeues) {
92
+ context.nodeStates.set(nodeId, {
93
+ nodeId,
94
+ status: 'failed',
95
+ error: `Node "${node.label ?? nodeId}" stalled: predecessors never completed`,
96
+ startedAt: new Date().toISOString(),
97
+ completedAt: new Date().toISOString(),
98
+ retryCount: 0,
99
+ });
100
+ emit({ type: 'error', message: `Predecessor stall detected for node "${node.label ?? nodeId}"` });
101
+ continue;
102
+ }
103
+ requeueCounts.set(nodeId, count);
104
+ queue.push(nodeId);
105
+ queueSet.add(nodeId);
106
+ continue;
107
+ }
108
+
109
+ const existingState = context.nodeStates.get(nodeId);
110
+ if (existingState && (existingState.status === 'passed' || existingState.status === 'failed')) {
111
+ if (node.type !== 'loop') {
112
+ enqueueSuccessors(node, outgoing, context, queue, queueSet, emit);
113
+ continue;
114
+ }
115
+ }
116
+
117
+ // At join nodes, propagate failure if any non-error predecessor failed
118
+ if (anyPredecessorFailed(nodeId, incoming, context)) {
119
+ context.nodeStates.set(nodeId, {
120
+ nodeId,
121
+ status: 'failed',
122
+ error: `Skipped: predecessor on incoming branch failed`,
123
+ startedAt: new Date().toISOString(),
124
+ completedAt: new Date().toISOString(),
125
+ retryCount: 0,
126
+ });
127
+ emit({ type: 'node_done', nodeId, status: 'failed' });
128
+ handleFailure(node, outgoing, context, queue, queueSet, emit);
129
+ continue;
130
+ }
131
+
132
+ // Provide predecessor IDs so decision executor can find actual predecessors
133
+ if (node.config.nodeType === 'decision') {
134
+ const predecessorIds = (incoming.get(nodeId) ?? []).map((e) => e.source);
135
+ context.variables.set('__predecessor_ids', predecessorIds);
136
+ }
137
+
138
+ const result = await executeNode(node, context, emit);
139
+
140
+ // Clean up temporary predecessor context
141
+ context.variables.delete('__predecessor_ids');
142
+
143
+ if (node.config.nodeType === 'decision') {
144
+ handleDecision(node, outgoing, context, queue, queueSet, emit);
145
+ } else if (node.config.nodeType === 'loop') {
146
+ handleLoop(node, outgoing, incoming, context, queue, queueSet, emit, nodeMap, loopCounters);
147
+ } else if (result.status === 'failed') {
148
+ handleFailure(node, outgoing, context, queue, queueSet, emit);
149
+ } else {
150
+ enqueueSuccessors(node, outgoing, context, queue, queueSet, emit);
151
+ }
152
+ }
153
+
154
+ const allStates = [...context.nodeStates.values()];
155
+ const anyFailed = allStates.some((s) => s.status === 'failed');
156
+ context.status = context.cancelled ? 'cancelled' : anyFailed ? 'failed' : 'passed';
157
+
158
+ emit({ type: 'done', status: context.status });
159
+ return context;
160
+ }
161
+
162
+ function allPredecessorsComplete(
163
+ nodeId: string,
164
+ incoming: Map<string, WorkflowEdge[]>,
165
+ context: ExecutionContext,
166
+ ): boolean {
167
+ const edges = incoming.get(nodeId) ?? [];
168
+ if (edges.length === 0) return true;
169
+
170
+ return edges.every((edge) => {
171
+ const state = context.nodeStates.get(edge.source);
172
+ return state && (state.status === 'passed' || state.status === 'failed');
173
+ });
174
+ }
175
+
176
+ function anyPredecessorFailed(
177
+ nodeId: string,
178
+ incoming: Map<string, WorkflowEdge[]>,
179
+ context: ExecutionContext,
180
+ ): boolean {
181
+ const edges = incoming.get(nodeId) ?? [];
182
+ return edges.some((edge) => {
183
+ // Error edges carry failure intentionally — don't propagate through them
184
+ if (edge.sourcePort === 'error') return false;
185
+ const state = context.nodeStates.get(edge.source);
186
+ return state?.status === 'failed';
187
+ });
188
+ }
189
+
190
+ function enqueueSuccessors(
191
+ node: WorkflowNode,
192
+ outgoing: Map<string, WorkflowEdge[]>,
193
+ context: ExecutionContext,
194
+ queue: string[],
195
+ queueSet: Set<string>,
196
+ emit: SSEEmitter,
197
+ ): void {
198
+ const edges = outgoing.get(node.id) ?? [];
199
+ for (const edge of edges) {
200
+ if (edge.sourcePort === 'error') continue;
201
+ if (edge.condition && !evaluateEdgeCondition(edge, context)) continue;
202
+
203
+ emit({ type: 'edge_traversed', edgeId: edge.id, source: edge.source, target: edge.target });
204
+ if (!queueSet.has(edge.target)) {
205
+ queue.push(edge.target);
206
+ queueSet.add(edge.target);
207
+ }
208
+ }
209
+ }
210
+
211
+ function handleFailure(
212
+ node: WorkflowNode,
213
+ outgoing: Map<string, WorkflowEdge[]>,
214
+ context: ExecutionContext,
215
+ queue: string[],
216
+ queueSet: Set<string>,
217
+ emit: SSEEmitter,
218
+ ): void {
219
+ const edges = outgoing.get(node.id) ?? [];
220
+ const errorEdges = edges.filter((e) => e.sourcePort === 'error');
221
+
222
+ if (errorEdges.length > 0) {
223
+ for (const edge of errorEdges) {
224
+ emit({ type: 'edge_traversed', edgeId: edge.id, source: edge.source, target: edge.target });
225
+ if (!queueSet.has(edge.target)) {
226
+ queue.push(edge.target);
227
+ queueSet.add(edge.target);
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ function handleDecision(
234
+ node: WorkflowNode,
235
+ outgoing: Map<string, WorkflowEdge[]>,
236
+ context: ExecutionContext,
237
+ queue: string[],
238
+ queueSet: Set<string>,
239
+ emit: SSEEmitter,
240
+ ): void {
241
+ const edges = outgoing.get(node.id) ?? [];
242
+
243
+ // Use __decision_port set by the decision executor — single source of truth
244
+ const matchedPort = context.variables.get('__decision_port') as string | undefined;
245
+
246
+ if (matchedPort) {
247
+ emit({ type: 'decision_result', nodeId: node.id, port: matchedPort });
248
+
249
+ for (const edge of edges) {
250
+ if (edge.sourcePort === matchedPort) {
251
+ emit({ type: 'edge_traversed', edgeId: edge.id, source: edge.source, target: edge.target });
252
+ if (!queueSet.has(edge.target)) {
253
+ queue.push(edge.target);
254
+ queueSet.add(edge.target);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ function handleLoop(
262
+ node: WorkflowNode,
263
+ outgoing: Map<string, WorkflowEdge[]>,
264
+ incoming: Map<string, WorkflowEdge[]>,
265
+ context: ExecutionContext,
266
+ queue: string[],
267
+ queueSet: Set<string>,
268
+ emit: SSEEmitter,
269
+ nodeMap: Map<string, WorkflowNode>,
270
+ loopCounters: Map<string, number>,
271
+ ): void {
272
+ const config = node.config as LoopConfig;
273
+ const maxIter = config.maxIterations ?? MAX_LOOP_ITERATIONS;
274
+ const counter = loopCounters.get(node.id) ?? 0;
275
+
276
+ const edges = outgoing.get(node.id) ?? [];
277
+ const bodyEdges = edges.filter((e) => e.sourcePort !== 'done' && e.sourcePort !== 'error');
278
+ const doneEdges = edges.filter((e) => e.sourcePort === 'done');
279
+
280
+ const shouldContinue = evaluateLoopCondition(config, counter, context);
281
+
282
+ if (shouldContinue && counter < maxIter) {
283
+ context.loopContext = {
284
+ index: counter,
285
+ item: getLoopItem(config, counter, context),
286
+ collection: getLoopCollection(config, context),
287
+ };
288
+
289
+ emit({ type: 'loop_iteration', nodeId: node.id, index: counter });
290
+ loopCounters.set(node.id, counter + 1);
291
+
292
+ const bodyNodeIds = collectBodyNodes(bodyEdges, outgoing, doneEdges.map((e) => e.target));
293
+ for (const id of bodyNodeIds) {
294
+ context.nodeStates.delete(id);
295
+ }
296
+
297
+ for (const edge of bodyEdges) {
298
+ emit({ type: 'edge_traversed', edgeId: edge.id, source: edge.source, target: edge.target });
299
+ if (!queueSet.has(edge.target)) {
300
+ queue.push(edge.target);
301
+ queueSet.add(edge.target);
302
+ }
303
+ }
304
+
305
+ if (!queueSet.has(node.id)) {
306
+ queue.push(node.id);
307
+ queueSet.add(node.id);
308
+ }
309
+
310
+ context.nodeStates.delete(node.id);
311
+ } else {
312
+ context.loopContext = undefined;
313
+ for (const edge of doneEdges) {
314
+ emit({ type: 'edge_traversed', edgeId: edge.id, source: edge.source, target: edge.target });
315
+ if (!queueSet.has(edge.target)) {
316
+ queue.push(edge.target);
317
+ queueSet.add(edge.target);
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ function evaluateLoopCondition(
324
+ config: LoopConfig,
325
+ counter: number,
326
+ context: ExecutionContext,
327
+ ): boolean {
328
+ switch (config.loopType) {
329
+ case 'count':
330
+ return counter < (config.count ?? 0);
331
+ case 'while':
332
+ return config.condition ? evaluate(config.condition, context) : false;
333
+ case 'for-each': {
334
+ const collection = getLoopCollection(config, context);
335
+ return collection !== undefined && counter < (collection?.length ?? 0);
336
+ }
337
+ default:
338
+ return false;
339
+ }
340
+ }
341
+
342
+ function getLoopCollection(config: LoopConfig, context: ExecutionContext): unknown[] | undefined {
343
+ if (!config.collection) return undefined;
344
+ const value = context.variables.get(config.collection);
345
+ return Array.isArray(value) ? value : undefined;
346
+ }
347
+
348
+ function getLoopItem(config: LoopConfig, index: number, context: ExecutionContext): unknown {
349
+ if (config.loopType !== 'for-each') return undefined;
350
+ const collection = getLoopCollection(config, context);
351
+ return collection?.[index];
352
+ }
353
+
354
+ function collectBodyNodes(
355
+ bodyEdges: WorkflowEdge[],
356
+ outgoing: Map<string, WorkflowEdge[]>,
357
+ exitNodeIds: string[],
358
+ ): Set<string> {
359
+ const bodyNodes = new Set<string>();
360
+ const visited = new Set<string>();
361
+ const stack = bodyEdges.map((e) => e.target);
362
+
363
+ while (stack.length > 0) {
364
+ const id = stack.pop()!;
365
+ if (visited.has(id) || exitNodeIds.includes(id)) continue;
366
+ visited.add(id);
367
+ bodyNodes.add(id);
368
+
369
+ const successors = outgoing.get(id) ?? [];
370
+ for (const edge of successors) {
371
+ stack.push(edge.target);
372
+ }
373
+ }
374
+
375
+ return bodyNodes;
376
+ }
377
+
378
+ function evaluateEdgeCondition(edge: WorkflowEdge, context: ExecutionContext): boolean {
379
+ if (!edge.condition) return true;
380
+
381
+ const { type, variable, operator, value, expression } = edge.condition;
382
+
383
+ switch (type) {
384
+ case 'expression':
385
+ return expression ? evaluate(expression, context) : true;
386
+
387
+ case 'exit-code': {
388
+ const sourceState = context.nodeStates.get(edge.source);
389
+ const exitCode = sourceState?.exitCode;
390
+ const target = value !== undefined ? Number(value) : 0;
391
+ return exitCode === target;
392
+ }
393
+
394
+ case 'variable-match': {
395
+ if (!variable || !operator) return true;
396
+ const actual = String(context.variables.get(variable) ?? '');
397
+ const expected = value ?? '';
398
+ return compareValues(actual, expected, operator);
399
+ }
400
+
401
+ default:
402
+ return true;
403
+ }
404
+ }
405
+
406
+ function compareValues(
407
+ left: string,
408
+ right: string,
409
+ operator: string,
410
+ ): boolean {
411
+ switch (operator) {
412
+ case '==': return left === right;
413
+ case '!=': return left !== right;
414
+ case '>': return Number(left) > Number(right);
415
+ case '<': return Number(left) < Number(right);
416
+ case '>=': return Number(left) >= Number(right);
417
+ case '<=': return Number(left) <= Number(right);
418
+ case 'contains': return left.includes(right);
419
+ case 'matches': {
420
+ try {
421
+ if (right.length > 200) return false;
422
+ return new RegExp(right).test(left);
423
+ } catch { return false; }
424
+ }
425
+ default: return false;
426
+ }
427
+ }
428
+
429
+ function coerceVariable(value: string, type: string): unknown {
430
+ switch (type) {
431
+ case 'number': return Number(value);
432
+ case 'boolean': return value === 'true';
433
+ case 'json': {
434
+ try { return JSON.parse(value); } catch { return value; }
435
+ }
436
+ default: return value;
437
+ }
438
+ }
@@ -0,0 +1,104 @@
1
+ import type {
2
+ WorkflowNode,
3
+ ExecutionContext,
4
+ SSEEmitter,
5
+ ExecutorResult,
6
+ NodeExecutionState,
7
+ } from '../types.js';
8
+ import { getExecutor } from './node-registry.js';
9
+ import { VariableResolver } from './variable-resolver.js';
10
+
11
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
12
+
13
+ const resolver = new VariableResolver();
14
+
15
+ export async function executeNode(
16
+ node: WorkflowNode,
17
+ context: ExecutionContext,
18
+ emit: SSEEmitter,
19
+ ): Promise<ExecutorResult> {
20
+ const executor = getExecutor(node.type);
21
+ if (!executor) {
22
+ const error = `No executor registered for node type: ${node.type}`;
23
+ const state = failState(node.id, error);
24
+ context.nodeStates.set(node.id, state);
25
+ emit({ type: 'node_done', nodeId: node.id, status: 'failed' });
26
+ return { status: 'failed', error };
27
+ }
28
+
29
+ const resolvedConfig = resolver.resolveObject(node.config, context);
30
+ const maxRetries = node.retries ?? 0;
31
+ const retryDelay = node.retryDelay ?? 1000;
32
+ const timeout = node.timeout ?? DEFAULT_TIMEOUT_MS;
33
+
34
+ const state: NodeExecutionState = {
35
+ nodeId: node.id,
36
+ status: 'running',
37
+ startedAt: new Date().toISOString(),
38
+ retryCount: 0,
39
+ };
40
+ context.nodeStates.set(node.id, state);
41
+ emit({ type: 'node_start', nodeId: node.id });
42
+
43
+ let result: ExecutorResult | undefined;
44
+
45
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
46
+ state.retryCount = attempt;
47
+
48
+ try {
49
+ result = await withTimeout(executor(resolvedConfig, context, emit), timeout);
50
+ } catch (err) {
51
+ const message = err instanceof Error ? err.message : String(err);
52
+ result = { status: 'failed', error: message };
53
+ }
54
+
55
+ if (result.status === 'passed' || attempt >= maxRetries) break;
56
+
57
+ if (attempt < maxRetries) {
58
+ await sleep(retryDelay);
59
+ }
60
+ }
61
+
62
+ result = result!;
63
+
64
+ state.status = result.status;
65
+ state.completedAt = new Date().toISOString();
66
+ state.output = result.output;
67
+ state.exitCode = result.exitCode;
68
+ state.error = result.error;
69
+
70
+ if (result.variables) {
71
+ for (const [key, value] of Object.entries(result.variables)) {
72
+ context.variables.set(key, value);
73
+ }
74
+ }
75
+
76
+ emit({ type: 'node_done', nodeId: node.id, status: result.status, exitCode: result.exitCode });
77
+
78
+ return result;
79
+ }
80
+
81
+ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
82
+ return new Promise<T>((resolve, reject) => {
83
+ const timer = setTimeout(() => reject(new Error(`Node timed out after ${ms}ms`)), ms);
84
+ promise.then(
85
+ (val) => { clearTimeout(timer); resolve(val); },
86
+ (err) => { clearTimeout(timer); reject(err); },
87
+ );
88
+ });
89
+ }
90
+
91
+ function sleep(ms: number): Promise<void> {
92
+ return new Promise((resolve) => setTimeout(resolve, ms));
93
+ }
94
+
95
+ function failState(nodeId: string, error: string): NodeExecutionState {
96
+ return {
97
+ nodeId,
98
+ status: 'failed',
99
+ startedAt: new Date().toISOString(),
100
+ completedAt: new Date().toISOString(),
101
+ error,
102
+ retryCount: 0,
103
+ };
104
+ }
@@ -0,0 +1,15 @@
1
+ import type { NodeType, ExecutorFunction } from '../types.js';
2
+
3
+ const registry = new Map<NodeType, ExecutorFunction>();
4
+
5
+ export function registerExecutor(type: NodeType, executor: ExecutorFunction): void {
6
+ registry.set(type, executor);
7
+ }
8
+
9
+ export function getExecutor(type: NodeType): ExecutorFunction | undefined {
10
+ return registry.get(type);
11
+ }
12
+
13
+ export function getRegisteredTypes(): NodeType[] {
14
+ return [...registry.keys()];
15
+ }
@@ -0,0 +1,109 @@
1
+ import type { ExecutionContext } from '../types.js';
2
+
3
+ const PLACEHOLDER_RE = /\{\{\s*([\w.[\]]+)\s*\}\}/g;
4
+
5
+ export class VariableResolver {
6
+ resolve(template: string, context: ExecutionContext): string {
7
+ return template.replace(PLACEHOLDER_RE, (_match, path: string) => {
8
+ const value = this.resolvePath(path, context);
9
+ return value === undefined ? '' : String(value);
10
+ });
11
+ }
12
+
13
+ resolveObject<T>(obj: T, context: ExecutionContext): T {
14
+ return this.deepResolve(structuredClone(obj), context) as T;
15
+ }
16
+
17
+ private deepResolve(value: unknown, context: ExecutionContext): unknown {
18
+ if (typeof value === 'string') {
19
+ return this.resolve(value, context);
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return value.map((item) => this.deepResolve(item, context));
23
+ }
24
+ if (value !== null && typeof value === 'object') {
25
+ const result: Record<string, unknown> = {};
26
+ for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
27
+ result[key] = this.deepResolve(val, context);
28
+ }
29
+ return result;
30
+ }
31
+ return value;
32
+ }
33
+
34
+ private resolvePath(path: string, context: ExecutionContext): unknown {
35
+ const segments = path.split('.');
36
+
37
+ switch (segments[0]) {
38
+ case 'steps':
39
+ return this.resolveSteps(segments.slice(1), context);
40
+ case 'project':
41
+ return this.resolveProject(segments.slice(1), context);
42
+ case 'loop':
43
+ return this.resolveLoop(segments.slice(1), context);
44
+ case 'trigger':
45
+ return this.resolveTrigger(segments.slice(1), context);
46
+ case 'env':
47
+ return segments[1] ? process.env[segments[1]] : undefined;
48
+ case 'variables':
49
+ return segments[1] ? context.variables.get(segments[1]) : undefined;
50
+ default:
51
+ return undefined;
52
+ }
53
+ }
54
+
55
+ private resolveSteps(segments: string[], context: ExecutionContext): unknown {
56
+ if (segments.length < 2) return undefined;
57
+ const nodeId = segments[0];
58
+ const state = context.nodeStates.get(nodeId);
59
+ if (!state) return undefined;
60
+
61
+ const field = segments[1];
62
+ switch (field) {
63
+ case 'output':
64
+ return state.output;
65
+ case 'exitCode':
66
+ return state.exitCode;
67
+ case 'status':
68
+ return state.status;
69
+ case 'error':
70
+ return state.error;
71
+ default:
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ private resolveProject(segments: string[], context: ExecutionContext): unknown {
77
+ if (!context.project || segments.length === 0) return undefined;
78
+ switch (segments[0]) {
79
+ case 'name':
80
+ return context.project.name;
81
+ case 'path':
82
+ return context.project.path;
83
+ case 'id':
84
+ return context.project.id;
85
+ default:
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ private resolveLoop(segments: string[], context: ExecutionContext): unknown {
91
+ if (!context.loopContext || segments.length === 0) return undefined;
92
+ switch (segments[0]) {
93
+ case 'index':
94
+ return context.loopContext.index;
95
+ case 'item':
96
+ return context.loopContext.item;
97
+ default:
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ private resolveTrigger(segments: string[], context: ExecutionContext): unknown {
103
+ if (segments.length === 0) return undefined;
104
+ if (segments[0] === 'payload') {
105
+ return context.variables.get('__triggerPayload');
106
+ }
107
+ return undefined;
108
+ }
109
+ }