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,424 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { z } from 'zod';
4
+ import type { Workflow } from '../types.js';
5
+ import { getActiveProject } from '../../../project-context.js';
6
+ import { WORKFLOWS_DIR, INSTRUCTIONS_DIR } from '../../../packages/paths.js';
7
+ import { JsonFileStore } from '../../../packages/json-file-store.js';
8
+
9
+ // ── Zod schema for validating workflow JSON files from disk ──────────────────
10
+ const workflowFileSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ version: z.number(),
14
+ nodes: z.array(z.object({
15
+ id: z.string(),
16
+ type: z.string(),
17
+ label: z.string(),
18
+ config: z.record(z.unknown()),
19
+ position: z.object({ x: z.number(), y: z.number() }),
20
+ }).passthrough()),
21
+ edges: z.array(z.object({
22
+ id: z.string(),
23
+ source: z.string(),
24
+ target: z.string(),
25
+ }).passthrough()),
26
+ variables: z.array(z.object({
27
+ name: z.string(),
28
+ type: z.enum(['string', 'number', 'boolean', 'json']),
29
+ }).passthrough()).default([]),
30
+ tags: z.array(z.string()).default([]),
31
+ createdAt: z.string(),
32
+ updatedAt: z.string(),
33
+ description: z.string().optional(),
34
+ projectId: z.string().optional(),
35
+ enabled: z.boolean().optional(),
36
+ global: z.boolean().optional(),
37
+ }).passthrough();
38
+
39
+ interface WorkflowSummary {
40
+ id: string;
41
+ name: string;
42
+ description?: string;
43
+ version: number;
44
+ projectId?: string;
45
+ tags: string[];
46
+ nodeCount: number;
47
+ edgeCount: number;
48
+ updatedAt: string;
49
+ scope: 'project' | 'global';
50
+ enabled?: boolean;
51
+ global?: boolean;
52
+ }
53
+
54
+ /**
55
+ * Per-project and global workflow storage.
56
+ * One JSON file per workflow for git-friendly diffs.
57
+ */
58
+ export class WorkflowStore extends JsonFileStore<Workflow> {
59
+ private static instance: WorkflowStore;
60
+ protected readonly baseDir = WORKFLOWS_DIR;
61
+
62
+ static getInstance(): WorkflowStore {
63
+ if (!WorkflowStore.instance) {
64
+ WorkflowStore.instance = new WorkflowStore();
65
+ }
66
+ return WorkflowStore.instance;
67
+ }
68
+
69
+ /** Override to add Zod validation on disk reads. */
70
+ protected override async readEntityFile(filePath: string): Promise<Workflow | null> {
71
+ try {
72
+ const raw = await fs.readFile(filePath, 'utf-8');
73
+ const parsed = JSON.parse(raw);
74
+ const result = workflowFileSchema.safeParse(parsed);
75
+ if (!result.success) {
76
+ console.warn(`[workflow-store] Invalid workflow file ${filePath}: ${result.error.issues[0]?.message}`);
77
+ return null;
78
+ }
79
+ return result.data as unknown as Workflow;
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ async list(projectId?: string): Promise<WorkflowSummary[]> {
86
+ const seen = new Map<string, WorkflowSummary>();
87
+ const scopeId = projectId ?? getActiveProject()?.id;
88
+
89
+ // Project-scoped workflows take precedence over global
90
+ const projectDir = this.getProjectDir();
91
+ if (projectDir) {
92
+ for (const s of await this.scanDir(projectDir, 'project')) {
93
+ seen.set(s.id, s);
94
+ }
95
+ }
96
+
97
+ const globalDir = this.getGlobalDir();
98
+ for (const s of await this.scanDir(globalDir, 'global')) {
99
+ if (seen.has(s.id)) continue;
100
+ // In a project context, only include global-dir workflows that are either:
101
+ // - explicitly marked global, OR
102
+ // - belong to this project (projectId matches)
103
+ // Unscoped workflows (no projectId, not global) are legacy — skip in project context
104
+ if (scopeId && !s.global) {
105
+ if (!s.projectId || s.projectId !== scopeId) continue;
106
+ }
107
+ seen.set(s.id, s);
108
+ }
109
+
110
+ return [...seen.values()];
111
+ }
112
+
113
+ async listFull(projectId?: string): Promise<Workflow[]> {
114
+ const seen = new Map<string, Workflow>();
115
+ const scopeId = projectId ?? getActiveProject()?.id;
116
+
117
+ const projectDir = this.getProjectDir();
118
+ if (projectDir) {
119
+ for (const w of await this.scanDirFull(projectDir)) {
120
+ seen.set(w.id, w);
121
+ }
122
+ }
123
+
124
+ const globalDir = this.getGlobalDir();
125
+ for (const w of await this.scanDirFull(globalDir)) {
126
+ if (seen.has(w.id)) continue;
127
+ if (scopeId && !w.global) {
128
+ if (!w.projectId || w.projectId !== scopeId) continue;
129
+ }
130
+ seen.set(w.id, w);
131
+ }
132
+
133
+ return [...seen.values()];
134
+ }
135
+
136
+ async save(
137
+ input: Omit<Workflow, 'id' | 'createdAt' | 'updatedAt'> & { id?: string; scope?: 'project' | 'global'; enabled?: boolean },
138
+ ): Promise<Workflow> {
139
+ const lockKey = input.id ?? this.generateId();
140
+ return this.withLock(lockKey, async () => {
141
+ const now = new Date().toISOString();
142
+ const isUpdate = !!input.id;
143
+
144
+ let existing: Workflow | null = null;
145
+ let oldScope: 'project' | 'global' | undefined;
146
+ if (isUpdate) {
147
+ existing = await this.get(input.id!);
148
+ oldScope = await this.resolveExistingScope(input.id!);
149
+ }
150
+
151
+ const workflow: Workflow = {
152
+ id: input.id ?? lockKey,
153
+ name: input.name,
154
+ description: input.description,
155
+ version: input.version,
156
+ projectId: input.projectId ?? existing?.projectId ?? getActiveProject()?.id,
157
+ tags: input.tags,
158
+ enabled: input.enabled,
159
+ global: input.global,
160
+ nodes: input.nodes,
161
+ edges: input.edges,
162
+ variables: input.variables,
163
+ createdAt: existing?.createdAt ?? now,
164
+ updatedAt: now,
165
+ };
166
+
167
+ // When updating, preserve the original storage location; only new workflows use default scope
168
+ let scope = input.scope;
169
+ if (input.global === true) {
170
+ scope = 'global';
171
+ } else if (input.global === false && workflow.projectId) {
172
+ scope = 'project';
173
+ } else if (!scope && isUpdate) {
174
+ scope = oldScope;
175
+ }
176
+ scope = scope ?? (getActiveProject() ? 'project' : 'global');
177
+
178
+ await this.writeEntity(workflow, scope, workflow.projectId);
179
+
180
+ // If scope changed during update, clean up old file location
181
+ if (isUpdate && oldScope && oldScope !== scope) {
182
+ await this.removeFromScope(workflow.id, oldScope, existing?.projectId ?? getActiveProject()?.id);
183
+ }
184
+
185
+ await this.generateInstructionsFile(workflow.projectId);
186
+
187
+ return workflow;
188
+ });
189
+ }
190
+
191
+ override async delete(id: string): Promise<boolean> {
192
+ return this.withLock(id, async () => {
193
+ const existing = await this.get(id);
194
+ const projectDir = this.getProjectDir();
195
+ if (projectDir) {
196
+ try {
197
+ await fs.unlink(path.join(projectDir, `${id}.json`));
198
+ await this.generateInstructionsFile(existing?.projectId);
199
+ return true;
200
+ } catch {
201
+ // Not in project dir, try global
202
+ }
203
+ }
204
+
205
+ try {
206
+ await fs.unlink(path.join(this.getGlobalDir(), `${id}.json`));
207
+ await this.generateInstructionsFile(existing?.projectId);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ });
213
+ }
214
+
215
+ async exists(id: string): Promise<boolean> {
216
+ return (await this.get(id)) !== null;
217
+ }
218
+
219
+ async generateInstructionsFile(projectId?: string): Promise<void> {
220
+ const markdown = await this.buildInstructionsMarkdown(projectId);
221
+ const dirPath = INSTRUCTIONS_DIR;
222
+ await this.ensureDir(dirPath);
223
+ const filename = projectId || '_global';
224
+ await fs.writeFile(path.join(dirPath, `${filename}.md`), markdown);
225
+ }
226
+
227
+ async getCompiledInstructions(projectId?: string): Promise<string> {
228
+ return this.buildInstructionsMarkdown(projectId);
229
+ }
230
+
231
+ async match(prompt: string, projectId?: string): Promise<{ matches: Array<{ id: string; name: string; description?: string; tags: string[]; score: number; instructions: string }> }> {
232
+ const tokens = this.tokenize(prompt);
233
+ if (tokens.length === 0) return { matches: [] };
234
+
235
+ const workflows = await this.listFull(projectId);
236
+ const scored: Array<{ id: string; name: string; description?: string; tags: string[]; score: number; workflow: Workflow }> = [];
237
+
238
+ for (const w of workflows) {
239
+ if (w.enabled === false) continue;
240
+
241
+ const corpus = this.buildSearchCorpus(w);
242
+ const score = this.scoreMatch(tokens, corpus);
243
+ if (score > 0) {
244
+ scored.push({ id: w.id, name: w.name, description: w.description, tags: w.tags, score, workflow: w });
245
+ }
246
+ }
247
+
248
+ scored.sort((a, b) => b.score - a.score);
249
+
250
+ const matches = scored.map(({ id, name, description, tags, score, workflow }) => ({
251
+ id,
252
+ name,
253
+ description,
254
+ tags,
255
+ score,
256
+ instructions: this.buildSingleWorkflowMarkdown(workflow),
257
+ }));
258
+
259
+ return { matches };
260
+ }
261
+
262
+ private tokenize(text: string): string[] {
263
+ return text
264
+ .toLowerCase()
265
+ .replace(/[^a-z0-9\s-]/g, ' ')
266
+ .split(/\s+/)
267
+ .filter((t) => t.length > 2);
268
+ }
269
+
270
+ private buildSearchCorpus(w: Workflow): string {
271
+ const parts: string[] = [
272
+ w.name,
273
+ w.description ?? '',
274
+ ...w.tags,
275
+ ];
276
+
277
+ for (const node of w.nodes) {
278
+ parts.push(node.label);
279
+ const config = node.config as any;
280
+ if (config.triggerType) parts.push(config.triggerType);
281
+ if (config.gitEvent) parts.push(config.gitEvent);
282
+ if (config.operation) parts.push(config.operation);
283
+ if (config.instructions) parts.push(config.instructions);
284
+ if (config.command) parts.push(config.command);
285
+ }
286
+
287
+ return parts.join(' ').toLowerCase();
288
+ }
289
+
290
+ private scoreMatch(tokens: string[], corpus: string): number {
291
+ let score = 0;
292
+ for (const token of tokens) {
293
+ if (corpus.includes(token)) score++;
294
+ }
295
+ return score;
296
+ }
297
+
298
+ private buildSingleWorkflowMarkdown(workflow: Workflow): string {
299
+ const lines: string[] = [`## ${workflow.name}`];
300
+ if (workflow.description) lines.push('', workflow.description);
301
+
302
+ const steps = this.compileWorkflowSteps(workflow);
303
+ for (const step of steps) {
304
+ lines.push('', `### Step ${step.stepNumber}: ${step.label}`);
305
+ if (step.instructions) lines.push(step.instructions);
306
+ else if (step.instructionFile) lines.push(`See: ${step.instructionFile}`);
307
+ }
308
+
309
+ return lines.join('\n');
310
+ }
311
+
312
+ private async buildInstructionsMarkdown(projectId?: string): Promise<string> {
313
+ const workflows = await this.listFull(projectId);
314
+ const filtered = workflows.filter((w) => w.enabled !== false);
315
+
316
+ const lines: string[] = [
317
+ '# DevGlide Workflow Instructions',
318
+ '',
319
+ '> Auto-generated. Do not edit manually.',
320
+ `> Last updated: ${new Date().toISOString()}`,
321
+ ];
322
+
323
+ for (const workflow of filtered) {
324
+ lines.push('', `## ${workflow.name}`);
325
+ if (workflow.description) {
326
+ lines.push('', workflow.description);
327
+ }
328
+
329
+ const steps = this.compileWorkflowSteps(workflow);
330
+ for (const step of steps) {
331
+ lines.push('', `### Step ${step.stepNumber}: ${step.label}`);
332
+ if (step.instructions) {
333
+ lines.push(step.instructions);
334
+ } else if (step.instructionFile) {
335
+ lines.push(`See: ${step.instructionFile}`);
336
+ }
337
+ }
338
+
339
+ lines.push('', '---');
340
+ }
341
+
342
+ return lines.join('\n') + '\n';
343
+ }
344
+
345
+ private compileWorkflowSteps(workflow: Workflow): Array<{ stepNumber: number; label: string; instructions?: string; instructionFile?: string }> {
346
+ const adjacency = new Map<string, string[]>();
347
+ const incomingCount = new Map<string, number>();
348
+
349
+ for (const node of workflow.nodes) {
350
+ adjacency.set(node.id, []);
351
+ incomingCount.set(node.id, 0);
352
+ }
353
+
354
+ for (const edge of workflow.edges) {
355
+ adjacency.get(edge.source)?.push(edge.target);
356
+ incomingCount.set(edge.target, (incomingCount.get(edge.target) ?? 0) + 1);
357
+ }
358
+
359
+ // Start nodes: no incoming edges
360
+ const queue: string[] = [];
361
+ for (const node of workflow.nodes) {
362
+ if ((incomingCount.get(node.id) ?? 0) === 0) {
363
+ queue.push(node.id);
364
+ }
365
+ }
366
+
367
+ const ordered: string[] = [];
368
+
369
+ while (queue.length > 0) {
370
+ const nodeId = queue.shift()!;
371
+ ordered.push(nodeId);
372
+
373
+ for (const neighbor of adjacency.get(nodeId) ?? []) {
374
+ const count = (incomingCount.get(neighbor) ?? 1) - 1;
375
+ incomingCount.set(neighbor, count);
376
+ if (count === 0) {
377
+ queue.push(neighbor);
378
+ }
379
+ }
380
+ }
381
+
382
+ const nodeMap = new Map(workflow.nodes.map((n) => [n.id, n]));
383
+ const steps: Array<{ stepNumber: number; label: string; instructions?: string; instructionFile?: string }> = [];
384
+ let stepNumber = 1;
385
+
386
+ for (const nodeId of ordered) {
387
+ const node = nodeMap.get(nodeId);
388
+ if (!node) continue;
389
+ const config = node.config as any;
390
+ steps.push({
391
+ stepNumber: stepNumber++,
392
+ label: node.label,
393
+ instructions: config.instructions,
394
+ instructionFile: config.instructionFile,
395
+ });
396
+ }
397
+
398
+ return steps;
399
+ }
400
+
401
+ private async scanDir(dir: string, scope: 'project' | 'global'): Promise<WorkflowSummary[]> {
402
+ const summaries: WorkflowSummary[] = [];
403
+ const workflows = await this.scanDirFull(dir);
404
+
405
+ for (const workflow of workflows) {
406
+ summaries.push({
407
+ id: workflow.id,
408
+ name: workflow.name,
409
+ description: workflow.description,
410
+ version: workflow.version,
411
+ projectId: workflow.projectId,
412
+ tags: workflow.tags,
413
+ nodeCount: workflow.nodes.length,
414
+ edgeCount: workflow.edges.length,
415
+ updatedAt: workflow.updatedAt,
416
+ scope,
417
+ enabled: workflow.enabled,
418
+ global: workflow.global,
419
+ });
420
+ }
421
+
422
+ return summaries;
423
+ }
424
+ }
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import type { WorkflowNode, WorkflowEdge } from '../types.js';
3
+
4
+ // Mock the node registry before importing the validator
5
+ vi.mock('../engine/node-registry.js', () => ({
6
+ getRegisteredTypes: () => ['trigger', 'action:shell', 'action:kanban', 'decision', 'loop'],
7
+ }));
8
+
9
+ const { validateWorkflowGraph } = await import('./workflow-validator.js');
10
+
11
+ function makeNode(id: string, type: string, label?: string): WorkflowNode {
12
+ return { id, type: type as WorkflowNode['type'], label: label ?? id, config: {} as WorkflowNode['config'], position: { x: 0, y: 0 } };
13
+ }
14
+
15
+ function makeEdge(source: string, target: string): WorkflowEdge {
16
+ return { id: `${source}-${target}`, source, target };
17
+ }
18
+
19
+ describe('validateWorkflowGraph', () => {
20
+ it('passes a valid linear graph', () => {
21
+ const nodes = [
22
+ makeNode('t1', 'trigger'),
23
+ makeNode('a1', 'action:shell'),
24
+ ];
25
+ const edges = [makeEdge('t1', 'a1')];
26
+ const result = validateWorkflowGraph(nodes, edges);
27
+ expect(result.valid).toBe(true);
28
+ expect(result.errors).toHaveLength(0);
29
+ });
30
+
31
+ it('requires at least one trigger node', () => {
32
+ const nodes = [makeNode('a1', 'action:shell')];
33
+ const result = validateWorkflowGraph(nodes, []);
34
+ expect(result.valid).toBe(false);
35
+ expect(result.errors).toContain('Workflow must have at least one trigger node');
36
+ });
37
+
38
+ it('rejects unknown node types', () => {
39
+ const nodes = [
40
+ makeNode('t1', 'trigger'),
41
+ makeNode('x1', 'action:unknown', 'MyNode'),
42
+ ];
43
+ const edges = [makeEdge('t1', 'x1')];
44
+ const result = validateWorkflowGraph(nodes, edges);
45
+ expect(result.valid).toBe(false);
46
+ expect(result.errors[0]).toContain('unknown type');
47
+ });
48
+
49
+ it('detects dangling edge references', () => {
50
+ const nodes = [makeNode('t1', 'trigger')];
51
+ const edges = [makeEdge('t1', 'nonexistent')];
52
+ const result = validateWorkflowGraph(nodes, edges);
53
+ expect(result.valid).toBe(false);
54
+ expect(result.errors[0]).toContain('non-existent target');
55
+ });
56
+
57
+ it('detects disconnected nodes', () => {
58
+ const nodes = [
59
+ makeNode('t1', 'trigger'),
60
+ makeNode('a1', 'action:shell', 'Orphan'),
61
+ ];
62
+ const result = validateWorkflowGraph(nodes, []);
63
+ expect(result.valid).toBe(false);
64
+ expect(result.errors.some((e) => e.includes('disconnected'))).toBe(true);
65
+ });
66
+
67
+ it('detects cycles', () => {
68
+ const nodes = [
69
+ makeNode('t1', 'trigger'),
70
+ makeNode('a1', 'action:shell'),
71
+ makeNode('a2', 'action:shell'),
72
+ ];
73
+ const edges = [
74
+ makeEdge('t1', 'a1'),
75
+ makeEdge('a1', 'a2'),
76
+ makeEdge('a2', 'a1'), // cycle
77
+ ];
78
+ const result = validateWorkflowGraph(nodes, edges);
79
+ expect(result.valid).toBe(false);
80
+ expect(result.errors).toContain('Workflow graph contains a cycle');
81
+ });
82
+
83
+ it('detects decision nodes without ports', () => {
84
+ const nodes = [
85
+ makeNode('t1', 'trigger'),
86
+ makeNode('d1', 'decision', 'MyDecision'),
87
+ ];
88
+ const edges = [makeEdge('t1', 'd1')];
89
+ const result = validateWorkflowGraph(nodes, edges);
90
+ expect(result.valid).toBe(false);
91
+ expect(result.errors.some((e) => e.includes('must have at least one port'))).toBe(true);
92
+ });
93
+
94
+ it('allows sub-workflow nodes without registry check', () => {
95
+ const nodes = [
96
+ makeNode('t1', 'trigger'),
97
+ makeNode('sw1', 'sub-workflow'),
98
+ ];
99
+ const edges = [makeEdge('t1', 'sw1')];
100
+ const result = validateWorkflowGraph(nodes, edges);
101
+ expect(result.valid).toBe(true);
102
+ });
103
+ });
@@ -0,0 +1,98 @@
1
+ import type { WorkflowNode, WorkflowEdge } from '../types.js';
2
+ import { getRegisteredTypes } from '../engine/node-registry.js';
3
+
4
+ export interface ValidationResult {
5
+ valid: boolean;
6
+ errors: string[];
7
+ }
8
+
9
+ /**
10
+ * Validate a workflow graph for structural correctness:
11
+ * - Trigger node presence
12
+ * - Node type validity
13
+ * - Edge reference integrity
14
+ * - Decision node port requirements
15
+ * - Disconnected node detection
16
+ * - Cycle detection via DFS
17
+ */
18
+ export function validateWorkflowGraph(nodes: WorkflowNode[], edges: WorkflowEdge[]): ValidationResult {
19
+ const errors: string[] = [];
20
+ const nodeIds = new Set(nodes.map((n) => n.id));
21
+ const registeredTypes = new Set(getRegisteredTypes());
22
+
23
+ // Must have at least one trigger
24
+ const triggerNodes = nodes.filter((n) => n.type === 'trigger');
25
+ if (triggerNodes.length === 0) {
26
+ errors.push('Workflow must have at least one trigger node');
27
+ }
28
+
29
+ // Node type validation
30
+ for (const node of nodes) {
31
+ if (node.type !== 'trigger' && node.type !== 'sub-workflow' && !registeredTypes.has(node.type)) {
32
+ errors.push(`Node "${node.label}" (${node.id}) has unknown type "${node.type}"`);
33
+ }
34
+ }
35
+
36
+ // Edge reference integrity
37
+ for (const edge of edges) {
38
+ if (!nodeIds.has(edge.source)) {
39
+ errors.push(`Edge "${edge.id}" references non-existent source node "${edge.source}"`);
40
+ }
41
+ if (!nodeIds.has(edge.target)) {
42
+ errors.push(`Edge "${edge.id}" references non-existent target node "${edge.target}"`);
43
+ }
44
+ }
45
+
46
+ // Decision node port requirements
47
+ for (const node of nodes) {
48
+ if (node.type === 'decision') {
49
+ const config = node.config as any;
50
+ if (!config.ports || !Array.isArray(config.ports) || config.ports.length === 0) {
51
+ errors.push(`Decision node "${node.label}" (${node.id}) must have at least one port`);
52
+ }
53
+ }
54
+ }
55
+
56
+ // Disconnected node detection
57
+ for (const node of nodes) {
58
+ if (node.type === 'trigger') continue;
59
+ const hasIncoming = edges.some((e) => e.target === node.id);
60
+ const hasOutgoing = edges.some((e) => e.source === node.id);
61
+ if (!hasIncoming && !hasOutgoing) {
62
+ errors.push(`Node "${node.label}" (${node.id}) is disconnected from the graph`);
63
+ }
64
+ }
65
+
66
+ // Cycle detection via DFS (three-color algorithm)
67
+ const adjacency = new Map<string, string[]>();
68
+ for (const node of nodes) adjacency.set(node.id, []);
69
+ for (const edge of edges) adjacency.get(edge.source)?.push(edge.target);
70
+
71
+ const WHITE = 0, GRAY = 1, BLACK = 2;
72
+ const colors = new Map<string, number>();
73
+ for (const node of nodes) colors.set(node.id, WHITE);
74
+
75
+ let hasCycle = false;
76
+
77
+ function dfs(nodeId: string): void {
78
+ if (hasCycle) return;
79
+ colors.set(nodeId, GRAY);
80
+ for (const neighbor of adjacency.get(nodeId) ?? []) {
81
+ const color = colors.get(neighbor);
82
+ if (color === GRAY) { hasCycle = true; return; }
83
+ if (color === WHITE) dfs(neighbor);
84
+ }
85
+ colors.set(nodeId, BLACK);
86
+ }
87
+
88
+ for (const node of nodes) {
89
+ if (colors.get(node.id) === WHITE) {
90
+ dfs(node.id);
91
+ if (hasCycle) break;
92
+ }
93
+ }
94
+
95
+ if (hasCycle) errors.push('Workflow graph contains a cycle');
96
+
97
+ return { valid: errors.length === 0, errors };
98
+ }
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { createWorkflowMcpServer } from "../mcp.js";
3
+ import { runStdio } from "@devglide/mcp-utils";
4
+
5
+ // ── Stdio MCP mode ──────────────────────────────────────────────────────────
6
+ if (process.argv.includes("--stdio")) {
7
+ const server = createWorkflowMcpServer();
8
+ await runStdio(server);
9
+ console.error("Devglide Workflow MCP server running on stdio");
10
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "CI Pipeline",
3
+ "description": "Lint, test, and build pipeline",
4
+ "version": 1,
5
+ "tags": ["ci", "pipeline"],
6
+ "variables": [],
7
+ "nodes": [
8
+ { "id": "trigger-1", "type": "trigger", "label": "Manual Trigger", "config": { "nodeType": "trigger", "triggerType": "manual" }, "position": { "x": 400, "y": 0 } },
9
+ { "id": "lint-1", "type": "action:shell", "label": "Lint", "config": { "nodeType": "action:shell", "command": "npm run lint", "captureOutput": true }, "position": { "x": 400, "y": 150 } },
10
+ { "id": "test-1", "type": "action:shell", "label": "Test", "config": { "nodeType": "action:shell", "command": "npm test", "captureOutput": true }, "position": { "x": 400, "y": 300 } },
11
+ { "id": "build-1", "type": "action:shell", "label": "Build", "config": { "nodeType": "action:shell", "command": "npm run build", "captureOutput": true }, "position": { "x": 400, "y": 450 } }
12
+ ],
13
+ "edges": [
14
+ { "id": "e1", "source": "trigger-1", "target": "lint-1" },
15
+ { "id": "e2", "source": "lint-1", "target": "test-1" },
16
+ { "id": "e3", "source": "test-1", "target": "build-1" }
17
+ ]
18
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "Code Review",
3
+ "description": "Auto-review: run tests, check diff, log results",
4
+ "version": 1,
5
+ "tags": ["git", "review"],
6
+ "variables": [],
7
+ "nodes": [
8
+ { "id": "trigger-1", "type": "trigger", "label": "Git Push", "config": { "nodeType": "trigger", "triggerType": "git-event", "gitEvent": "push" }, "position": { "x": 400, "y": 0 } },
9
+ { "id": "diff-1", "type": "action:git", "label": "Get Diff", "config": { "nodeType": "action:git", "operation": "diff" }, "position": { "x": 400, "y": 150 } },
10
+ { "id": "test-1", "type": "action:shell", "label": "Run Tests", "config": { "nodeType": "action:shell", "command": "npm test", "captureOutput": true }, "position": { "x": 400, "y": 300 } },
11
+ { "id": "decision-1", "type": "decision", "label": "Tests Passed?", "config": { "nodeType": "decision", "conditionType": "exit-code", "ports": [{ "id": "yes", "label": "Passed" }, { "id": "no", "label": "Failed" }] }, "position": { "x": 400, "y": 450 } },
12
+ { "id": "log-pass", "type": "action:log", "label": "Log Success", "config": { "nodeType": "action:log", "operation": "write", "message": "Code review passed: all tests green" }, "position": { "x": 250, "y": 600 } },
13
+ { "id": "log-fail", "type": "action:log", "label": "Log Failure", "config": { "nodeType": "action:log", "operation": "write", "message": "Code review failed: tests did not pass" }, "position": { "x": 550, "y": 600 } }
14
+ ],
15
+ "edges": [
16
+ { "id": "e1", "source": "trigger-1", "target": "diff-1" },
17
+ { "id": "e2", "source": "diff-1", "target": "test-1" },
18
+ { "id": "e3", "source": "test-1", "target": "decision-1" },
19
+ { "id": "e4", "source": "decision-1", "target": "log-pass", "sourcePort": "yes" },
20
+ { "id": "e5", "source": "decision-1", "target": "log-fail", "sourcePort": "no" }
21
+ ]
22
+ }