@ryanfw/prompt-orchestration-pipeline 0.0.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 +290 -0
- package/package.json +51 -0
- package/src/api/index.js +220 -0
- package/src/cli/index.js +70 -0
- package/src/core/config.js +345 -0
- package/src/core/environment.js +56 -0
- package/src/core/orchestrator.js +335 -0
- package/src/core/pipeline-runner.js +182 -0
- package/src/core/retry.js +83 -0
- package/src/core/task-runner.js +305 -0
- package/src/core/validation.js +100 -0
- package/src/llm/README.md +345 -0
- package/src/llm/index.js +320 -0
- package/src/providers/anthropic.js +117 -0
- package/src/providers/base.js +71 -0
- package/src/providers/deepseek.js +122 -0
- package/src/providers/openai.js +314 -0
- package/src/ui/README.md +86 -0
- package/src/ui/public/app.js +260 -0
- package/src/ui/public/index.html +53 -0
- package/src/ui/public/style.css +341 -0
- package/src/ui/server.js +230 -0
- package/src/ui/state.js +67 -0
- package/src/ui/watcher.js +85 -0
package/src/ui/server.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single Node.js server handling static files, API, and SSE
|
|
3
|
+
* Serves UI and provides real-time file change updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import http from "http";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import url from "url";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { start as startWatcher, stop as stopWatcher } from "./watcher.js";
|
|
12
|
+
import * as state from "./state.js";
|
|
13
|
+
|
|
14
|
+
// Get __dirname equivalent in ES modules
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
const PORT = process.env.PORT || 4000;
|
|
20
|
+
const WATCHED_PATHS = (process.env.WATCHED_PATHS || "pipeline-config,runs")
|
|
21
|
+
.split(",")
|
|
22
|
+
.map((p) => p.trim());
|
|
23
|
+
const HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
|
24
|
+
|
|
25
|
+
// SSE clients management
|
|
26
|
+
const sseClients = new Set();
|
|
27
|
+
let heartbeatTimer = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Send SSE message to a client
|
|
31
|
+
*/
|
|
32
|
+
function sendSSE(res, event, data) {
|
|
33
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Broadcast state update to all SSE clients
|
|
38
|
+
*/
|
|
39
|
+
function broadcastStateUpdate(currentState) {
|
|
40
|
+
const deadClients = new Set();
|
|
41
|
+
|
|
42
|
+
sseClients.forEach((client) => {
|
|
43
|
+
try {
|
|
44
|
+
sendSSE(client, "state", currentState);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
deadClients.add(client);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Clean up dead connections
|
|
51
|
+
deadClients.forEach((client) => sseClients.delete(client));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Start heartbeat to keep connections alive
|
|
56
|
+
*/
|
|
57
|
+
function startHeartbeat() {
|
|
58
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
59
|
+
|
|
60
|
+
heartbeatTimer = setInterval(() => {
|
|
61
|
+
const deadClients = new Set();
|
|
62
|
+
|
|
63
|
+
sseClients.forEach((client) => {
|
|
64
|
+
try {
|
|
65
|
+
client.write(":heartbeat\n\n");
|
|
66
|
+
} catch (err) {
|
|
67
|
+
deadClients.add(client);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
deadClients.forEach((client) => sseClients.delete(client));
|
|
72
|
+
}, HEARTBEAT_INTERVAL);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Serve static files from public directory
|
|
77
|
+
*/
|
|
78
|
+
function serveStatic(res, filePath) {
|
|
79
|
+
const ext = path.extname(filePath);
|
|
80
|
+
const contentTypes = {
|
|
81
|
+
".html": "text/html",
|
|
82
|
+
".js": "application/javascript",
|
|
83
|
+
".css": "text/css",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
fs.readFile(filePath, (err, content) => {
|
|
87
|
+
if (err) {
|
|
88
|
+
res.writeHead(404);
|
|
89
|
+
res.end("Not Found");
|
|
90
|
+
} else {
|
|
91
|
+
res.writeHead(200, { "Content-Type": contentTypes[ext] || "text/plain" });
|
|
92
|
+
res.end(content);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create and start the HTTP server
|
|
99
|
+
*/
|
|
100
|
+
function createServer() {
|
|
101
|
+
const server = http.createServer((req, res) => {
|
|
102
|
+
const parsedUrl = url.parse(req.url, true);
|
|
103
|
+
const pathname = parsedUrl.pathname;
|
|
104
|
+
|
|
105
|
+
// CORS headers for API endpoints
|
|
106
|
+
if (pathname.startsWith("/api/")) {
|
|
107
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
108
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
109
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
110
|
+
|
|
111
|
+
if (req.method === "OPTIONS") {
|
|
112
|
+
res.writeHead(204);
|
|
113
|
+
res.end();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Route: GET /api/state
|
|
119
|
+
if (pathname === "/api/state" && req.method === "GET") {
|
|
120
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
121
|
+
res.end(JSON.stringify(state.getState()));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Route: GET /api/events (SSE)
|
|
126
|
+
if (pathname === "/api/events" && req.method === "GET") {
|
|
127
|
+
res.writeHead(200, {
|
|
128
|
+
"Content-Type": "text/event-stream",
|
|
129
|
+
"Cache-Control": "no-cache",
|
|
130
|
+
Connection: "keep-alive",
|
|
131
|
+
"Access-Control-Allow-Origin": "*",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Send initial state
|
|
135
|
+
sendSSE(res, "state", state.getState());
|
|
136
|
+
|
|
137
|
+
// Add to clients
|
|
138
|
+
sseClients.add(res);
|
|
139
|
+
|
|
140
|
+
// Remove client on disconnect
|
|
141
|
+
req.on("close", () => {
|
|
142
|
+
sseClients.delete(res);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Serve static files
|
|
149
|
+
if (pathname === "/" || pathname === "/index.html") {
|
|
150
|
+
serveStatic(res, path.join(__dirname, "public", "index.html"));
|
|
151
|
+
} else if (pathname === "/app.js") {
|
|
152
|
+
serveStatic(res, path.join(__dirname, "public", "app.js"));
|
|
153
|
+
} else if (pathname === "/style.css") {
|
|
154
|
+
serveStatic(res, path.join(__dirname, "public", "style.css"));
|
|
155
|
+
} else {
|
|
156
|
+
res.writeHead(404);
|
|
157
|
+
res.end("Not Found");
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return server;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Initialize file watcher
|
|
166
|
+
*/
|
|
167
|
+
let watcher = null;
|
|
168
|
+
|
|
169
|
+
function initializeWatcher() {
|
|
170
|
+
state.setWatchedPaths(WATCHED_PATHS);
|
|
171
|
+
|
|
172
|
+
watcher = startWatcher(WATCHED_PATHS, (changes) => {
|
|
173
|
+
// Update state for each change
|
|
174
|
+
changes.forEach(({ path, type }) => {
|
|
175
|
+
state.recordChange(path, type);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Broadcast updated state
|
|
179
|
+
broadcastStateUpdate(state.getState());
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Start the server
|
|
185
|
+
*/
|
|
186
|
+
function start(customPort) {
|
|
187
|
+
const port = customPort || PORT;
|
|
188
|
+
const server = createServer();
|
|
189
|
+
|
|
190
|
+
server.listen(port, () => {
|
|
191
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
192
|
+
console.log(`Watching paths: ${WATCHED_PATHS.join(", ")}`);
|
|
193
|
+
|
|
194
|
+
initializeWatcher();
|
|
195
|
+
startHeartbeat();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Graceful shutdown
|
|
199
|
+
process.on("SIGINT", async () => {
|
|
200
|
+
console.log("\nShutting down gracefully...");
|
|
201
|
+
|
|
202
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
203
|
+
if (watcher) await stopWatcher(watcher);
|
|
204
|
+
|
|
205
|
+
sseClients.forEach((client) => client.end());
|
|
206
|
+
sseClients.clear();
|
|
207
|
+
|
|
208
|
+
server.close(() => {
|
|
209
|
+
console.log("Server closed");
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return server;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Export for testing
|
|
218
|
+
export {
|
|
219
|
+
createServer,
|
|
220
|
+
start,
|
|
221
|
+
broadcastStateUpdate,
|
|
222
|
+
sseClients,
|
|
223
|
+
initializeWatcher,
|
|
224
|
+
state,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Start server if run directly
|
|
228
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
229
|
+
start();
|
|
230
|
+
}
|
package/src/ui/state.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple state manager for tracking file changes
|
|
3
|
+
* Maintains an in-memory state with change history
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MAX_RECENT_CHANGES = 10;
|
|
7
|
+
|
|
8
|
+
let state = {
|
|
9
|
+
updatedAt: new Date().toISOString(),
|
|
10
|
+
changeCount: 0,
|
|
11
|
+
recentChanges: [],
|
|
12
|
+
watchedPaths: [],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the current state
|
|
17
|
+
* @returns {Object} Current state object
|
|
18
|
+
*/
|
|
19
|
+
export function getState() {
|
|
20
|
+
return { ...state };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Record a file change event
|
|
25
|
+
* @param {string} path - File path that changed
|
|
26
|
+
* @param {string} type - Type of change: 'created', 'modified', or 'deleted'
|
|
27
|
+
* @returns {Object} Updated state
|
|
28
|
+
*/
|
|
29
|
+
export function recordChange(path, type) {
|
|
30
|
+
const timestamp = new Date().toISOString();
|
|
31
|
+
|
|
32
|
+
// Add to recent changes (FIFO)
|
|
33
|
+
const recentChanges = [
|
|
34
|
+
{ path, type, timestamp },
|
|
35
|
+
...state.recentChanges,
|
|
36
|
+
].slice(0, MAX_RECENT_CHANGES);
|
|
37
|
+
|
|
38
|
+
// Update state
|
|
39
|
+
state = {
|
|
40
|
+
...state,
|
|
41
|
+
updatedAt: timestamp,
|
|
42
|
+
changeCount: state.changeCount + 1,
|
|
43
|
+
recentChanges,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return getState();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset state to initial values
|
|
51
|
+
*/
|
|
52
|
+
export function reset() {
|
|
53
|
+
state = {
|
|
54
|
+
updatedAt: new Date().toISOString(),
|
|
55
|
+
changeCount: 0,
|
|
56
|
+
recentChanges: [],
|
|
57
|
+
watchedPaths: state.watchedPaths, // Preserve watched paths
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set the paths being watched
|
|
63
|
+
* @param {string[]} paths - Array of watched directory paths
|
|
64
|
+
*/
|
|
65
|
+
export function setWatchedPaths(paths) {
|
|
66
|
+
state.watchedPaths = [...paths];
|
|
67
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher for monitoring pipeline directories
|
|
3
|
+
* Provides real-time file change notifications
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chokidar from "chokidar";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Start watching specified paths for file changes
|
|
10
|
+
* @param {string[]} paths - Array of directory paths to watch
|
|
11
|
+
* @param {Function} onChange - Callback function to handle file changes
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @param {number} options.debounceMs - Debounce time in milliseconds (default: 200)
|
|
14
|
+
* @returns {Object} Watcher instance with close method
|
|
15
|
+
*/
|
|
16
|
+
export function start(paths, onChange, options = {}) {
|
|
17
|
+
const debounceMs = options.debounceMs || 200;
|
|
18
|
+
let debounceTimer = null;
|
|
19
|
+
let pendingChanges = [];
|
|
20
|
+
|
|
21
|
+
// Initialize chokidar watcher
|
|
22
|
+
const watcher = chokidar.watch(paths, {
|
|
23
|
+
ignored: /(^|[\/\\])(\.git|node_modules|dist)([\/\\]|$)/,
|
|
24
|
+
persistent: true,
|
|
25
|
+
ignoreInitial: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Debounced change handler
|
|
29
|
+
const flushChanges = () => {
|
|
30
|
+
if (pendingChanges.length > 0) {
|
|
31
|
+
const changes = [...pendingChanges];
|
|
32
|
+
pendingChanges = [];
|
|
33
|
+
onChange(changes);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const scheduleFlush = () => {
|
|
38
|
+
if (debounceTimer) {
|
|
39
|
+
clearTimeout(debounceTimer);
|
|
40
|
+
}
|
|
41
|
+
debounceTimer = setTimeout(flushChanges, debounceMs);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Handle file events
|
|
45
|
+
watcher.on("add", (path) => {
|
|
46
|
+
pendingChanges.push({ path, type: "created" });
|
|
47
|
+
scheduleFlush();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
watcher.on("change", (path) => {
|
|
51
|
+
pendingChanges.push({ path, type: "modified" });
|
|
52
|
+
scheduleFlush();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
watcher.on("unlink", (path) => {
|
|
56
|
+
pendingChanges.push({ path, type: "deleted" });
|
|
57
|
+
scheduleFlush();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Return watcher with enhanced close method
|
|
61
|
+
return {
|
|
62
|
+
_chokidarWatcher: watcher,
|
|
63
|
+
_debounceTimer: debounceTimer,
|
|
64
|
+
close: async () => {
|
|
65
|
+
if (debounceTimer) {
|
|
66
|
+
clearTimeout(debounceTimer);
|
|
67
|
+
debounceTimer = null;
|
|
68
|
+
}
|
|
69
|
+
pendingChanges = [];
|
|
70
|
+
await watcher.close();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Stop watching files
|
|
77
|
+
* @param {Object} watcher - Watcher instance to stop
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
export async function stop(watcher) {
|
|
81
|
+
if (!watcher) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await watcher.close();
|
|
85
|
+
}
|