@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/README.md +273 -35
- package/bin/jos +76 -0
- package/package.json +21 -49
- package/src/commands/get.js +245 -0
- package/src/commands/repo.js +139 -0
- package/src/commands/run.js +225 -0
- package/src/commands/secrets.js +137 -0
- package/src/index.js +0 -0
- package/src/serve.js +780 -0
- package/LICENSE +0 -20
- package/NOTICE +0 -4
- package/THIRD_PARTY_NOTICES.md +0 -11
- package/bin/jos.js +0 -36
- package/examples/env-check.json +0 -39
- package/lib/resolve.js +0 -32
- package/lib/run.js +0 -43
- package/lib/serve.js +0 -6
- package/lib/validate.js +0 -11
- package/schemas/jos.v0.3.1.schema.json +0 -64
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 <file.jos></code></td><td>Execute .jos artifacts</td></tr>
|
|
389
|
+
<tr><td><code>jos get <pkg></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
|
+
};
|