create-byan-agent 2.7.9 → 2.8.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,215 @@
1
+ /**
2
+ * BYAN WebUI Server
3
+ * Lightweight HTTP + WebSocket server for browser-based install/update.
4
+ * No framework -- Node built-in http module + ws.
5
+ */
6
+
7
+ const http = require('http');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { WebSocketServer } = require('ws');
11
+
12
+ const MIME_TYPES = {
13
+ '.html': 'text/html; charset=utf-8',
14
+ '.css': 'text/css; charset=utf-8',
15
+ '.js': 'application/javascript; charset=utf-8',
16
+ '.json': 'application/json; charset=utf-8',
17
+ '.svg': 'image/svg+xml',
18
+ '.png': 'image/png',
19
+ '.ico': 'image/x-icon'
20
+ };
21
+
22
+ class ByanWebUI {
23
+ constructor(options = {}) {
24
+ this.port = options.port || 3000;
25
+ this.projectRoot = options.projectRoot || process.cwd();
26
+ this.publicDir = path.join(__dirname, 'public');
27
+ this.server = null;
28
+ this.wss = null;
29
+ this.clients = new Set();
30
+ this.api = require('./api');
31
+ }
32
+
33
+ start() {
34
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
35
+ this.wss = new WebSocketServer({ server: this.server });
36
+
37
+ this.wss.on('connection', (ws) => {
38
+ this.clients.add(ws);
39
+ ws.on('close', () => this.clients.delete(ws));
40
+ ws.on('error', () => this.clients.delete(ws));
41
+ });
42
+
43
+ return new Promise((resolve) => {
44
+ this.server.listen(this.port, () => {
45
+ const url = `http://localhost:${this.port}`;
46
+ console.log(`BYAN WebUI running at ${url}`);
47
+ this.openBrowser(url);
48
+ resolve(this);
49
+ });
50
+ });
51
+ }
52
+
53
+ openBrowser(url) {
54
+ const { exec } = require('child_process');
55
+ const cmds = {
56
+ darwin: 'open',
57
+ win32: 'start',
58
+ linux: 'xdg-open'
59
+ };
60
+ const cmd = cmds[process.platform] || 'xdg-open';
61
+ exec(`${cmd} ${url}`, () => {});
62
+ }
63
+
64
+ handleRequest(req, res) {
65
+ if (req.url.startsWith('/api/')) {
66
+ return this.handleAPI(req, res);
67
+ }
68
+ return this.serveStatic(req, res);
69
+ }
70
+
71
+ serveStatic(req, res) {
72
+ let urlPath = req.url.split('?')[0];
73
+ if (urlPath === '/') urlPath = '/index.html';
74
+
75
+ const filePath = path.join(this.publicDir, urlPath);
76
+ const resolved = path.resolve(filePath);
77
+
78
+ if (!resolved.startsWith(this.publicDir)) {
79
+ res.writeHead(403);
80
+ res.end('Forbidden');
81
+ return;
82
+ }
83
+
84
+ const ext = path.extname(resolved);
85
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
86
+
87
+ fs.readFile(resolved, (err, data) => {
88
+ if (err) {
89
+ if (err.code === 'ENOENT') {
90
+ fs.readFile(path.join(this.publicDir, 'index.html'), (e2, fallback) => {
91
+ if (e2) {
92
+ res.writeHead(404);
93
+ res.end('Not Found');
94
+ return;
95
+ }
96
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
97
+ res.end(fallback);
98
+ });
99
+ return;
100
+ }
101
+ res.writeHead(500);
102
+ res.end('Internal Server Error');
103
+ return;
104
+ }
105
+ res.writeHead(200, { 'Content-Type': contentType });
106
+ res.end(data);
107
+ });
108
+ }
109
+
110
+ handleAPI(req, res) {
111
+ const url = new URL(req.url, `http://${req.headers.host}`);
112
+ const route = url.pathname.replace('/api/', '');
113
+ const method = req.method.toUpperCase();
114
+
115
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
116
+
117
+ const handler = this.api.resolve(method, route);
118
+ if (!handler) {
119
+ res.writeHead(404);
120
+ res.end(JSON.stringify({ error: 'Not Found' }));
121
+ return;
122
+ }
123
+
124
+ if (method === 'POST' || method === 'PUT') {
125
+ let body = '';
126
+ req.on('data', (chunk) => { body += chunk; });
127
+ req.on('end', () => {
128
+ try {
129
+ req.body = body ? JSON.parse(body) : {};
130
+ } catch {
131
+ res.writeHead(400);
132
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
133
+ return;
134
+ }
135
+ this.runHandler(handler, req, res);
136
+ });
137
+ } else {
138
+ this.runHandler(handler, req, res);
139
+ }
140
+ }
141
+
142
+ async runHandler(handler, req, res) {
143
+ try {
144
+ await handler(req, res, this);
145
+ } catch (err) {
146
+ console.error('API error:', err);
147
+ if (!res.headersSent) {
148
+ res.writeHead(500);
149
+ res.end(JSON.stringify({ error: err.message }));
150
+ }
151
+ }
152
+ }
153
+
154
+ broadcast(data) {
155
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
156
+ for (const client of this.clients) {
157
+ if (client.readyState === 1) {
158
+ client.send(payload);
159
+ }
160
+ }
161
+ }
162
+
163
+ broadcastLog(level, message) {
164
+ this.broadcast({ type: 'log', level, message, timestamp: Date.now() });
165
+ }
166
+
167
+ broadcastProgress(step, total, label) {
168
+ this.broadcast({ type: 'progress', step, total, label });
169
+ }
170
+
171
+ broadcastComplete(success, summary) {
172
+ this.broadcast({ type: 'complete', success, summary });
173
+ }
174
+
175
+ stop() {
176
+ return new Promise((resolve) => {
177
+ for (const client of this.clients) {
178
+ client.close();
179
+ }
180
+ this.clients.clear();
181
+
182
+ if (this.wss) {
183
+ this.wss.close(() => {
184
+ if (this.server) {
185
+ this.server.close(() => resolve());
186
+ } else {
187
+ resolve();
188
+ }
189
+ });
190
+ } else if (this.server) {
191
+ this.server.close(() => resolve());
192
+ } else {
193
+ resolve();
194
+ }
195
+ });
196
+ }
197
+ }
198
+
199
+ if (require.main === module) {
200
+ const port = parseInt(process.argv[2], 10) || 3000;
201
+ const projectRoot = process.argv[3] || path.resolve(__dirname, '..', '..', '..');
202
+ const ui = new ByanWebUI({ port, projectRoot });
203
+ ui.start().then(() => {
204
+ console.log(`Project root: ${ui.projectRoot}`);
205
+ });
206
+
207
+ const shutdown = () => {
208
+ console.log('\nShutting down...');
209
+ ui.stop().then(() => process.exit(0));
210
+ };
211
+ process.on('SIGINT', shutdown);
212
+ process.on('SIGTERM', shutdown);
213
+ }
214
+
215
+ module.exports = ByanWebUI;