erne-universal 0.2.0 → 0.3.0

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,191 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { WebSocketServer } = require('ws');
5
+
6
+ const PORT = parseInt(process.env.ERNE_DASHBOARD_PORT, 10) || 3333;
7
+ const PUBLIC_DIR = path.join(__dirname, 'public');
8
+ const AGENT_TIMEOUT_MS = 5 * 60 * 1000;
9
+ const TIMEOUT_CHECK_INTERVAL_MS = 30 * 1000;
10
+ const DONE_TO_IDLE_DELAY_MS = 3000;
11
+
12
+ const MIME_TYPES = {
13
+ '.html': 'text/html',
14
+ '.js': 'application/javascript',
15
+ '.css': 'text/css',
16
+ '.png': 'image/png',
17
+ '.json': 'application/json',
18
+ };
19
+
20
+ const AGENT_DEFINITIONS = [
21
+ { name: 'architect', room: 'development' },
22
+ { name: 'native-bridge-builder', room: 'development' },
23
+ { name: 'expo-config-resolver', room: 'development' },
24
+ { name: 'ui-designer', room: 'development' },
25
+ { name: 'code-reviewer', room: 'review' },
26
+ { name: 'upgrade-assistant', room: 'review' },
27
+ { name: 'tdd-guide', room: 'testing' },
28
+ { name: 'performance-profiler', room: 'testing' },
29
+ { name: 'senior-developer', room: 'development' },
30
+ { name: 'feature-builder', room: 'development' },
31
+ ];
32
+
33
+ const agentState = {};
34
+
35
+ const initAgentState = () => {
36
+ for (const def of AGENT_DEFINITIONS) {
37
+ agentState[def.name] = {
38
+ status: 'idle',
39
+ task: null,
40
+ room: def.room,
41
+ startedAt: null,
42
+ lastEvent: null,
43
+ };
44
+ }
45
+ };
46
+
47
+ initAgentState();
48
+
49
+ const handleEvent = (event) => {
50
+ const { type, agent, task } = event;
51
+ if (!agent || !agentState[agent]) {
52
+ return { error: `Unknown agent: ${agent}` };
53
+ }
54
+
55
+ const now = new Date().toISOString();
56
+ const state = agentState[agent];
57
+ state.lastEvent = now;
58
+
59
+ if (type === 'agent:start') {
60
+ state.status = 'working';
61
+ state.task = task || null;
62
+ state.startedAt = now;
63
+ } else if (type === 'agent:complete') {
64
+ state.status = 'done';
65
+ state.task = task || state.task;
66
+ state.startedAt = null;
67
+ setTimeout(() => {
68
+ if (agentState[agent].status === 'done') {
69
+ agentState[agent].status = 'idle';
70
+ agentState[agent].task = null;
71
+ broadcastState();
72
+ }
73
+ }, DONE_TO_IDLE_DELAY_MS);
74
+ }
75
+
76
+ return { ok: true };
77
+ };
78
+
79
+ let wss;
80
+
81
+ const broadcastState = () => {
82
+ if (!wss) return;
83
+ const data = JSON.stringify(agentState);
84
+ for (const client of wss.clients) {
85
+ if (client.readyState === 1) {
86
+ client.send(data);
87
+ }
88
+ }
89
+ };
90
+
91
+ // Auto-timeout: reset agents to idle after 5 minutes of no events
92
+ setInterval(() => {
93
+ const now = Date.now();
94
+ let changed = false;
95
+ for (const name of Object.keys(agentState)) {
96
+ const agent = agentState[name];
97
+ if (agent.status !== 'idle' && agent.lastEvent) {
98
+ const elapsed = now - new Date(agent.lastEvent).getTime();
99
+ if (elapsed > AGENT_TIMEOUT_MS) {
100
+ agent.status = 'idle';
101
+ agent.task = null;
102
+ agent.startedAt = null;
103
+ changed = true;
104
+ }
105
+ }
106
+ }
107
+ if (changed) {
108
+ broadcastState();
109
+ }
110
+ }, TIMEOUT_CHECK_INTERVAL_MS);
111
+
112
+ const parseBody = (req) =>
113
+ new Promise((resolve, reject) => {
114
+ const chunks = [];
115
+ req.on('data', (chunk) => chunks.push(chunk));
116
+ req.on('end', () => {
117
+ try {
118
+ resolve(JSON.parse(Buffer.concat(chunks).toString()));
119
+ } catch (e) {
120
+ reject(e);
121
+ }
122
+ });
123
+ req.on('error', reject);
124
+ });
125
+
126
+ const serveStatic = (req, res) => {
127
+ let urlPath = req.url.split('?')[0];
128
+ if (urlPath === '/') urlPath = '/index.html';
129
+
130
+ const filePath = path.join(PUBLIC_DIR, urlPath);
131
+ const resolved = path.resolve(filePath);
132
+
133
+ // Directory traversal prevention
134
+ if (!resolved.startsWith(PUBLIC_DIR + path.sep) && resolved !== PUBLIC_DIR) {
135
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
136
+ res.end('Forbidden');
137
+ return;
138
+ }
139
+
140
+ const ext = path.extname(resolved);
141
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
142
+
143
+ fs.readFile(resolved, (err, data) => {
144
+ if (err) {
145
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
146
+ res.end('Not Found');
147
+ return;
148
+ }
149
+ res.writeHead(200, { 'Content-Type': contentType });
150
+ res.end(data);
151
+ });
152
+ };
153
+
154
+ const server = http.createServer(async (req, res) => {
155
+ if (req.method === 'GET' && req.url === '/api/state') {
156
+ res.writeHead(200, { 'Content-Type': 'application/json' });
157
+ res.end(JSON.stringify(agentState));
158
+ return;
159
+ }
160
+
161
+ if (req.method === 'POST' && req.url === '/api/events') {
162
+ try {
163
+ const body = await parseBody(req);
164
+ const result = handleEvent(body);
165
+ if (result.error) {
166
+ res.writeHead(400, { 'Content-Type': 'application/json' });
167
+ res.end(JSON.stringify(result));
168
+ } else {
169
+ broadcastState();
170
+ res.writeHead(200, { 'Content-Type': 'application/json' });
171
+ res.end(JSON.stringify(result));
172
+ }
173
+ } catch (e) {
174
+ res.writeHead(400, { 'Content-Type': 'application/json' });
175
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
176
+ }
177
+ return;
178
+ }
179
+
180
+ serveStatic(req, res);
181
+ });
182
+
183
+ wss = new WebSocketServer({ server });
184
+
185
+ wss.on('connection', (ws) => {
186
+ ws.send(JSON.stringify(agentState));
187
+ });
188
+
189
+ server.listen(PORT, () => {
190
+ console.log(`ERNE Dashboard running on http://localhost:${PORT}`);
191
+ });