@josfox/jos 3.1.0 → 4.0.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.
package/src/serve.js ADDED
@@ -0,0 +1,780 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const url = require('url');
5
+ const net = require('net');
6
+ const os = require('os');
7
+
8
+ // AURORA Design System - Terminal Colors
9
+ const C = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ purple: '\x1b[38;5;135m',
14
+ magenta: '\x1b[38;5;198m',
15
+ cyan: '\x1b[38;5;51m',
16
+ blue: '\x1b[38;5;39m',
17
+ green: '\x1b[38;5;78m',
18
+ white: '\x1b[38;5;255m',
19
+ gray: '\x1b[38;5;245m',
20
+ pink: '\x1b[38;5;213m',
21
+ teal: '\x1b[38;5;44m',
22
+ };
23
+
24
+ // Aurora palette for random Kitsune colors
25
+ const AURORA_COLORS = [C.purple, C.magenta, C.cyan, C.blue, C.pink, C.teal];
26
+ const randomAurora = () => AURORA_COLORS[Math.floor(Math.random() * AURORA_COLORS.length)];
27
+
28
+ // The legendary Kitsune fox 🦊
29
+ const KITSUNE_FOX = `
30
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀
31
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠙⠻⢶⣄⡀⠀⠀⠀⢀⣤⠶⠛⠛⡇⠀⠀⠀
32
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠀⣙⣿⣦⣤⣴⣿⣁⠀⠀⣸⠇⠀⠀⠀
33
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣡⣾⣿⣿⣿⣿⣿⣿⣿⣷⣌⠋⠀⠀⠀⠀
34
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣷⣄⡈⢻⣿⡟⢁⣠⣾⣿⣦⠀⠀⠀⠀
35
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⠘⣿⠃⣿⣿⣿⣿⡏⠀⠀⠀⠀
36
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠈⠛⣰⠿⣆⠛⠁⠀⡀⠀⠀⠀⠀⠀
37
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣦⠀⠘⠛⠋⠀⣴⣿⠁⠀⠀⠀⠀⠀
38
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣾⣿⣿⣿⣿⡇⠀⠀⠀⢸⣿⣏⠀⠀⠀⠀⠀⠀
39
+ ⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠀⠀⠀⠾⢿⣿⠀⠀⠀⠀⠀⠀
40
+ ⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠟⠋⣁⣠⣤⣤⡶⠶⠶⣤⣄⠈⠀⠀⠀⠀⠀⠀
41
+ ⠀⠀⠀⢰⣿⣿⣮⣉⣉⣉⣤⣴⣶⣿⣿⣋⡥⠄⠀⠀⠀⠀⠉⢻⣄⠀⠀⠀⠀⠀
42
+ ⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣋⣁⣤⣀⣀⣤⣤⣤⣤⣄⣿⡄⠀⠀⠀⠀
43
+ ⠀⠀⠀⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠋⠉⠁⠀⠀⠀⠀⠈⠛⠃⠀⠀⠀⠀
44
+ ⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀`;
45
+
46
+ // Get colored fox
47
+ const getKitsuneBanner = () => {
48
+ const color = randomAurora();
49
+ return `${color}${KITSUNE_FOX}${C.reset}
50
+ ${C.dim}Made with ❤️ & AI${C.reset}`;
51
+ };
52
+
53
+ // Web dashboard fox (HTML version with aurora gradient)
54
+ const BRAND_ART = `<pre style="font-size:9px;line-height:9px;background:linear-gradient(135deg,#af7ac5,#00ffff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">
55
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀
56
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠙⠻⢶⣄⡀⠀⠀⠀⢀⣤⠶⠛⠛⡇⠀⠀⠀
57
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠀⣙⣿⣦⣤⣴⣿⣁⠀⠀⣸⠇⠀⠀⠀
58
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣡⣾⣿⣿⣿⣿⣿⣿⣿⣷⣌⠋⠀⠀⠀⠀
59
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣷⣄⡈⢻⣿⡟⢁⣠⣾⣿⣦⠀⠀⠀⠀
60
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⠘⣿⠃⣿⣿⣿⣿⡏⠀⠀⠀⠀
61
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠈⠛⣰⠿⣆⠛⠁⠀⡀⠀⠀⠀⠀⠀
62
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣦⠀⠘⠛⠋⠀⣴⣿⠁⠀⠀⠀⠀⠀
63
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣾⣿⣿⣿⣿⡇⠀⠀⠀⢸⣿⣏⠀⠀⠀⠀⠀⠀
64
+ ⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠀⠀⠀⠾⢿⣿⠀⠀⠀⠀⠀⠀
65
+ ⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⡿⠟⠋⣁⣠⣤⣤⡶⠶⠶⣤⣄⠈⠀⠀⠀⠀⠀⠀
66
+ ⠀⠀⠀⢰⣿⣿⣮⣉⣉⣉⣤⣴⣶⣿⣿⣋⡥⠄⠀⠀⠀⠀⠉⢻⣄⠀⠀⠀⠀⠀
67
+ ⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣋⣁⣤⣀⣀⣤⣤⣤⣤⣄⣿⡄⠀⠀⠀⠀
68
+ ⠀⠀⠀⠀⠙⠿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠋⠉⠁⠀⠀⠀⠀⠈⠛⠃⠀⠀⠀⠀
69
+ ⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
70
+ </pre><div style="text-align:right;font-size:0.7em;color:#8b949e;margin-top:-10px">Made with ❤️ & AI</div>`;
71
+
72
+
73
+ const STYLES = `
74
+ :root { --bg:#0d1117; --card:#161b22; --text:#c9d1d9; --accent:#00ffff; --success:#3fb950; --warning:#d29922; }
75
+ body { font-family:monospace; background:var(--bg); color:var(--text); margin:0; padding:20px; }
76
+ a { color:var(--text); text-decoration:none; } a:hover { color:var(--accent); }
77
+ .nav { display:flex; gap:20px; padding-bottom:20px; border-bottom:1px solid #30363d; margin-bottom:20px; }
78
+ .nav a.active { color:var(--accent); border-bottom:2px solid var(--accent); }
79
+ .card { background:var(--card); border:1px solid #30363d; padding:15px; border-radius:6px; margin-bottom:10px; transition:transform 0.1s; }
80
+ .card:hover { border-color:var(--accent); transform:translateY(-2px); }
81
+ .grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:15px; }
82
+ .badge { background:rgba(0,255,255,0.1); color:var(--accent); padding:2px 8px; border-radius:4px; font-size:0.75em; border:1px solid rgba(0,255,255,0.3); }
83
+ .badge-green { background:rgba(63,185,80,0.1); color:var(--success); border-color:rgba(63,185,80,0.3); }
84
+ .badge-yellow { background:rgba(210,153,34,0.1); color:var(--warning); border-color:rgba(210,153,34,0.3); }
85
+ h2 { border-bottom:1px solid #30363d; padding-bottom:5px; margin-top:30px; }
86
+ .stat-box { text-align:center; padding:20px; }
87
+ .stat-value { font-size:2em; color:var(--accent); font-weight:bold; }
88
+ .stat-label { font-size:0.8em; color:#8b949e; }
89
+ table { width:100%; border-collapse:collapse; }
90
+ th, td { padding:10px; text-align:left; border-bottom:1px solid #30363d; }
91
+ th { color:var(--accent); }
92
+ `;
93
+
94
+ // --- UTILITIES ---
95
+ function getIPs() {
96
+ const nets = os.networkInterfaces();
97
+ const results = [];
98
+ for (const k of Object.keys(nets)) {
99
+ for (const n of nets[k]) {
100
+ if (n.family === 'IPv4' && !n.internal) results.push(n.address);
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+
106
+ // SECURITY: Path jail - prevents directory traversal attacks
107
+ function isPathSafe(requestedPath, allowedRoots) {
108
+ const resolved = path.resolve(requestedPath);
109
+ return allowedRoots.some(root => {
110
+ const resolvedRoot = path.resolve(root);
111
+ return resolved.startsWith(resolvedRoot + path.sep) || resolved === resolvedRoot;
112
+ });
113
+ }
114
+
115
+ // SECURITY: Compute SHA-256 integrity hash for .jos files
116
+ function computeIntegrity(filePath) {
117
+ const crypto = require('crypto');
118
+ const content = fs.readFileSync(filePath);
119
+ return crypto.createHash('sha256').update(content).digest('hex');
120
+ }
121
+
122
+ async function isPortAvailable(port) {
123
+ return new Promise(r => {
124
+ const s = net.createServer().once('error', () => r(false))
125
+ .once('listening', () => s.close(() => r(true)));
126
+ s.listen(port);
127
+ });
128
+ }
129
+
130
+ function getShadowClones(home) {
131
+ const runsDir = path.join(home, 'runs');
132
+ if (!fs.existsSync(runsDir)) return [];
133
+ return fs.readdirSync(runsDir).reverse().map(id => {
134
+ try {
135
+ const reportPath = path.join(runsDir, id, 'report.json');
136
+ if (!fs.existsSync(reportPath)) return null;
137
+ const report = JSON.parse(fs.readFileSync(reportPath));
138
+ if (report.meta?.pid) {
139
+ try {
140
+ process.kill(report.meta.pid, 0);
141
+ return { ...report, epochId: id, alive: true };
142
+ } catch (e) {
143
+ return { ...report, epochId: id, alive: false };
144
+ }
145
+ }
146
+ } catch (e) { }
147
+ return null;
148
+ }).filter(Boolean);
149
+ }
150
+
151
+ function scanJosFiles(dirs) {
152
+ const list = [];
153
+ const scan = (dir, depth = 0) => {
154
+ if (depth > 5) return;
155
+ try {
156
+ fs.readdirSync(dir).forEach(f => {
157
+ if (f.startsWith('.') || f === 'node_modules') return;
158
+ const p = path.join(dir, f);
159
+ const stat = fs.statSync(p);
160
+ if (stat.isDirectory()) scan(p, depth + 1);
161
+ else if (f.endsWith('.jos')) list.push({ path: p, dir });
162
+ });
163
+ } catch (e) { }
164
+ };
165
+ dirs.forEach(d => { if (fs.existsSync(d)) scan(d); });
166
+ return list;
167
+ }
168
+
169
+ function formatUptime(ms) {
170
+ const s = Math.floor(ms / 1000);
171
+ const m = Math.floor(s / 60);
172
+ const h = Math.floor(m / 60);
173
+ if (h > 0) return `${h}h ${m % 60}m`;
174
+ if (m > 0) return `${m}m ${s % 60}s`;
175
+ return `${s}s`;
176
+ }
177
+
178
+ // --- HTML TEMPLATE ---
179
+ const HTML = (title, content, activeTab = '') => `
180
+ <!DOCTYPE html>
181
+ <html><head><meta charset="utf-8"><title>JOS // ${title}</title>
182
+ <style>${STYLES}</style>
183
+ <meta http-equiv="refresh" content="30">
184
+ </head><body>
185
+ <div class="nav">
186
+ <a href="/" class="${activeTab === 'home' ? 'active' : ''}">🏠 Home</a>
187
+ <a href="/library" class="${activeTab === 'library' ? 'active' : ''}">📚 Library</a>
188
+ <a href="/clones" class="${activeTab === 'clones' ? 'active' : ''}">👻 Clones</a>
189
+ <a href="/stats" class="${activeTab === 'stats' ? 'active' : ''}">📊 Stats</a>
190
+ <a href="/about" class="${activeTab === 'about' ? 'active' : ''}">🦊 About</a>
191
+ </div>
192
+ ${BRAND_ART}
193
+ ${content}
194
+ <div style="margin-top:50px; text-align:center; font-size:0.8rem; color:#8b949e; border-top:1px solid #30363d; padding-top:20px">
195
+ JOS Open Solutions Foundation — Kernel v1.0
196
+ </div>
197
+ </body></html>`;
198
+
199
+ // --- SERVER STATE ---
200
+ const serverState = {
201
+ startTime: Date.now(),
202
+ requestCount: 0,
203
+ lastRequest: null
204
+ };
205
+
206
+ // --- MAIN EXECUTE ---
207
+ exports.execute = async (args, home) => {
208
+ // Help handler
209
+ if (args.includes('--help') || args.includes('-h')) {
210
+ console.log(`
211
+ ${C.cyan}${C.bold}JOS SERVE${C.reset} - Development server & artifact host
212
+
213
+ ${C.white}Usage:${C.reset} jos serve [options]
214
+
215
+ ${C.white}Options:${C.reset}
216
+ --port <n> Set port (default: 1111, auto-hunts if busy)
217
+ --detach, -d Run as background shadow clone
218
+ --help, -h Show this help
219
+
220
+ ${C.white}Features:${C.reset}
221
+ 📚 Library Browse .jos artifacts with auto-documentation
222
+ 👻 Clones Shadow clone management (PID tracking)
223
+ 📊 Stats Server statistics and system info
224
+ 🦊 About Kernel info, commands, known issues
225
+
226
+ ${C.white}Dashboard:${C.reset}
227
+ Home http://localhost:<port>/
228
+ Library http://localhost:<port>/library
229
+ Studio http://localhost:<port>/studio?file=<path>
230
+ Clones http://localhost:<port>/clones
231
+ Stats http://localhost:<port>/stats
232
+ About http://localhost:<port>/about
233
+
234
+ ${C.white}As Repository:${C.reset}
235
+ Any jos serve instance becomes a package repository:
236
+ jos repo add myrepo http://192.168.1.10:1111
237
+ jos get myrepo:package-name
238
+
239
+ ${C.white}Examples:${C.reset}
240
+ jos serve # Start on port 1111
241
+ jos serve --port 8080 # Start on port 8080
242
+ jos server # Alias for serve
243
+ `);
244
+ return;
245
+ }
246
+
247
+ // Handle --detach flag (spawn as shadow clone)
248
+ if (args.includes('--detach') || args.includes('-d')) {
249
+ const { spawn } = require('child_process');
250
+ const newArgs = args.filter(a => a !== '--detach' && a !== '-d');
251
+
252
+ const child = spawn(process.execPath, [process.argv[1], 'serve', ...newArgs], {
253
+ detached: true,
254
+ stdio: 'ignore',
255
+ cwd: process.cwd()
256
+ });
257
+
258
+ child.unref();
259
+ console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
260
+ console.log(`${C.cyan}👻 Shadow Clone Spawned!${C.reset}`);
261
+ console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
262
+ console.log(`${C.white} PID:${C.reset} ${child.pid}`);
263
+ console.log(`${C.white} Dir:${C.reset} ${process.cwd()}`);
264
+ console.log(`${C.dim} View clones: http://localhost:1111/clones${C.reset}`);
265
+ console.log(`${C.dim} To kill: kill ${child.pid}${C.reset}\n`);
266
+ return;
267
+ }
268
+
269
+ // Support both --port=1112 and --port 1112 formats
270
+ let port = 1111;
271
+ const portEqArg = args.find(a => a.startsWith('--port='));
272
+ const portFlagIdx = args.indexOf('--port');
273
+ if (portEqArg) {
274
+ port = parseInt(portEqArg.split('=')[1]);
275
+ } else if (portFlagIdx !== -1 && args[portFlagIdx + 1]) {
276
+ port = parseInt(args[portFlagIdx + 1]);
277
+ }
278
+ const root = process.cwd();
279
+
280
+ // Auto port hunting - try ports until one is available
281
+ const MAX_PORT_ATTEMPTS = 10;
282
+ let actualPort = port;
283
+ let portFree = await isPortAvailable(actualPort);
284
+
285
+ if (!portFree) {
286
+ console.log(`${C.gray}🔍 Port ${actualPort} busy, hunting for available port...${C.reset}`);
287
+ for (let i = 1; i < MAX_PORT_ATTEMPTS; i++) {
288
+ actualPort = port + i;
289
+ portFree = await isPortAvailable(actualPort);
290
+ if (portFree) {
291
+ console.log(`${C.green}✓ Found available port: ${actualPort}${C.reset}`);
292
+ break;
293
+ }
294
+ }
295
+ }
296
+
297
+ if (!portFree) {
298
+ const clones = getShadowClones(home);
299
+ const serveClones = clones.filter(c => c.command === 'serve' && c.alive);
300
+ console.log(`${C.magenta}✖ All ports ${port}-${port + MAX_PORT_ATTEMPTS - 1} are in use!${C.reset}`);
301
+ if (serveClones.length > 0) {
302
+ console.log(`${C.cyan}👻 Active JOS Shadow Clones:${C.reset}`);
303
+ serveClones.forEach(c => {
304
+ console.log(` PID ${c.meta.pid} - Port ${c.port || '?'}`);
305
+ });
306
+ console.log(`\n${C.dim}To kill: kill ${serveClones[0].meta.pid}${C.reset}`);
307
+ }
308
+ process.exit(1);
309
+ }
310
+
311
+ port = actualPort;
312
+
313
+ // Setup dirs
314
+ const publicDir = path.join(home, 'public');
315
+ const runsDir = path.join(home, 'runs');
316
+ if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir, { recursive: true });
317
+ if (!fs.existsSync(runsDir)) fs.mkdirSync(runsDir, { recursive: true });
318
+
319
+ // LOG ROTATION: Keep only 50 most recent runs
320
+ const MAX_RUNS = 50;
321
+ const allRuns = fs.readdirSync(runsDir).sort();
322
+ if (allRuns.length > MAX_RUNS) {
323
+ const toDelete = allRuns.slice(0, allRuns.length - MAX_RUNS);
324
+ toDelete.forEach(old => {
325
+ fs.rmSync(path.join(runsDir, old), { recursive: true, force: true });
326
+ });
327
+ console.log(`${C.dim}🧹 Log rotation: cleaned ${toDelete.length} old runs${C.reset}`);
328
+ }
329
+
330
+ // Write our own run record
331
+ const epochId = new Date().toISOString().replace(/[:.]/g, '-');
332
+ const runDir = path.join(runsDir, epochId);
333
+ fs.mkdirSync(runDir, { recursive: true });
334
+ fs.writeFileSync(path.join(runDir, 'report.json'), JSON.stringify({
335
+ meta: { pid: process.pid, timestamp: new Date().toISOString() },
336
+ command: 'serve',
337
+ status: 'RUNNING',
338
+ port
339
+ }, null, 2));
340
+
341
+ const ips = getIPs();
342
+ console.log(getKitsuneBanner());
343
+ console.log(`\n${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}`);
344
+ console.log(` ${C.cyan}📡${C.reset} ${C.white}Local:${C.reset} ${C.cyan}http://localhost:${port}${C.reset}`);
345
+ if (ips.length > 0) console.log(` ${C.magenta}🌐${C.reset} ${C.white}Network:${C.reset} ${C.magenta}http://${ips[0]}:${port}${C.reset}`);
346
+ console.log(` ${C.blue}📂${C.reset} ${C.white}Root:${C.reset} ${C.gray}${root}${C.reset}`);
347
+ console.log(` ${C.purple}🆔${C.reset} ${C.white}PID:${C.reset} ${C.purple}${process.pid}${C.reset}`);
348
+ console.log(`${C.purple}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${C.reset}\n`);
349
+
350
+ http.createServer((req, res) => {
351
+ serverState.requestCount++;
352
+ serverState.lastRequest = new Date();
353
+
354
+ // Use modern URL API (not deprecated url.parse)
355
+ const reqUrl = new URL(req.url, `http://${req.headers.host}`);
356
+ const pathname = decodeURIComponent(reqUrl.pathname);
357
+ const query = Object.fromEntries(reqUrl.searchParams);
358
+
359
+ // --- ABOUT PAGE ---
360
+ if (pathname === '/about') {
361
+ const auroraColors = ['#af7ac5', '#ff6b9d', '#00ffff', '#5dade2', '#f8b4d9', '#48c9b0'];
362
+ const randomColor = auroraColors[Math.floor(Math.random() * auroraColors.length)];
363
+
364
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
365
+ res.end(HTML('About', `
366
+ <div style="text-align:center">
367
+ <pre style="font-size:9px;line-height:9px;color:${randomColor};">
368
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⡀⠀⠀⠀
369
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⠙⠻⢶⣄⡀⠀⠀⠀⢀⣤⠶⠛⠛⡇⠀⠀⠀
370
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠀⣙⣿⣦⣤⣴⣿⣁⠀⠀⣸⠇⠀⠀⠀
371
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣡⣾⣿⣿⣿⣿⣿⣿⣿⣷⣌⠋⠀⠀⠀⠀
372
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣴⣿⣷⣄⡈⢻⣿⡟⢁⣠⣾⣿⣦⠀⠀⠀⠀
373
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⠘⣿⠃⣿⣿⣿⣿⡏⠀⠀⠀⠀
374
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠀⠈⠛⣰⠿⣆⠛⠁⠀⡀⠀⠀⠀⠀⠀
375
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣼⣿⣦⠀⠘⠛⠋⠀⣴⣿⠁⠀⠀⠀⠀⠀
376
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣤⣶⣾⣿⣿⣿⣿⡇⠀⠀⠀⢸⣿⣏⠀⠀⠀⠀⠀⠀
377
+ ⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠀⠀⠀⠾⢿⣿⠀⠀⠀⠀⠀⠀
378
+ </pre>
379
+ <h1 style="color:var(--accent)">JOS Kernel v2.2-beta</h1>
380
+ <p style="color:#8b949e">Stoic Architecture // Made with ❤️ & AI</p>
381
+ </div>
382
+
383
+ <div class="grid" style="margin-top:30px">
384
+ <div class="card">
385
+ <h3>📋 Commands</h3>
386
+ <table>
387
+ <tr><td><code>jos serve</code></td><td>Start development server</td></tr>
388
+ <tr><td><code>jos run &lt;file.jos&gt;</code></td><td>Execute .jos artifacts</td></tr>
389
+ <tr><td><code>jos get &lt;pkg&gt;</code></td><td>Fetch packages</td></tr>
390
+ <tr><td><code>jos secrets</code></td><td>Manage credentials</td></tr>
391
+ </table>
392
+ </div>
393
+ <div class="card">
394
+ <h3>📦 Repositories</h3>
395
+ <table>
396
+ <tr><td>Default</td><td><code>https://registry.josfox.ai</code></td></tr>
397
+ <tr><td>Local</td><td><code>~/.jos/artifacts</code></td></tr>
398
+ <tr><td>Custom</td><td>Any <code>jos serve</code> instance</td></tr>
399
+ </table>
400
+ <p style="color:#6e7681;font-size:0.85em;margin-top:10px">Config: ~/.jos/repos.json</p>
401
+ </div>
402
+ </div>
403
+
404
+ <div class="card" style="margin-top:20px">
405
+ <h3>✨ Features v1.0</h3>
406
+ <ul style="color:#8b949e">
407
+ <li>🦊 Kitsune fox with Aurora colors</li>
408
+ <li>🔐 SHA-256 integrity + lock.json</li>
409
+ <li>🛡️ Path traversal protection + AES-256 secrets</li>
410
+ <li>📡 Auto port hunting (1111-1120)</li>
411
+ <li>📚 Clickable library with auto-documentation</li>
412
+ <li>👻 Shadow clones with --detach flag</li>
413
+ <li>📊 Mermaid.js flow diagrams</li>
414
+ <li>🧹 Log rotation (50 runs max)</li>
415
+ <li>📋 JOSFOXAI MAGIC validation</li>
416
+ <li>📦 Full repo management (add/remove/list/default)</li>
417
+ </ul>
418
+ </div>
419
+
420
+ <div class="card" style="margin-top:20px;border-color:var(--success)">
421
+ <h3 style="color:var(--success)">✅ Production Ready</h3>
422
+ <ul style="color:#8b949e">
423
+ <li>All commands implemented with --help</li>
424
+ <li>Unit tests recommended for enterprise deployment</li>
425
+ </ul>
426
+ </div>
427
+ `, 'about'));
428
+ return;
429
+ }
430
+
431
+ // --- STUDIO PAGE (artifact auto-documentation) ---
432
+ if (pathname === '/studio') {
433
+ const filePath = query.file;
434
+ if (!filePath || !fs.existsSync(filePath)) {
435
+ res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' });
436
+ res.end(HTML('Not Found', '<h2>Artifact not found</h2><a href="/library">← Back to Library</a>'));
437
+ return;
438
+ }
439
+
440
+ const content = fs.readFileSync(filePath, 'utf8');
441
+ const integrity = computeIntegrity(filePath);
442
+ let json = {}; try { json = JSON.parse(content); } catch (e) { }
443
+ const meta = json.meta || json._josfox || {};
444
+ const intention = json.intention?.objective || json.description || meta.intention || 'No intention defined';
445
+
446
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
447
+ res.end(HTML(meta.name || path.basename(filePath), `
448
+ <div style="margin-bottom:20px"><a href="/library">← Back to Library</a></div>
449
+ <h2 style="color:var(--accent)">⚡ ${meta.name || path.basename(filePath)}</h2>
450
+ <p style="color:#8b949e;font-size:1.1em">${intention}</p>
451
+
452
+ <div class="card" style="border-color:var(--success)">
453
+ <strong style="color:var(--success)">🔐 Integrity (SHA-256)</strong>
454
+ <code style="margin-left:10px;font-size:0.85em">${integrity}</code>
455
+ </div>
456
+
457
+ ${json.pipelines ? `
458
+ <div class="card" style="margin-top:20px">
459
+ <h3>🔄 Orchestration (Pipelines)</h3>
460
+ ${Object.entries(json.pipelines).map(([name, p]) => {
461
+ const steps = p.steps || [];
462
+ const mermaidNodes = steps.map((s, i) => {
463
+ const nodeName = s.replace('tasks.', '').replace(/[^a-zA-Z0-9_]/g, '_');
464
+ return i < steps.length - 1
465
+ ? `${nodeName}[${s.replace('tasks.', '')}] --> `
466
+ : `${nodeName}[${s.replace('tasks.', '')}]`;
467
+ }).join('');
468
+ return `
469
+ <div style="margin:10px 0;padding:10px;background:#0d1117;border-radius:4px">
470
+ <strong style="color:var(--accent)">${name}</strong>
471
+ <span class="badge" style="margin-left:10px">${steps.length} steps</span>
472
+ <div style="margin-top:8px;font-size:0.85em;color:#8b949e">
473
+ ${steps.map((s, i) => `<span style="margin-right:15px">${i + 1}. ${s}</span>`).join('')}
474
+ </div>
475
+ ${steps.length > 1 ? `
476
+ <div style="margin-top:15px;background:#161b22;padding:15px;border-radius:4px">
477
+ <div style="font-size:0.75em;color:var(--accent);margin-bottom:10px">📊 Flow Diagram:</div>
478
+ <div class="mermaid" style="background:transparent">${'graph LR\\n ' + mermaidNodes}</div>
479
+ </div>` : ''}
480
+ </div>`;
481
+ }).join('')}
482
+ </div>
483
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
484
+ <script>mermaid.initialize({startOnLoad:true,theme:'dark'});</script>
485
+ ` : ''}
486
+
487
+ ${json.tasks ? `
488
+ <div class="card" style="margin-top:20px">
489
+ <h3>📋 Tasks</h3>
490
+ <table>
491
+ ${Object.entries(json.tasks).map(([name, t]) => `
492
+ <tr>
493
+ <td><code>${name}</code></td>
494
+ <td style="color:#8b949e">${t.description || ''}</td>
495
+ </tr>
496
+ `).join('')}
497
+ </table>
498
+ </div>` : ''}
499
+
500
+ ${json.requirements ? `
501
+ <div class="card" style="margin-top:20px">
502
+ <h3>📦 Dependencies & Requirements</h3>
503
+ <pre style="background:#0d1117;padding:15px;border-radius:4px;overflow-x:auto">${JSON.stringify(json.requirements, null, 2)}</pre>
504
+ </div>` : ''}
505
+
506
+ <div class="card" style="margin-top:20px">
507
+ <h3>📄 Raw JSON</h3>
508
+ <pre style="background:#0d1117;padding:15px;border-radius:4px;overflow-x:auto;max-height:400px">${JSON.stringify(json, null, 2)}</pre>
509
+ </div>
510
+
511
+ <div style="margin-top:20px;padding:15px;background:#161b22;border-radius:4px">
512
+ <strong>▶ Run this artifact:</strong>
513
+ <code style="margin-left:10px;color:var(--accent)">jos run "${filePath}"</code>
514
+ </div>
515
+ `));
516
+ return;
517
+ }
518
+
519
+ // --- EPOCH DETAILS PAGE ---
520
+ if (pathname.startsWith('/epoch/')) {
521
+ const epochId = pathname.replace('/epoch/', '');
522
+ const epochDir = path.join(home, 'runs', epochId);
523
+ const reportPath = path.join(epochDir, 'report.json');
524
+
525
+ if (!fs.existsSync(reportPath)) {
526
+ res.writeHead(404, { 'Content-Type': 'text/html;charset=utf-8' });
527
+ res.end(HTML('Epoch Not Found', '<h2>Epoch not found</h2><a href="/clones">← Back to Clones</a>'));
528
+ return;
529
+ }
530
+
531
+ const report = JSON.parse(fs.readFileSync(reportPath));
532
+ let isAlive = false;
533
+ try { process.kill(report.meta?.pid, 0); isAlive = true; } catch (e) { }
534
+
535
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
536
+ res.end(HTML(`Epoch ${epochId.substring(0, 16)}`, `
537
+ <div style="margin-bottom:20px"><a href="/clones">← Back to Clones</a></div>
538
+ <h2>📜 Epoch Details</h2>
539
+ <span class="badge ${isAlive ? 'badge-green' : 'badge-yellow'}">${isAlive ? '● RUNNING' : '○ ENDED'}</span>
540
+
541
+ <div class="card" style="margin-top:20px">
542
+ <table>
543
+ <tr><th>Epoch ID</th><td><code>${epochId}</code></td></tr>
544
+ <tr><th>PID</th><td>${report.meta?.pid || 'N/A'}</td></tr>
545
+ <tr><th>Command</th><td>${report.command || 'unknown'}</td></tr>
546
+ <tr><th>Port</th><td>${report.port || '-'}</td></tr>
547
+ <tr><th>Status</th><td>${report.status || 'unknown'}</td></tr>
548
+ <tr><th>Started</th><td>${report.meta?.timestamp || 'unknown'}</td></tr>
549
+ </table>
550
+ </div>
551
+
552
+ <div class="card" style="margin-top:20px">
553
+ <h3>📄 Full Report</h3>
554
+ <pre style="background:#0d1117;padding:15px;border-radius:4px">${JSON.stringify(report, null, 2)}</pre>
555
+ </div>
556
+
557
+ ${isAlive ? `
558
+ <div style="margin-top:20px;padding:15px;background:#161b22;border-radius:4px">
559
+ <strong>⚡ To stop this process:</strong>
560
+ <code style="margin-left:10px;color:var(--warning)">kill ${report.meta?.pid}</code>
561
+ </div>` : ''}
562
+ `));
563
+ return;
564
+ }
565
+
566
+ if (pathname === '/stats') {
567
+ const uptime = Date.now() - serverState.startTime;
568
+ const mem = process.memoryUsage();
569
+ const clones = getShadowClones(home);
570
+
571
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
572
+ res.end(HTML('Stats', `
573
+ <h2>📊 Server Statistics</h2>
574
+ <div class="grid">
575
+ <div class="card stat-box">
576
+ <div class="stat-value">${formatUptime(uptime)}</div>
577
+ <div class="stat-label">Uptime</div>
578
+ </div>
579
+ <div class="card stat-box">
580
+ <div class="stat-value">${serverState.requestCount}</div>
581
+ <div class="stat-label">Requests</div>
582
+ </div>
583
+ <div class="card stat-box">
584
+ <div class="stat-value">${Math.round(mem.heapUsed / 1024 / 1024)}MB</div>
585
+ <div class="stat-label">Memory</div>
586
+ </div>
587
+ <div class="card stat-box">
588
+ <div class="stat-value">${clones.filter(c => c.alive).length}</div>
589
+ <div class="stat-label">Active Clones</div>
590
+ </div>
591
+ </div>
592
+ <div class="card" style="margin-top:20px">
593
+ <h3>System Info</h3>
594
+ <table>
595
+ <tr><th>PID</th><td>${process.pid}</td></tr>
596
+ <tr><th>Port</th><td>${port}</td></tr>
597
+ <tr><th>Node</th><td>${process.version}</td></tr>
598
+ <tr><th>Platform</th><td>${os.platform()} ${os.arch()}</td></tr>
599
+ <tr><th>Hostname</th><td>${os.hostname()}</td></tr>
600
+ <tr><th>Started</th><td>${new Date(serverState.startTime).toISOString()}</td></tr>
601
+ </table>
602
+ </div>
603
+ `, 'stats'));
604
+ return;
605
+ }
606
+
607
+ // --- CLONES PAGE ---
608
+ if (pathname === '/clones') {
609
+ const clones = getShadowClones(home);
610
+ const aliveCount = clones.filter(c => c.alive).length;
611
+ const endedCount = clones.filter(c => !c.alive).length;
612
+
613
+ const rows = clones.map(c => `
614
+ <tr>
615
+ <td><span class="badge ${c.alive ? 'badge-green' : 'badge-yellow'}">${c.alive ? '● RUNNING' : '○ ENDED'}</span></td>
616
+ <td>${c.meta?.pid || 'N/A'}</td>
617
+ <td>${c.command || 'unknown'}</td>
618
+ <td>${c.port || '-'}</td>
619
+ <td><a href="/epoch/${c.epochId}" style="color:var(--accent)">${c.epochId.substring(0, 16)}...</a></td>
620
+ <td>${c.alive ? `<code>kill ${c.meta?.pid}</code>` : '-'}</td>
621
+ </tr>
622
+ `).join('');
623
+
624
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
625
+ res.end(HTML('Shadow Clones', `
626
+ <h2>👻 Shadow Clone Management</h2>
627
+ <div class="grid" style="grid-template-columns:repeat(3,1fr);margin-bottom:20px">
628
+ <div class="card stat-box" style="border-color:var(--success)">
629
+ <div class="stat-value" style="color:var(--success)">${aliveCount}</div>
630
+ <div class="stat-label">Running</div>
631
+ </div>
632
+ <div class="card stat-box">
633
+ <div class="stat-value">${endedCount}</div>
634
+ <div class="stat-label">Ended</div>
635
+ </div>
636
+ <div class="card stat-box">
637
+ <div class="stat-value">${clones.length}</div>
638
+ <div class="stat-label">Total Epochs</div>
639
+ </div>
640
+ </div>
641
+ <div class="card">
642
+ <table>
643
+ <tr><th>Status</th><th>PID</th><th>Command</th><th>Port</th><th>Epoch (click for details)</th><th>Action</th></tr>
644
+ ${rows || '<tr><td colspan="6" style="text-align:center;color:#8b949e">No clones found</td></tr>'}
645
+ </table>
646
+ </div>
647
+ <div style="margin-top:20px; color:#8b949e; font-size:0.9em">
648
+ 💡 Spawn a clone: <code>jos serve --detach</code>
649
+ </div>
650
+ `, 'clones'));
651
+ return;
652
+ }
653
+
654
+ // --- SECURITY: Kill API removed (was a backdoor!) ---
655
+ // Process management should only be done via CLI, not web UI
656
+ if (pathname.startsWith('/api/')) {
657
+ res.writeHead(403, { 'Content-Type': 'application/json' });
658
+ res.end(JSON.stringify({ error: 'API disabled for security' }));
659
+ return;
660
+ }
661
+
662
+ // --- LIBRARY PAGE ---
663
+ if (pathname === '/library') {
664
+ const files = scanJosFiles([root, path.join(home, 'artifacts'), home]);
665
+ const cards = files.map(f => {
666
+ let meta = { name: path.basename(f.path) };
667
+ let fullJson = {};
668
+ try { fullJson = JSON.parse(fs.readFileSync(f.path)); meta = fullJson.meta || fullJson._josfox || meta; } catch (e) { }
669
+ const rel = f.path.startsWith(root) ? path.relative(root, f.path) : f.path;
670
+ const isHome = f.path.startsWith(home);
671
+ const studioUrl = `/studio?file=${encodeURIComponent(f.path)}`;
672
+ const intention = fullJson.intention?.objective || fullJson.description || meta.intention || '';
673
+ return `<a href="${studioUrl}" class="card" style="display:block;text-decoration:none;">
674
+ <div style="display:flex;justify-content:space-between;align-items:center">
675
+ <strong style="color:#00ffff">⚡ ${meta.name || path.basename(f.path)}</strong>
676
+ ${isHome ? '<span class="badge">~/.jos</span>' : ''}
677
+ </div>
678
+ <div style="font-size:0.8em; color:#8b949e; margin:5px 0">${rel}</div>
679
+ <div style="font-size:0.75em; color:#6e7681">${intention}</div>
680
+ </a>`;
681
+ }).join('');
682
+
683
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
684
+ res.end(HTML('Library', `
685
+ <h2>📚 .jos Library</h2>
686
+ <p style="color:#8b949e">Click any artifact to view documentation, orchestration, and run options.</p>
687
+ <p style="color:#6e7681;font-size:0.85em">Scanning: <code>${root}</code> + <code>~/.jos/</code></p>
688
+ <div class="grid">${cards || '<p>No .jos files found</p>'}</div>
689
+ `, 'library'));
690
+ return;
691
+ }
692
+
693
+ // --- STUDIO VIEW (.jos files) ---
694
+ const target = path.join(root, pathname.replace(/^\//, ''));
695
+
696
+ // SECURITY: Path jail check
697
+ if (!isPathSafe(target, [root, home])) {
698
+ res.writeHead(403, { 'Content-Type': 'text/html;charset=utf-8' });
699
+ res.end(HTML('Forbidden', `<h2>🔒 Access Denied</h2><p>Path traversal blocked.</p>`));
700
+ return;
701
+ }
702
+
703
+ if (fs.existsSync(target) && target.endsWith('.jos') && !query.raw) {
704
+ const content = fs.readFileSync(target, 'utf8');
705
+ let json = {}; try { json = JSON.parse(content); } catch (e) { }
706
+ const integrity = computeIntegrity(target);
707
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
708
+ res.end(HTML(json.meta?.name || 'Studio', `
709
+ <div style="margin-bottom:20px"><a href="/library">← Back to Library</a></div>
710
+ <h2 style="color:var(--accent)">⚡ ${json.meta?.name || path.basename(target)}</h2>
711
+ <p style="color:#8b949e">${json.meta?.intention || json.intention?.objective || 'No intention defined'}</p>
712
+ <div class="card" style="border-color:var(--success)">
713
+ <strong style="color:var(--success)">🔐 Integrity (SHA-256):</strong>
714
+ <code style="font-size:0.8em;margin-left:10px">${integrity.substring(0, 16)}...${integrity.substring(48)}</code>
715
+ </div>
716
+ ${json.pipelines ? `<div class="card"><h3>🔄 Orchestration</h3><ul>${Object.keys(json.pipelines).map(p => `<li><strong>${p}</strong>: ${json.pipelines[p].steps?.length || 0} steps</li>`).join('')}</ul></div>` : ''}
717
+ ${json.tasks ? `<div class="card"><h3>📋 Tasks</h3><ul>${Object.keys(json.tasks).map(t => `<li><strong>${t}</strong>: ${json.tasks[t].description || ''}</li>`).join('')}</ul></div>` : ''}
718
+ ${json.requirements ? `<div class="card"><h3>📦 Dependencies</h3><pre>${JSON.stringify(json.requirements, null, 2)}</pre></div>` : ''}
719
+ <div class="card"><h3>📄 Raw JSON</h3><pre>${JSON.stringify(json, null, 2)}</pre></div>
720
+ `));
721
+ return;
722
+ }
723
+
724
+ // --- FILE EXPLORER (Default) ---
725
+ if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
726
+ const files = fs.readdirSync(target).map(f => {
727
+ const isJos = f.endsWith('.jos');
728
+ const link = path.join(pathname, f);
729
+ return `<li style="padding:5px">
730
+ <a href="${link}" style="${isJos ? 'color:#00ffff;font-weight:bold' : ''}">${f}</a>
731
+ ${isJos ? '<span class="badge" style="margin-left:8px">JOS</span>' : ''}
732
+ </li>`;
733
+ }).join('');
734
+
735
+ const clones = getShadowClones(home).filter(c => c.alive);
736
+ const cloneAlert = clones.length > 0 ? `
737
+ <div class="card" style="border-color:var(--warning);background:rgba(210,153,34,0.1);margin-bottom:20px">
738
+ <strong style="color:var(--warning)">👻 ${clones.length} Shadow Clone${clones.length > 1 ? 's' : ''} Active</strong>
739
+ <a href="/clones" style="margin-left:10px;color:var(--accent)">Manage →</a>
740
+ </div>
741
+ ` : '';
742
+
743
+ res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
744
+ res.end(HTML('Explorer', `
745
+ ${cloneAlert}
746
+ <div class="card" style="border-color:#3fb950; background:rgba(63,185,80,0.1)">
747
+ <strong style="color:#3fb950">🚀 Share JOS:</strong>
748
+ <code style="margin-left:10px">curl -L http://${req.headers.host}/__public/install.sh | bash</code>
749
+ </div>
750
+ <h2>📂 Explorer: ${pathname}</h2>
751
+ <ul style="list-style:none;padding:0">${files}</ul>
752
+ `, 'home'));
753
+ return;
754
+ }
755
+
756
+ // --- Static Files ---
757
+ if (fs.existsSync(target)) {
758
+ const ext = path.extname(target);
759
+ const mimes = { '.html': 'text/html', '.json': 'application/json', '.js': 'text/javascript', '.css': 'text/css' };
760
+ res.writeHead(200, { 'Content-Type': mimes[ext] || 'text/plain' });
761
+ fs.createReadStream(target).pipe(res);
762
+ return;
763
+ }
764
+
765
+ res.writeHead(404);
766
+ res.end(HTML('404', '<h2>404 // Not Found</h2><p>The requested resource does not exist.</p>'));
767
+
768
+ }).listen(port);
769
+
770
+ // Cleanup on exit
771
+ process.on('SIGINT', () => {
772
+ fs.writeFileSync(path.join(runDir, 'report.json'), JSON.stringify({
773
+ meta: { pid: process.pid, timestamp: new Date().toISOString() },
774
+ command: 'serve',
775
+ status: 'STOPPED',
776
+ port
777
+ }, null, 2));
778
+ process.exit(0);
779
+ });
780
+ };