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.
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/bin/claude-md-template.js +94 -0
- package/bin/devglide.js +387 -0
- package/package.json +85 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/apps/coder/.turbo/turbo-lint.log +5 -0
- package/src/apps/coder/package.json +16 -0
- package/src/apps/coder/public/favicon.svg +7 -0
- package/src/apps/coder/public/page.css +275 -0
- package/src/apps/coder/public/page.js +528 -0
- package/src/apps/coder/server.js +3 -0
- package/src/apps/documentation/public/page.css +597 -0
- package/src/apps/documentation/public/page.js +609 -0
- package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
- package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/kanban/package.json +32 -0
- package/src/apps/kanban/public/favicon.svg +7 -0
- package/src/apps/kanban/public/page.css +1010 -0
- package/src/apps/kanban/public/page.js +1730 -0
- package/src/apps/kanban/public/vendor/marked.min.js +6 -0
- package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
- package/src/apps/kanban/src/db.ts +319 -0
- package/src/apps/kanban/src/index.ts +14 -0
- package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
- package/src/apps/kanban/src/mcp-helpers.ts +60 -0
- package/src/apps/kanban/src/mcp.ts +59 -0
- package/src/apps/kanban/src/routes/attachments.ts +161 -0
- package/src/apps/kanban/src/routes/features.ts +233 -0
- package/src/apps/kanban/src/routes/issues.ts +373 -0
- package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
- package/src/apps/kanban/src/tools/item-tools.ts +307 -0
- package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
- package/src/apps/kanban/tsconfig.check.json +9 -0
- package/src/apps/kanban/tsconfig.json +9 -0
- package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
- package/src/apps/keymap/package.json +16 -0
- package/src/apps/keymap/public/page.css +275 -0
- package/src/apps/keymap/public/page.js +294 -0
- package/src/apps/keymap/server.js +25 -0
- package/src/apps/log/.turbo/turbo-build.log +5 -0
- package/src/apps/log/.turbo/turbo-lint.log +45 -0
- package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/log/node_modules/.bin/tsc +21 -0
- package/src/apps/log/node_modules/.bin/tsserver +21 -0
- package/src/apps/log/node_modules/.bin/tsx +21 -0
- package/src/apps/log/package.json +36 -0
- package/src/apps/log/public/console-sniffer.js +221 -0
- package/src/apps/log/public/favicon.svg +7 -0
- package/src/apps/log/public/page.css +322 -0
- package/src/apps/log/public/page.js +463 -0
- package/src/apps/log/src/index.ts +9 -0
- package/src/apps/log/src/mcp.ts +122 -0
- package/src/apps/log/src/routes/log.ts +333 -0
- package/src/apps/log/src/routes/status.ts +25 -0
- package/src/apps/log/src/server-sniffer.ts +118 -0
- package/src/apps/log/src/services/file-patterns.ts +39 -0
- package/src/apps/log/src/services/file-tailer.ts +228 -0
- package/src/apps/log/src/services/line-parser.ts +94 -0
- package/src/apps/log/src/services/log-writer.ts +39 -0
- package/src/apps/log/tsconfig.json +8 -0
- package/src/apps/prompts/.turbo/turbo-build.log +5 -0
- package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
- package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/prompts/mcp.ts +175 -0
- package/src/apps/prompts/node_modules/.bin/tsc +21 -0
- package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
- package/src/apps/prompts/node_modules/.bin/tsx +21 -0
- package/src/apps/prompts/package.json +25 -0
- package/src/apps/prompts/public/page.css +315 -0
- package/src/apps/prompts/public/page.js +541 -0
- package/src/apps/prompts/services/prompt-store.ts +212 -0
- package/src/apps/prompts/src/index.ts +9 -0
- package/src/apps/prompts/tsconfig.json +8 -0
- package/src/apps/prompts/types.ts +27 -0
- package/src/apps/shell/.turbo/turbo-build.log +5 -0
- package/src/apps/shell/.turbo/turbo-lint.log +34 -0
- package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/shell/package.json +35 -0
- package/src/apps/shell/public/favicon.svg +7 -0
- package/src/apps/shell/public/page.css +407 -0
- package/src/apps/shell/public/page.js +1577 -0
- package/src/apps/shell/src/index.ts +150 -0
- package/src/apps/shell/src/mcp.ts +398 -0
- package/src/apps/shell/src/shell-types.ts +41 -0
- package/src/apps/shell/tsconfig.json +8 -0
- package/src/apps/test/.turbo/turbo-build.log +5 -0
- package/src/apps/test/.turbo/turbo-lint.log +27 -0
- package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/test/node_modules/.bin/tsc +21 -0
- package/src/apps/test/node_modules/.bin/tsserver +21 -0
- package/src/apps/test/node_modules/.bin/tsx +21 -0
- package/src/apps/test/node_modules/.bin/uuid +21 -0
- package/src/apps/test/package.json +35 -0
- package/src/apps/test/public/favicon.svg +7 -0
- package/src/apps/test/public/page.css +499 -0
- package/src/apps/test/public/page.js +417 -0
- package/src/apps/test/public/scenario-runner.js +450 -0
- package/src/apps/test/src/index.ts +9 -0
- package/src/apps/test/src/mcp.ts +192 -0
- package/src/apps/test/src/routes/trigger.ts +285 -0
- package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
- package/src/apps/test/src/services/scenario-manager.ts +361 -0
- package/src/apps/test/src/services/scenario-store.ts +145 -0
- package/src/apps/test/tsconfig.json +8 -0
- package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
- package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
- package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/vocabulary/mcp.ts +173 -0
- package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
- package/src/apps/vocabulary/package.json +25 -0
- package/src/apps/vocabulary/public/page.css +247 -0
- package/src/apps/vocabulary/public/page.js +444 -0
- package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
- package/src/apps/vocabulary/src/index.ts +10 -0
- package/src/apps/vocabulary/tsconfig.json +8 -0
- package/src/apps/vocabulary/types.ts +22 -0
- package/src/apps/voice/.turbo/turbo-build.log +5 -0
- package/src/apps/voice/.turbo/turbo-lint.log +43 -0
- package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/voice/node_modules/.bin/openai +21 -0
- package/src/apps/voice/node_modules/.bin/tsc +21 -0
- package/src/apps/voice/node_modules/.bin/tsserver +21 -0
- package/src/apps/voice/node_modules/.bin/tsx +21 -0
- package/src/apps/voice/package.json +35 -0
- package/src/apps/voice/public/favicon.svg +7 -0
- package/src/apps/voice/public/page.css +388 -0
- package/src/apps/voice/public/page.js +718 -0
- package/src/apps/voice/src/index.ts +10 -0
- package/src/apps/voice/src/mcp.ts +70 -0
- package/src/apps/voice/src/providers/index.ts +85 -0
- package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
- package/src/apps/voice/src/providers/types.ts +27 -0
- package/src/apps/voice/src/routes/config.ts +118 -0
- package/src/apps/voice/src/routes/transcribe.ts +90 -0
- package/src/apps/voice/src/services/config-store.ts +129 -0
- package/src/apps/voice/src/services/stats.ts +108 -0
- package/src/apps/voice/src/transcribe.ts +11 -0
- package/src/apps/voice/src/utils/mime.ts +16 -0
- package/src/apps/voice/tsconfig.json +8 -0
- package/src/apps/workflow/.turbo/turbo-build.log +5 -0
- package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
- package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
- package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
- package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
- package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
- package/src/apps/workflow/engine/executors/index.ts +28 -0
- package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
- package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
- package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
- package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
- package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
- package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
- package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
- package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
- package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
- package/src/apps/workflow/engine/graph-runner.ts +438 -0
- package/src/apps/workflow/engine/node-executor.ts +104 -0
- package/src/apps/workflow/engine/node-registry.ts +15 -0
- package/src/apps/workflow/engine/variable-resolver.ts +109 -0
- package/src/apps/workflow/mcp.ts +223 -0
- package/src/apps/workflow/node_modules/.bin/tsc +21 -0
- package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
- package/src/apps/workflow/node_modules/.bin/tsx +21 -0
- package/src/apps/workflow/package.json +25 -0
- package/src/apps/workflow/public/editor/canvas.js +366 -0
- package/src/apps/workflow/public/editor/drag-manager.js +326 -0
- package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
- package/src/apps/workflow/public/editor/history-manager.js +147 -0
- package/src/apps/workflow/public/editor/layout-engine.js +159 -0
- package/src/apps/workflow/public/editor/node-renderer.js +199 -0
- package/src/apps/workflow/public/editor/selection-manager.js +193 -0
- package/src/apps/workflow/public/favicon.svg +7 -0
- package/src/apps/workflow/public/models/node-types.js +300 -0
- package/src/apps/workflow/public/models/workflow-model.js +257 -0
- package/src/apps/workflow/public/page.css +406 -0
- package/src/apps/workflow/public/page.js +658 -0
- package/src/apps/workflow/public/panels/inspector.js +360 -0
- package/src/apps/workflow/public/panels/palette.js +106 -0
- package/src/apps/workflow/public/panels/run-view.js +275 -0
- package/src/apps/workflow/public/panels/toolbar.js +232 -0
- package/src/apps/workflow/public/panels/workflow-list.js +237 -0
- package/src/apps/workflow/public/state/store.js +47 -0
- package/src/apps/workflow/services/custom-node-loader.ts +48 -0
- package/src/apps/workflow/services/legacy-converter.ts +72 -0
- package/src/apps/workflow/services/run-manager.ts +190 -0
- package/src/apps/workflow/services/workflow-store.ts +424 -0
- package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
- package/src/apps/workflow/services/workflow-validator.ts +98 -0
- package/src/apps/workflow/src/index.ts +10 -0
- package/src/apps/workflow/templates/ci-pipeline.json +18 -0
- package/src/apps/workflow/templates/code-review.json +22 -0
- package/src/apps/workflow/templates/kanban-testing.json +24 -0
- package/src/apps/workflow/tsconfig.json +8 -0
- package/src/apps/workflow/types.ts +268 -0
- package/src/packages/auth-middleware.ts +14 -0
- package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
- package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
- package/src/packages/design-tokens/build.js +413 -0
- package/src/packages/design-tokens/demo/index.html +1367 -0
- package/src/packages/design-tokens/demo/proposition-a.html +717 -0
- package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
- package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
- package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
- package/src/packages/design-tokens/dist/tokens.css +345 -0
- package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
- package/src/packages/design-tokens/dist/tokens.js +386 -0
- package/src/packages/design-tokens/package.json +25 -0
- package/src/packages/design-tokens/tokens.json +228 -0
- package/src/packages/devtools-middleware.ts +22 -0
- package/src/packages/eslint-config/index.js +63 -0
- package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
- package/src/packages/eslint-config/package.json +18 -0
- package/src/packages/json-file-store.ts +232 -0
- package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
- package/src/packages/mcp-utils/dist/index.d.ts +33 -0
- package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
- package/src/packages/mcp-utils/dist/index.js +126 -0
- package/src/packages/mcp-utils/dist/index.js.map +1 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
- package/src/packages/mcp-utils/package.json +32 -0
- package/src/packages/mcp-utils/src/index.ts +171 -0
- package/src/packages/mcp-utils/tsconfig.json +9 -0
- package/src/packages/paths.ts +18 -0
- package/src/packages/project-context/index.js +55 -0
- package/src/packages/project-context/package.json +13 -0
- package/src/packages/project-store.ts +127 -0
- package/src/packages/server-sniffer.ts +132 -0
- package/src/packages/shared-assets/favicon.svg +7 -0
- package/src/packages/shared-assets/keymap-registry.js +512 -0
- package/src/packages/shared-assets/logo.svg +6 -0
- package/src/packages/shared-assets/package.json +11 -0
- package/src/packages/shared-assets/ui-utils.js +48 -0
- package/src/packages/shared-assets/voice-widget.d.ts +37 -0
- package/src/packages/shared-assets/voice-widget.js +695 -0
- package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
- package/src/packages/shared-types/dist/index.d.ts +39 -0
- package/src/packages/shared-types/dist/index.d.ts.map +1 -0
- package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
- package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
- package/src/packages/shared-types/package.json +25 -0
- package/src/packages/shared-types/src/index.ts +41 -0
- package/src/packages/shared-types/tsconfig.json +11 -0
- package/src/packages/tsconfig/base.json +15 -0
- package/src/packages/tsconfig/next.json +14 -0
- package/src/packages/tsconfig/node.json +11 -0
- package/src/packages/tsconfig/package.json +10 -0
- package/turbo.json +25 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// ── Workflow Editor — Workflow Browse/Manage List ────────────────────────
|
|
2
|
+
// List view for browsing, creating, and managing saved workflows.
|
|
3
|
+
|
|
4
|
+
const API = '/api/workflow';
|
|
5
|
+
|
|
6
|
+
let _container = null;
|
|
7
|
+
let _onSelect = null;
|
|
8
|
+
let _onNew = null;
|
|
9
|
+
let _confirmFn = null;
|
|
10
|
+
let _toastFn = null;
|
|
11
|
+
|
|
12
|
+
function esc(s) {
|
|
13
|
+
return String(s).replace(/&/g, '&').replace(/"/g, '"')
|
|
14
|
+
.replace(/</g, '<').replace(/>/g, '>');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function timeAgo(dateStr) {
|
|
18
|
+
if (!dateStr) return '';
|
|
19
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
20
|
+
const mins = Math.floor(diff / 60000);
|
|
21
|
+
if (mins < 1) return 'just now';
|
|
22
|
+
if (mins < 60) return `${mins}m ago`;
|
|
23
|
+
const hrs = Math.floor(mins / 60);
|
|
24
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
25
|
+
const days = Math.floor(hrs / 24);
|
|
26
|
+
return `${days}d ago`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function fetchWorkflows() {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`${API}/workflows`);
|
|
32
|
+
if (!res.ok) throw new Error('Failed to load');
|
|
33
|
+
return await res.json();
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function deleteWorkflow(id) {
|
|
40
|
+
const res = await fetch(`${API}/workflows/${id}`, { method: 'DELETE' });
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const err = await res.json().catch(() => ({ error: 'Delete failed' }));
|
|
43
|
+
throw new Error(err.error || 'Delete failed');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function toggleWorkflow(wf) {
|
|
48
|
+
const enabled = wf.enabled === false ? true : false;
|
|
49
|
+
const res = await fetch(`${API}/workflows/${wf.id}`, {
|
|
50
|
+
method: 'PUT',
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ ...wf, enabled }),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) throw new Error('Toggle failed');
|
|
55
|
+
return await res.json();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function toggleGlobal(wf) {
|
|
59
|
+
const isGlobal = !wf.global;
|
|
60
|
+
const res = await fetch(`${API}/workflows/${wf.id}`, {
|
|
61
|
+
method: 'PUT',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ global: isGlobal }),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) throw new Error('Global toggle failed');
|
|
66
|
+
return await res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderWorkflowCard(wf) {
|
|
70
|
+
const nodeCount = wf.nodes?.length ?? 0;
|
|
71
|
+
const edgeCount = wf.edges?.length ?? 0;
|
|
72
|
+
const enabled = wf.enabled !== false;
|
|
73
|
+
const tags = (wf.tags || []).map(t =>
|
|
74
|
+
`<span style="font-size:9px;padding:0 4px;border:1px solid var(--df-color-border-default);
|
|
75
|
+
color:var(--df-color-text-muted);text-transform:uppercase;letter-spacing:var(--df-letter-spacing-wider);">
|
|
76
|
+
${esc(t)}</span>`
|
|
77
|
+
).join('');
|
|
78
|
+
|
|
79
|
+
return `
|
|
80
|
+
<div class="wb-wflist-card" data-wf-id="${esc(wf.id)}" style="${!enabled ? 'opacity:0.5;' : ''}">
|
|
81
|
+
<div class="wb-wflist-card-body">
|
|
82
|
+
<div class="wb-wflist-card-title">${esc(wf.name)}</div>
|
|
83
|
+
${wf.description ? `<div class="wb-wflist-card-desc">${esc(wf.description).replace(/\\n/g, ' ').replace(/\n/g, ' ')}</div>` : ''}
|
|
84
|
+
<div class="wb-wflist-card-meta">
|
|
85
|
+
<span>${nodeCount} node${nodeCount !== 1 ? 's' : ''}</span>
|
|
86
|
+
<span>·</span>
|
|
87
|
+
<span>${edgeCount} edge${edgeCount !== 1 ? 's' : ''}</span>
|
|
88
|
+
${wf.updatedAt ? `<span>·</span><span>${timeAgo(wf.updatedAt)}</span>` : ''}
|
|
89
|
+
</div>
|
|
90
|
+
${tags ? `<div class="wb-wflist-card-tags">${tags}</div>` : ''}
|
|
91
|
+
</div>
|
|
92
|
+
<div class="wb-wflist-card-actions">
|
|
93
|
+
<button class="btn btn-secondary" data-action="edit" title="Edit">✎</button>
|
|
94
|
+
<button class="btn ${enabled ? 'btn-primary' : 'btn-secondary'}" data-action="toggle" title="${enabled ? 'Disable' : 'Enable'}">${enabled ? '●' : '○'}</button>
|
|
95
|
+
<button class="btn ${wf.global ? 'btn-primary' : 'btn-secondary'}" data-action="toggle-global" title="${wf.global ? 'Make project-only' : 'Make global'}">${wf.global ? '⊕' : '⊙'}</button>
|
|
96
|
+
<button class="btn btn-secondary wb-wflist-delete" data-action="delete" title="Delete">×</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function render() {
|
|
102
|
+
if (!_container) return;
|
|
103
|
+
|
|
104
|
+
_container.innerHTML = `
|
|
105
|
+
<div class="wb-wflist-header">
|
|
106
|
+
<span style="font-size:var(--df-font-size-md);color:var(--df-color-accent-default);
|
|
107
|
+
font-family:var(--df-font-mono);letter-spacing:var(--df-letter-spacing-wider);
|
|
108
|
+
text-transform:uppercase;">Workflow</span>
|
|
109
|
+
<span style="flex:1"></span>
|
|
110
|
+
<button class="btn btn-primary" data-action="new-workflow">+ New Workflow</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="wb-wflist-loading" style="padding:var(--df-space-6);text-align:center;
|
|
113
|
+
color:var(--df-color-text-muted);font-size:var(--df-font-size-xs);text-transform:uppercase;
|
|
114
|
+
letter-spacing:var(--df-letter-spacing-wider);">Loading...</div>
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
const newBtn = _container.querySelector('[data-action="new-workflow"]');
|
|
118
|
+
if (newBtn) newBtn.addEventListener('click', () => _onNew?.());
|
|
119
|
+
|
|
120
|
+
await renderList();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function renderList() {
|
|
124
|
+
if (!_container) return;
|
|
125
|
+
|
|
126
|
+
const workflows = await fetchWorkflows();
|
|
127
|
+
|
|
128
|
+
const loading = _container.querySelector('.wb-wflist-loading');
|
|
129
|
+
if (loading) loading.remove();
|
|
130
|
+
|
|
131
|
+
const oldList = _container.querySelector('.wb-wflist-grid');
|
|
132
|
+
if (oldList) oldList.remove();
|
|
133
|
+
|
|
134
|
+
const grid = document.createElement('div');
|
|
135
|
+
grid.className = 'wb-wflist-grid';
|
|
136
|
+
|
|
137
|
+
if (!workflows.length) {
|
|
138
|
+
grid.innerHTML = `
|
|
139
|
+
<div style="padding:var(--df-space-8);text-align:center;color:var(--df-color-text-muted);">
|
|
140
|
+
<div style="font-size:48px;opacity:0.15;margin-bottom:var(--df-space-2);">⚙</div>
|
|
141
|
+
<div style="font-size:var(--df-font-size-sm);text-transform:uppercase;
|
|
142
|
+
letter-spacing:var(--df-letter-spacing-wide);">
|
|
143
|
+
No workflows yet. Create one to get started.
|
|
144
|
+
</div>
|
|
145
|
+
</div>`;
|
|
146
|
+
} else {
|
|
147
|
+
grid.innerHTML = workflows.map(renderWorkflowCard).join('');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_container.appendChild(grid);
|
|
151
|
+
bindCardEvents(grid, workflows);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function bindCardEvents(grid, workflows) {
|
|
155
|
+
const cards = grid.querySelectorAll('.wb-wflist-card');
|
|
156
|
+
for (const card of cards) {
|
|
157
|
+
const wfId = card.dataset.wfId;
|
|
158
|
+
const wf = workflows.find(w => w.id === wfId);
|
|
159
|
+
|
|
160
|
+
card.querySelector('[data-action="edit"]')?.addEventListener('click', () => _onSelect?.(wf));
|
|
161
|
+
|
|
162
|
+
card.querySelector('[data-action="toggle"]')?.addEventListener('click', async () => {
|
|
163
|
+
try {
|
|
164
|
+
await toggleWorkflow(wf);
|
|
165
|
+
await renderList();
|
|
166
|
+
} catch (e) {
|
|
167
|
+
_toastFn?.(e.message || 'Toggle failed', 'error');
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
card.querySelector('[data-action="toggle-global"]')?.addEventListener('click', async () => {
|
|
172
|
+
try {
|
|
173
|
+
await toggleGlobal(wf);
|
|
174
|
+
await renderList();
|
|
175
|
+
} catch (e) {
|
|
176
|
+
_toastFn?.(e.message || 'Global toggle failed', 'error');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
card.querySelector('[data-action="delete"]')?.addEventListener('click', async () => {
|
|
181
|
+
if (!_confirmFn) return;
|
|
182
|
+
const confirmed = await _confirmFn('Delete Workflow', `Delete workflow "${wf?.name}"? This cannot be undone.`);
|
|
183
|
+
if (!confirmed) return;
|
|
184
|
+
try {
|
|
185
|
+
await deleteWorkflow(wfId);
|
|
186
|
+
_toastFn?.('Workflow deleted', 'success');
|
|
187
|
+
} catch (e) {
|
|
188
|
+
_toastFn?.(e.message || 'Delete failed', 'error');
|
|
189
|
+
}
|
|
190
|
+
await renderList();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const body = card.querySelector('.wb-wflist-card-body');
|
|
194
|
+
if (body) {
|
|
195
|
+
body.style.cursor = 'pointer';
|
|
196
|
+
body.addEventListener('click', () => _onSelect?.(wf));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Exports ─────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export const WorkflowList = {
|
|
204
|
+
mount(container) {
|
|
205
|
+
_container = container;
|
|
206
|
+
render();
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
unmount() {
|
|
210
|
+
if (_container) _container.innerHTML = '';
|
|
211
|
+
_container = null;
|
|
212
|
+
_onSelect = null;
|
|
213
|
+
_onNew = null;
|
|
214
|
+
_confirmFn = null;
|
|
215
|
+
_toastFn = null;
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
onSelect(callback) {
|
|
219
|
+
_onSelect = callback;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
onNew(callback) {
|
|
223
|
+
_onNew = callback;
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
setConfirm(fn) {
|
|
227
|
+
_confirmFn = fn;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
setToast(fn) {
|
|
231
|
+
_toastFn = fn;
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async refresh() {
|
|
235
|
+
await renderList();
|
|
236
|
+
},
|
|
237
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ── Workflow Editor — Observable State Store ────────────────────────────
|
|
2
|
+
// Granular event-driven state with wildcard support.
|
|
3
|
+
|
|
4
|
+
export class Store {
|
|
5
|
+
constructor() {
|
|
6
|
+
this._state = {
|
|
7
|
+
workflow: null, // Current workflow being edited
|
|
8
|
+
selectedNodeIds: new Set(),
|
|
9
|
+
selectedEdgeIds: new Set(),
|
|
10
|
+
zoom: 1,
|
|
11
|
+
panX: 0,
|
|
12
|
+
panY: 0,
|
|
13
|
+
mode: 'select', // 'select' | 'connect' | 'pan'
|
|
14
|
+
isDirty: false,
|
|
15
|
+
runId: null, // Active run ID
|
|
16
|
+
nodeStates: new Map(), // Runtime node states during execution
|
|
17
|
+
};
|
|
18
|
+
this._listeners = new Map(); // event -> Set<fn>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get(key) {
|
|
22
|
+
return this._state[key];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
set(key, value) {
|
|
26
|
+
const old = this._state[key];
|
|
27
|
+
this._state[key] = value;
|
|
28
|
+
this._emit(key, value, old);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
on(event, fn) {
|
|
32
|
+
if (!this._listeners.has(event)) this._listeners.set(event, new Set());
|
|
33
|
+
this._listeners.get(event).add(fn);
|
|
34
|
+
return () => this._listeners.get(event)?.delete(fn);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_emit(event, ...args) {
|
|
38
|
+
this._listeners.get(event)?.forEach(fn => {
|
|
39
|
+
try { fn(...args); } catch { /* listener errors must not break store */ }
|
|
40
|
+
});
|
|
41
|
+
this._listeners.get('*')?.forEach(fn => {
|
|
42
|
+
try { fn(event, ...args); } catch { /* swallow */ }
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const store = new Store();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getActiveProject } from '../../../project-context.js';
|
|
4
|
+
|
|
5
|
+
export interface CustomNodeDefinition {
|
|
6
|
+
type: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
category: string;
|
|
10
|
+
configSchema: Record<string, { type: string; required?: boolean; description?: string }>;
|
|
11
|
+
executor: 'shell';
|
|
12
|
+
commandTemplate: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads user-defined node types from `.devglide/node-types/` directory.
|
|
17
|
+
*/
|
|
18
|
+
export async function loadCustomNodes(): Promise<CustomNodeDefinition[]> {
|
|
19
|
+
const ap = getActiveProject();
|
|
20
|
+
if (!ap) return [];
|
|
21
|
+
|
|
22
|
+
const nodeTypesDir = path.join(ap.path, '.devglide', 'node-types');
|
|
23
|
+
const definitions: CustomNodeDefinition[] = [];
|
|
24
|
+
|
|
25
|
+
let entries: string[];
|
|
26
|
+
try {
|
|
27
|
+
entries = await fs.readdir(nodeTypesDir);
|
|
28
|
+
} catch {
|
|
29
|
+
return definitions;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (!entry.endsWith('.json')) continue;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const raw = await fs.readFile(path.join(nodeTypesDir, entry), 'utf-8');
|
|
37
|
+
const def = JSON.parse(raw) as CustomNodeDefinition;
|
|
38
|
+
|
|
39
|
+
if (!def.type || !def.label || !def.category || !def.commandTemplate) continue;
|
|
40
|
+
|
|
41
|
+
definitions.push(def);
|
|
42
|
+
} catch {
|
|
43
|
+
// Skip invalid files
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return definitions;
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import type { Workflow, WorkflowNode, WorkflowEdge } from '../types.js';
|
|
3
|
+
|
|
4
|
+
interface LegacyStep {
|
|
5
|
+
name: string;
|
|
6
|
+
cmd: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface LegacyWorkflow {
|
|
11
|
+
id?: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
ecosystem?: string;
|
|
15
|
+
steps: LegacyStep[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts legacy `{ id, name, description, ecosystem, steps }` format
|
|
20
|
+
* to the new graph-based workflow format.
|
|
21
|
+
*/
|
|
22
|
+
export function convertLegacyWorkflow(legacy: LegacyWorkflow): Workflow {
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
|
|
25
|
+
const triggerNode: WorkflowNode = {
|
|
26
|
+
id: randomUUID(),
|
|
27
|
+
type: 'trigger',
|
|
28
|
+
label: 'Manual Trigger',
|
|
29
|
+
config: {
|
|
30
|
+
nodeType: 'trigger',
|
|
31
|
+
triggerType: 'manual',
|
|
32
|
+
},
|
|
33
|
+
position: { x: 400, y: 0 },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const stepNodes: WorkflowNode[] = legacy.steps.map((step, i) => ({
|
|
37
|
+
id: randomUUID(),
|
|
38
|
+
type: 'action:shell' as const,
|
|
39
|
+
label: step.name,
|
|
40
|
+
config: {
|
|
41
|
+
nodeType: 'action:shell' as const,
|
|
42
|
+
command: step.cmd,
|
|
43
|
+
...(step.cwd ? { cwd: step.cwd } : {}),
|
|
44
|
+
captureOutput: true,
|
|
45
|
+
},
|
|
46
|
+
position: { x: 400, y: 100 + i * 150 },
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const allNodes = [triggerNode, ...stepNodes];
|
|
50
|
+
|
|
51
|
+
const edges: WorkflowEdge[] = [];
|
|
52
|
+
for (let i = 0; i < allNodes.length - 1; i++) {
|
|
53
|
+
edges.push({
|
|
54
|
+
id: randomUUID(),
|
|
55
|
+
source: allNodes[i].id,
|
|
56
|
+
target: allNodes[i + 1].id,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: legacy.id ?? randomUUID(),
|
|
62
|
+
name: legacy.name,
|
|
63
|
+
description: legacy.description,
|
|
64
|
+
version: 1,
|
|
65
|
+
tags: legacy.ecosystem ? [legacy.ecosystem] : [],
|
|
66
|
+
nodes: allNodes,
|
|
67
|
+
edges,
|
|
68
|
+
variables: [],
|
|
69
|
+
createdAt: now,
|
|
70
|
+
updatedAt: now,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import type { Response } from 'express';
|
|
3
|
+
import type { Workflow, ExecutionContext, WorkflowEvent, RunStatus, ExecutorServices } from '../types.js';
|
|
4
|
+
import { runWorkflow } from '../engine/graph-runner.js';
|
|
5
|
+
|
|
6
|
+
const RUN_TTL_MS = 60 * 60 * 1000;
|
|
7
|
+
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
interface RunRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
workflowId: string;
|
|
12
|
+
workflowName: string;
|
|
13
|
+
startedAt: string;
|
|
14
|
+
status: RunStatus;
|
|
15
|
+
context: ExecutionContext;
|
|
16
|
+
events: WorkflowEvent[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Manages active and recent workflow runs with in-memory storage and TTL cleanup.
|
|
21
|
+
*/
|
|
22
|
+
export class RunManager {
|
|
23
|
+
private static instance: RunManager;
|
|
24
|
+
private runs = new Map<string, RunRecord>();
|
|
25
|
+
private clients = new Map<string, Set<Response>>();
|
|
26
|
+
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
private executorServices: ExecutorServices = {};
|
|
28
|
+
|
|
29
|
+
static getInstance(): RunManager {
|
|
30
|
+
if (!RunManager.instance) {
|
|
31
|
+
RunManager.instance = new RunManager();
|
|
32
|
+
}
|
|
33
|
+
return RunManager.instance;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Inject service providers for executor dependency injection. */
|
|
37
|
+
setServices(services: ExecutorServices): void {
|
|
38
|
+
this.executorServices = services;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
startRun(workflow: Workflow, triggerPayload?: unknown): string {
|
|
42
|
+
const runId = randomUUID();
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
|
|
45
|
+
const context: ExecutionContext = {
|
|
46
|
+
runId,
|
|
47
|
+
workflowId: workflow.id,
|
|
48
|
+
variables: new Map(),
|
|
49
|
+
nodeStates: new Map(),
|
|
50
|
+
status: 'running',
|
|
51
|
+
startedAt: now,
|
|
52
|
+
cancelled: false,
|
|
53
|
+
services: this.executorServices,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const record: RunRecord = {
|
|
57
|
+
id: runId,
|
|
58
|
+
workflowId: workflow.id,
|
|
59
|
+
workflowName: workflow.name,
|
|
60
|
+
startedAt: now,
|
|
61
|
+
status: 'running',
|
|
62
|
+
context,
|
|
63
|
+
events: [],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.runs.set(runId, record);
|
|
67
|
+
|
|
68
|
+
const emitter = (event: WorkflowEvent) => this.emit(runId, event);
|
|
69
|
+
|
|
70
|
+
runWorkflow(workflow, emitter, triggerPayload, undefined, this.executorServices).then((ctx) => {
|
|
71
|
+
const rec = this.runs.get(runId);
|
|
72
|
+
if (rec) {
|
|
73
|
+
rec.status = ctx.status;
|
|
74
|
+
rec.context = ctx;
|
|
75
|
+
}
|
|
76
|
+
}).catch((err) => {
|
|
77
|
+
const rec = this.runs.get(runId);
|
|
78
|
+
if (rec) {
|
|
79
|
+
rec.status = 'failed';
|
|
80
|
+
rec.context.status = 'failed';
|
|
81
|
+
this.emit(runId, { type: 'error', message: String(err) });
|
|
82
|
+
this.closeClients(runId);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return runId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
cancelRun(runId: string): boolean {
|
|
90
|
+
const record = this.runs.get(runId);
|
|
91
|
+
if (!record) return false;
|
|
92
|
+
record.context.cancelled = true;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getRun(runId: string): RunRecord | undefined {
|
|
97
|
+
return this.runs.get(runId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
listRuns(): RunRecord[] {
|
|
101
|
+
return [...this.runs.values()].sort(
|
|
102
|
+
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addClient(runId: string, res: Response): void {
|
|
107
|
+
let clients = this.clients.get(runId);
|
|
108
|
+
if (!clients) {
|
|
109
|
+
clients = new Set();
|
|
110
|
+
this.clients.set(runId, clients);
|
|
111
|
+
}
|
|
112
|
+
clients.add(res);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
removeClient(runId: string, res: Response): void {
|
|
116
|
+
const clients = this.clients.get(runId);
|
|
117
|
+
if (!clients) return;
|
|
118
|
+
clients.delete(res);
|
|
119
|
+
if (clients.size === 0) {
|
|
120
|
+
this.clients.delete(runId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
startCleanup(): void {
|
|
125
|
+
if (this.cleanupInterval) return;
|
|
126
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stopCleanup(): void {
|
|
130
|
+
if (this.cleanupInterval) {
|
|
131
|
+
clearInterval(this.cleanupInterval);
|
|
132
|
+
this.cleanupInterval = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
shutdown(): void {
|
|
137
|
+
this.stopCleanup();
|
|
138
|
+
for (const record of this.runs.values()) {
|
|
139
|
+
if (record.status === 'running') {
|
|
140
|
+
record.context.cancelled = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this.clients.clear();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private emit(runId: string, event: WorkflowEvent): void {
|
|
147
|
+
const record = this.runs.get(runId);
|
|
148
|
+
if (record) {
|
|
149
|
+
record.events.push(event);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const clients = this.clients.get(runId);
|
|
153
|
+
if (!clients) return;
|
|
154
|
+
|
|
155
|
+
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
156
|
+
for (const res of clients) {
|
|
157
|
+
try {
|
|
158
|
+
res.write(data);
|
|
159
|
+
} catch {
|
|
160
|
+
clients.delete(res);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Close SSE connections on terminal events to prevent resource leak
|
|
165
|
+
if (event.type === 'done') {
|
|
166
|
+
this.closeClients(runId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private closeClients(runId: string): void {
|
|
171
|
+
const clients = this.clients.get(runId);
|
|
172
|
+
if (!clients) return;
|
|
173
|
+
for (const res of clients) {
|
|
174
|
+
try { res.end(); } catch {}
|
|
175
|
+
}
|
|
176
|
+
this.clients.delete(runId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private cleanup(): void {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
for (const [id, record] of this.runs) {
|
|
182
|
+
if (record.status === 'running') continue;
|
|
183
|
+
const age = now - new Date(record.startedAt).getTime();
|
|
184
|
+
if (age > RUN_TTL_MS) {
|
|
185
|
+
this.runs.delete(id);
|
|
186
|
+
this.clients.delete(id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|