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,154 @@
1
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, KanbanConfig } from '../../types.js';
2
+ import type Database from 'better-sqlite3';
3
+ import { getDb, generateId, nowIso, appendVersionedEntry } from '../../../../apps/kanban/src/db.js';
4
+
5
+ export const kanbanExecutor: ExecutorFunction = async (
6
+ config: NodeConfig,
7
+ _context: ExecutionContext,
8
+ _emit: SSEEmitter,
9
+ ): Promise<ExecutorResult> => {
10
+ const cfg = config as KanbanConfig;
11
+ const db = getDb(_context.project?.id);
12
+
13
+ try {
14
+ switch (cfg.operation) {
15
+ case 'create': {
16
+ if (!cfg.featureId || !cfg.title) {
17
+ return { status: 'failed', error: 'featureId and title are required for create' };
18
+ }
19
+
20
+ let columnId = cfg.columnName
21
+ ? resolveColumnId(db, cfg.featureId, cfg.columnName)
22
+ : null;
23
+
24
+ if (!columnId) {
25
+ columnId = resolveColumnId(db, cfg.featureId, 'Backlog');
26
+ }
27
+
28
+ if (!columnId) {
29
+ return { status: 'failed', error: `Column not found` };
30
+ }
31
+
32
+ const maxOrder = db.prepare(
33
+ `SELECT MAX("order") AS maxOrd FROM "Issue" WHERE "columnId" = ?`
34
+ ).get(columnId) as { maxOrd: number | null } | undefined;
35
+
36
+ const order = (maxOrder?.maxOrd ?? -1) + 1;
37
+ const id = generateId();
38
+ const now = nowIso();
39
+
40
+ db.prepare(
41
+ `INSERT INTO "Issue" ("id", "title", "description", "type", "priority", "order", "labels", "projectId", "columnId", "updatedAt")
42
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
43
+ ).run(id, cfg.title, cfg.description || '', 'TASK', 'MEDIUM', order, '[]', cfg.featureId, columnId, now);
44
+
45
+ const issue = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(id);
46
+ return { status: 'passed', output: issue };
47
+ }
48
+
49
+ case 'move': {
50
+ if (!cfg.itemId || !cfg.columnName) {
51
+ return { status: 'failed', error: 'itemId and columnName are required for move' };
52
+ }
53
+
54
+ const item = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(cfg.itemId) as { projectId: string } | undefined;
55
+ if (!item) {
56
+ return { status: 'failed', error: `Issue ${cfg.itemId} not found` };
57
+ }
58
+
59
+ const columnId = resolveColumnId(db, item.projectId, cfg.columnName);
60
+ if (!columnId) {
61
+ return { status: 'failed', error: `Column "${cfg.columnName}" not found` };
62
+ }
63
+
64
+ const maxOrder = db.prepare(
65
+ `SELECT MAX("order") AS maxOrd FROM "Issue" WHERE "columnId" = ?`
66
+ ).get(columnId) as { maxOrd: number | null } | undefined;
67
+
68
+ const order = (maxOrder?.maxOrd ?? -1) + 1;
69
+
70
+ db.prepare(
71
+ `UPDATE "Issue" SET "columnId" = ?, "order" = ?, "updatedAt" = ? WHERE "id" = ?`
72
+ ).run(columnId, order, nowIso(), cfg.itemId);
73
+
74
+ const updated = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(cfg.itemId);
75
+ return { status: 'passed', output: updated };
76
+ }
77
+
78
+ case 'update': {
79
+ if (!cfg.itemId) {
80
+ return { status: 'failed', error: 'itemId is required for update' };
81
+ }
82
+
83
+ const sets: string[] = [];
84
+ const params: unknown[] = [];
85
+
86
+ if (cfg.title) { sets.push(`"title" = ?`); params.push(cfg.title); }
87
+ if (cfg.description !== undefined) { sets.push(`"description" = ?`); params.push(cfg.description); }
88
+
89
+ if (sets.length === 0) {
90
+ return { status: 'failed', error: 'No fields to update' };
91
+ }
92
+
93
+ sets.push(`"updatedAt" = ?`);
94
+ params.push(nowIso());
95
+ params.push(cfg.itemId);
96
+
97
+ db.prepare(`UPDATE "Issue" SET ${sets.join(', ')} WHERE "id" = ?`).run(...params);
98
+
99
+ const updated = db.prepare(`SELECT * FROM "Issue" WHERE "id" = ?`).get(cfg.itemId);
100
+ return { status: 'passed', output: updated };
101
+ }
102
+
103
+ case 'append-work-log': {
104
+ if (!cfg.itemId || !cfg.content) {
105
+ return { status: 'failed', error: 'itemId and content are required for append-work-log' };
106
+ }
107
+ const entry = appendVersionedEntry(db, cfg.itemId, 'work_log', cfg.content);
108
+ return { status: 'passed', output: entry };
109
+ }
110
+
111
+ case 'append-review': {
112
+ if (!cfg.itemId || !cfg.content) {
113
+ return { status: 'failed', error: 'itemId and content are required for append-review' };
114
+ }
115
+ const entry = appendVersionedEntry(db, cfg.itemId, 'review', cfg.content);
116
+ return { status: 'passed', output: entry };
117
+ }
118
+
119
+ case 'list': {
120
+ let query = `SELECT * FROM "Issue" WHERE 1=1`;
121
+ const params: unknown[] = [];
122
+
123
+ if (cfg.featureId) {
124
+ query += ` AND "projectId" = ?`;
125
+ params.push(cfg.featureId);
126
+ }
127
+
128
+ if (cfg.columnName && cfg.featureId) {
129
+ const columnId = resolveColumnId(db, cfg.featureId, cfg.columnName);
130
+ if (columnId) {
131
+ query += ` AND "columnId" = ?`;
132
+ params.push(columnId);
133
+ }
134
+ }
135
+
136
+ query += ` ORDER BY "order" ASC`;
137
+ const issues = db.prepare(query).all(...params);
138
+ return { status: 'passed', output: issues };
139
+ }
140
+
141
+ default:
142
+ return { status: 'failed', error: `Unknown kanban operation: ${(cfg as KanbanConfig).operation}` };
143
+ }
144
+ } catch (err) {
145
+ return { status: 'failed', error: (err as Error).message };
146
+ }
147
+ };
148
+
149
+ function resolveColumnId(db: Database.Database, featureId: string, columnName: string): string | null {
150
+ const col = db.prepare(
151
+ `SELECT "id" FROM "Column" WHERE "projectId" = ? AND "name" = ?`
152
+ ).get(featureId, columnName) as { id: string } | undefined;
153
+ return col?.id ?? null;
154
+ }
@@ -0,0 +1,46 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import OpenAI from 'openai';
4
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, LlmConfig } from '../../types.js';
5
+
6
+ export const llmExecutor: ExecutorFunction = async (
7
+ config: NodeConfig,
8
+ _context: ExecutionContext,
9
+ _emit: SSEEmitter,
10
+ ): Promise<ExecutorResult> => {
11
+ const cfg = config as LlmConfig;
12
+
13
+ try {
14
+ let prompt: string;
15
+
16
+ if (cfg.promptSource === 'file') {
17
+ if (!cfg.promptFile) {
18
+ return { status: 'failed', error: 'promptFile is required when promptSource is file' };
19
+ }
20
+ const base = _context.project?.path ?? process.cwd();
21
+ const filePath = path.resolve(base, cfg.promptFile.replace(/^\/+/, ''));
22
+ if (!filePath.startsWith(base + path.sep) && filePath !== base) {
23
+ return { status: 'failed', error: 'Path traversal denied' };
24
+ }
25
+ prompt = await fs.readFile(filePath, 'utf-8');
26
+ } else {
27
+ if (!cfg.prompt) {
28
+ return { status: 'failed', error: 'prompt is required when promptSource is inline' };
29
+ }
30
+ prompt = cfg.prompt;
31
+ }
32
+
33
+ const client = new OpenAI();
34
+ const response = await client.chat.completions.create({
35
+ model: cfg.model ?? 'gpt-4o-mini',
36
+ temperature: cfg.temperature ?? 0.7,
37
+ max_tokens: cfg.maxTokens,
38
+ messages: [{ role: 'user', content: prompt }],
39
+ });
40
+
41
+ const content = response.choices[0]?.message?.content ?? '';
42
+ return { status: 'passed', output: content };
43
+ } catch (err) {
44
+ return { status: 'failed', error: (err as Error).message };
45
+ }
46
+ };
@@ -0,0 +1,62 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, LogConfig } from '../../types.js';
4
+ import { LogWriter } from '../../../../apps/log/src/services/log-writer.js';
5
+
6
+ const writer = new LogWriter();
7
+
8
+ function resolveLogPath(projectPath: string | undefined, targetPath?: string): string {
9
+ const base = projectPath ?? process.cwd();
10
+ if (targetPath) return path.resolve(base, targetPath);
11
+ return path.join(base, '.devglide', 'logs', 'workflow.jsonl');
12
+ }
13
+
14
+ export const logExecutor: ExecutorFunction = async (
15
+ config: NodeConfig,
16
+ _context: ExecutionContext,
17
+ _emit: SSEEmitter,
18
+ ): Promise<ExecutorResult> => {
19
+ const cfg = config as LogConfig;
20
+
21
+ try {
22
+ switch (cfg.operation) {
23
+ case 'write': {
24
+ if (!cfg.message) {
25
+ return { status: 'failed', error: 'message is required for write' };
26
+ }
27
+ const targetPath = resolveLogPath(_context.project?.path, cfg.targetPath);
28
+ await writer.append(targetPath, {
29
+ type: cfg.type || 'WORKFLOW',
30
+ ts: new Date().toISOString(),
31
+ message: cfg.message,
32
+ source: 'workflow',
33
+ });
34
+ return { status: 'passed', output: `Logged to ${targetPath}` };
35
+ }
36
+
37
+ case 'read': {
38
+ const targetPath = resolveLogPath(_context.project?.path, cfg.targetPath);
39
+ const lines = cfg.lines ?? 50;
40
+ try {
41
+ const content = await fs.readFile(targetPath, 'utf-8');
42
+ const allLines = content.split('\n').filter(Boolean);
43
+ const tail = allLines.slice(-lines);
44
+ return { status: 'passed', output: tail.join('\n') };
45
+ } catch {
46
+ return { status: 'passed', output: '' };
47
+ }
48
+ }
49
+
50
+ case 'clear': {
51
+ const targetPath = resolveLogPath(_context.project?.path, cfg.targetPath);
52
+ await writer.clear(targetPath);
53
+ return { status: 'passed', output: `Cleared ${targetPath}` };
54
+ }
55
+
56
+ default:
57
+ return { status: 'failed', error: `Unknown log operation: ${(cfg as LogConfig).operation}` };
58
+ }
59
+ } catch (err) {
60
+ return { status: 'failed', error: (err as Error).message };
61
+ }
62
+ };
@@ -0,0 +1,14 @@
1
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter } from '../../types.js';
2
+
3
+ /**
4
+ * Loop executor is a pass-through — all loop iteration logic is owned by
5
+ * handleLoop() in graph-runner.ts which has access to the graph structure.
6
+ * The executor simply signals readiness so the graph-runner can take over.
7
+ */
8
+ export const loopExecutor: ExecutorFunction = async (
9
+ _config: NodeConfig,
10
+ _context: ExecutionContext,
11
+ _emit: SSEEmitter,
12
+ ): Promise<ExecutorResult> => {
13
+ return { status: 'passed' };
14
+ };
@@ -0,0 +1,107 @@
1
+ import { spawn } from 'child_process';
2
+ import path from 'path';
3
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, ShellConfig } from '../../types.js';
4
+
5
+ const DEFAULT_TIMEOUT = 5 * 60 * 1000;
6
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; // 10 MB
7
+
8
+ const ENV_DENYLIST = new Set([
9
+ 'LD_PRELOAD',
10
+ 'LD_LIBRARY_PATH',
11
+ 'DYLD_INSERT_LIBRARIES',
12
+ 'DYLD_LIBRARY_PATH',
13
+ 'NODE_OPTIONS',
14
+ 'ELECTRON_RUN_AS_NODE',
15
+ ]);
16
+
17
+ export const shellExecutor: ExecutorFunction = async (
18
+ config: NodeConfig,
19
+ context: ExecutionContext,
20
+ emit: SSEEmitter,
21
+ ): Promise<ExecutorResult> => {
22
+ const cfg = config as ShellConfig;
23
+
24
+ try {
25
+ const basePath = context.project?.path ?? process.cwd();
26
+ const cwd = cfg.cwd ? path.resolve(basePath, cfg.cwd) : basePath;
27
+
28
+ const env = { ...process.env };
29
+ if (cfg.env) {
30
+ for (const [key, value] of Object.entries(cfg.env)) {
31
+ if (!ENV_DENYLIST.has(key.toUpperCase())) {
32
+ env[key] = value;
33
+ }
34
+ }
35
+ }
36
+
37
+ const ac = new AbortController();
38
+
39
+ const result = await new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve, reject) => {
40
+ const child = spawn(cfg.command, {
41
+ shell: true,
42
+ cwd,
43
+ env,
44
+ signal: ac.signal,
45
+ });
46
+
47
+ const timer = setTimeout(() => {
48
+ ac.abort();
49
+ }, DEFAULT_TIMEOUT);
50
+
51
+ let stdout = '';
52
+ let stderr = '';
53
+ let totalBytes = 0;
54
+
55
+ child.stdout?.on('data', (data: Buffer) => {
56
+ totalBytes += data.length;
57
+ if (totalBytes > MAX_OUTPUT_BYTES) {
58
+ ac.abort();
59
+ return;
60
+ }
61
+ const chunk = data.toString();
62
+ stdout += chunk;
63
+ emit({ type: 'output', nodeId: context.runId, data: chunk });
64
+ });
65
+
66
+ child.stderr?.on('data', (data: Buffer) => {
67
+ totalBytes += data.length;
68
+ if (totalBytes > MAX_OUTPUT_BYTES) {
69
+ ac.abort();
70
+ return;
71
+ }
72
+ const chunk = data.toString();
73
+ stderr += chunk;
74
+ emit({ type: 'output', nodeId: context.runId, data: chunk });
75
+ });
76
+
77
+ child.on('error', (err) => {
78
+ clearTimeout(timer);
79
+ if (err.name === 'AbortError') {
80
+ resolve({ stdout, stderr, exitCode: 137 });
81
+ } else {
82
+ reject(err);
83
+ }
84
+ });
85
+
86
+ child.on('close', (code) => {
87
+ clearTimeout(timer);
88
+ resolve({ stdout, stderr, exitCode: code ?? 1 });
89
+ });
90
+ });
91
+
92
+ const output = (result.stdout + result.stderr).trim();
93
+ const variables: Record<string, unknown> = {};
94
+ if (cfg.captureOutput && cfg.outputVariable) {
95
+ variables[cfg.outputVariable] = output;
96
+ }
97
+
98
+ return {
99
+ status: result.exitCode === 0 ? 'passed' : 'failed',
100
+ output,
101
+ exitCode: result.exitCode,
102
+ variables: Object.keys(variables).length > 0 ? variables : undefined,
103
+ };
104
+ } catch (err) {
105
+ return { status: 'failed', error: (err as Error).message };
106
+ }
107
+ };
@@ -0,0 +1,61 @@
1
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, SubWorkflowConfig, WorkflowEvent } from '../../types.js';
2
+ import { runWorkflow } from '../graph-runner.js';
3
+
4
+ export const subWorkflowExecutor: ExecutorFunction = async (
5
+ config: NodeConfig,
6
+ context: ExecutionContext,
7
+ emit: SSEEmitter,
8
+ ): Promise<ExecutorResult> => {
9
+ const cfg = config as SubWorkflowConfig;
10
+
11
+ try {
12
+ if (!cfg.workflowId) {
13
+ return { status: 'failed', error: 'workflowId is required' };
14
+ }
15
+
16
+ if (!context.services.workflow) {
17
+ return { status: 'failed', error: 'Workflow services not available — ensure workflow module is initialized' };
18
+ }
19
+
20
+ const workflow = await context.services.workflow.getWorkflow(cfg.workflowId);
21
+ if (!workflow) {
22
+ return { status: 'failed', error: `Workflow ${cfg.workflowId} not found` };
23
+ }
24
+
25
+ const childVariables = new Map<string, unknown>(context.variables);
26
+ if (cfg.inputMappings) {
27
+ for (const [childKey, parentKey] of Object.entries(cfg.inputMappings)) {
28
+ childVariables.set(childKey, context.variables.get(parentKey));
29
+ }
30
+ }
31
+
32
+ const childEmit: SSEEmitter = (event: WorkflowEvent) => {
33
+ emit(event);
34
+ };
35
+
36
+ const result = await runWorkflow(workflow, childEmit, undefined, childVariables, context.services);
37
+
38
+ const outputVars: Record<string, unknown> = {};
39
+ if (cfg.outputMappings && result.variables) {
40
+ for (const [parentKey, childKey] of Object.entries(cfg.outputMappings)) {
41
+ if (result.variables.has(childKey)) {
42
+ outputVars[parentKey] = result.variables.get(childKey);
43
+ }
44
+ }
45
+ }
46
+
47
+ // Extract error from first failed node state (if any)
48
+ let error: string | undefined;
49
+ for (const ns of result.nodeStates.values()) {
50
+ if (ns.status === 'failed' && ns.error) { error = ns.error; break; }
51
+ }
52
+
53
+ return {
54
+ status: result.status === 'passed' ? 'passed' : 'failed',
55
+ variables: Object.keys(outputVars).length > 0 ? outputVars : undefined,
56
+ error,
57
+ };
58
+ } catch (err: unknown) {
59
+ return { status: 'failed', error: err instanceof Error ? err.message : String(err) };
60
+ }
61
+ };
@@ -0,0 +1,73 @@
1
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, TestConfig } from '../../types.js';
2
+
3
+ export const testExecutor: ExecutorFunction = async (
4
+ config: NodeConfig,
5
+ context: ExecutionContext,
6
+ _emit: SSEEmitter,
7
+ ): Promise<ExecutorResult> => {
8
+ const cfg = config as TestConfig;
9
+ const testService = context.services.test;
10
+
11
+ if (!testService) {
12
+ return { status: 'failed', error: 'Test services not available — ensure test module is initialized' };
13
+ }
14
+
15
+ try {
16
+ switch (cfg.operation) {
17
+ case 'run-scenario': {
18
+ if (!cfg.steps || cfg.steps.length === 0) {
19
+ return { status: 'failed', error: 'steps are required for run-scenario' };
20
+ }
21
+ const scenario = testService.submitScenario({
22
+ name: 'workflow-test',
23
+ steps: cfg.steps,
24
+ target: cfg.target,
25
+ });
26
+ return { status: 'passed', output: scenario };
27
+ }
28
+
29
+ case 'run-saved': {
30
+ if (!cfg.scenarioId) {
31
+ return { status: 'failed', error: 'scenarioId is required for run-saved' };
32
+ }
33
+ const saved = await testService.getSavedScenario(cfg.scenarioId);
34
+ if (!saved) {
35
+ return { status: 'failed', error: `Scenario ${cfg.scenarioId} not found` };
36
+ }
37
+ const scenario = testService.submitScenario({
38
+ name: saved.name,
39
+ steps: saved.steps,
40
+ target: saved.target,
41
+ });
42
+ await testService.markRun(cfg.scenarioId);
43
+ return { status: 'passed', output: scenario };
44
+ }
45
+
46
+ case 'save-scenario': {
47
+ if (!cfg.steps || !cfg.target) {
48
+ return { status: 'failed', error: 'steps and target are required for save-scenario' };
49
+ }
50
+ const description = cfg.linkedItemId
51
+ ? `Linked to kanban item: ${cfg.linkedItemId}`
52
+ : undefined;
53
+ const saved = await testService.saveScenario({
54
+ name: 'workflow-test',
55
+ description,
56
+ target: cfg.target,
57
+ steps: cfg.steps,
58
+ });
59
+ return { status: 'passed', output: saved };
60
+ }
61
+
62
+ case 'list-saved': {
63
+ const list = await testService.listSaved();
64
+ return { status: 'passed', output: list };
65
+ }
66
+
67
+ default:
68
+ return { status: 'failed', error: `Unknown test operation: ${(cfg as TestConfig).operation}` };
69
+ }
70
+ } catch (err: unknown) {
71
+ return { status: 'failed', error: err instanceof Error ? err.message : String(err) };
72
+ }
73
+ };
@@ -0,0 +1,39 @@
1
+ import type { ExecutorFunction, ExecutorResult, NodeConfig, ExecutionContext, SSEEmitter, TriggerConfig } from '../../types.js';
2
+
3
+ export const triggerExecutor: ExecutorFunction = async (
4
+ config: NodeConfig,
5
+ context: ExecutionContext,
6
+ _emit: SSEEmitter,
7
+ ): Promise<ExecutorResult> => {
8
+ const cfg = config as TriggerConfig;
9
+ const payload = context.variables.get('__triggerPayload');
10
+
11
+ switch (cfg.triggerType) {
12
+ case 'manual':
13
+ return { status: 'passed', output: 'Manual trigger' };
14
+
15
+ case 'schedule':
16
+ return { status: 'passed', output: payload ?? 'Scheduled trigger' };
17
+
18
+ case 'git-event':
19
+ return { status: 'passed', output: payload ?? 'Git event trigger' };
20
+
21
+ case 'log-pattern':
22
+ return { status: 'passed', output: payload ?? 'Log pattern trigger' };
23
+
24
+ case 'kanban-move':
25
+ return { status: 'passed', output: payload ?? 'Kanban move trigger' };
26
+
27
+ case 'webhook':
28
+ return { status: 'passed', output: payload ?? 'Webhook trigger' };
29
+
30
+ case 'prompt':
31
+ return { status: 'passed', output: payload ?? 'Prompt trigger' };
32
+
33
+ case 'voice':
34
+ return { status: 'passed', output: payload ?? 'Voice trigger' };
35
+
36
+ default:
37
+ return { status: 'passed', output: payload ?? 'Unknown trigger' };
38
+ }
39
+ };
@@ -0,0 +1,117 @@
1
+ import type { ExecutionContext } from '../types.js';
2
+ import { VariableResolver } from './variable-resolver.js';
3
+
4
+ const resolver = new VariableResolver();
5
+
6
+ export function evaluate(expression: string, context: ExecutionContext): boolean {
7
+ const resolved = resolver.resolve(expression, context);
8
+ return evaluateExpression(resolved.trim());
9
+ }
10
+
11
+ function evaluateExpression(expr: string): boolean {
12
+ const orParts = splitOutsideQuotes(expr, '||');
13
+ if (orParts.length > 1) {
14
+ return orParts.some((part) => evaluateExpression(part.trim()));
15
+ }
16
+
17
+ const andParts = splitOutsideQuotes(expr, '&&');
18
+ if (andParts.length > 1) {
19
+ return andParts.every((part) => evaluateExpression(part.trim()));
20
+ }
21
+
22
+ if (expr.startsWith('!') && !expr.startsWith('!=')) {
23
+ return !evaluateExpression(expr.slice(1).trim());
24
+ }
25
+
26
+ return evaluateComparison(expr);
27
+ }
28
+
29
+ function evaluateComparison(expr: string): boolean {
30
+ const operators = ['!=', '==', '>=', '<=', '>', '<', ' contains ', ' matches '] as const;
31
+
32
+ for (const op of operators) {
33
+ const idx = expr.indexOf(op);
34
+ if (idx === -1) continue;
35
+
36
+ const left = expr.slice(0, idx).trim();
37
+ const right = expr.slice(idx + op.length).trim();
38
+ const leftVal = parseValue(left);
39
+ const rightVal = parseValue(right);
40
+
41
+ switch (op.trim()) {
42
+ case '==':
43
+ return leftVal === rightVal;
44
+ case '!=':
45
+ return leftVal !== rightVal;
46
+ case '>':
47
+ return Number(leftVal) > Number(rightVal);
48
+ case '<':
49
+ return Number(leftVal) < Number(rightVal);
50
+ case '>=':
51
+ return Number(leftVal) >= Number(rightVal);
52
+ case '<=':
53
+ return Number(leftVal) <= Number(rightVal);
54
+ case 'contains':
55
+ return String(leftVal).includes(String(rightVal));
56
+ case 'matches': {
57
+ try {
58
+ const pattern = String(rightVal);
59
+ if (pattern.length > 200) return false;
60
+ const regex = new RegExp(pattern);
61
+ return regex.test(String(leftVal));
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ const val = parseValue(expr);
70
+ return Boolean(val) && val !== '0' && val !== 'false' && val !== '';
71
+ }
72
+
73
+ function parseValue(raw: string): string | number {
74
+ const trimmed = raw.trim();
75
+
76
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
77
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
78
+ return trimmed.slice(1, -1);
79
+ }
80
+
81
+ const num = Number(trimmed);
82
+ if (!Number.isNaN(num) && trimmed !== '') {
83
+ return num;
84
+ }
85
+
86
+ return trimmed;
87
+ }
88
+
89
+ function splitOutsideQuotes(str: string, delimiter: string): string[] {
90
+ const parts: string[] = [];
91
+ let current = '';
92
+ let inSingle = false;
93
+ let inDouble = false;
94
+ let i = 0;
95
+
96
+ while (i < str.length) {
97
+ if (str[i] === '"' && !inSingle) {
98
+ inDouble = !inDouble;
99
+ current += str[i];
100
+ i++;
101
+ } else if (str[i] === "'" && !inDouble) {
102
+ inSingle = !inSingle;
103
+ current += str[i];
104
+ i++;
105
+ } else if (!inSingle && !inDouble && str.slice(i, i + delimiter.length) === delimiter) {
106
+ parts.push(current);
107
+ current = '';
108
+ i += delimiter.length;
109
+ } else {
110
+ current += str[i];
111
+ i++;
112
+ }
113
+ }
114
+
115
+ parts.push(current);
116
+ return parts;
117
+ }