@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.
@@ -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
+ }
@@ -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
+ }