kodelyth-ecc 1.5.9 → 1.7.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/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +455 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +286 -4
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// scripts/dashboard/server.js
|
|
2
|
+
//
|
|
3
|
+
// Local observability dashboard server (v1.7.0).
|
|
4
|
+
//
|
|
5
|
+
// HTTP server using ONLY Node.js built-ins. Serves:
|
|
6
|
+
// GET / → static index.html
|
|
7
|
+
// GET /api/overview → counts + storage paths
|
|
8
|
+
// GET /api/memory → stats + recent captures
|
|
9
|
+
// GET /api/memory/search?q=… → BM25 search
|
|
10
|
+
// GET /api/evolve → reuse + miss + proposals
|
|
11
|
+
// GET /api/catalog?kind=…&q=… → agents | skills | commands | rules | bundles
|
|
12
|
+
// GET /api/sessions → orchestration session list
|
|
13
|
+
// GET /api/sessions/:name → single session detail
|
|
14
|
+
// GET /api/token-budget → per-session token usage
|
|
15
|
+
// GET /api/health → tiny liveness probe
|
|
16
|
+
//
|
|
17
|
+
// Default bind: 127.0.0.1 (localhost ONLY). Refuses to bind 0.0.0.0 unless
|
|
18
|
+
// the caller explicitly opts in. Zero telemetry. No external deps.
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const http = require('http');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const { execFileSync } = require('child_process');
|
|
26
|
+
|
|
27
|
+
const data = require('./data.js');
|
|
28
|
+
|
|
29
|
+
// ── SSE (Server-Sent Events) — real-time push to connected browsers ───────────
|
|
30
|
+
|
|
31
|
+
const sseClients = new Set();
|
|
32
|
+
const MAX_SSE_CLIENTS = 10;
|
|
33
|
+
|
|
34
|
+
// Key files that signal data changes when their mtime moves.
|
|
35
|
+
const WATCH_PATHS = [
|
|
36
|
+
path.join(os.homedir(), '.kodelyth', 'memory', 'memories.jsonl'),
|
|
37
|
+
path.join(os.homedir(), '.kodelyth', 'evolve', 'reuse.json'),
|
|
38
|
+
path.join(os.homedir(), '.kodelyth', 'evolve', 'proposals.jsonl'),
|
|
39
|
+
];
|
|
40
|
+
const lastMtimes = new Map();
|
|
41
|
+
const FILE_CHECK_MS = 10000; // check every 10 s
|
|
42
|
+
|
|
43
|
+
function broadcastSSE(obj) {
|
|
44
|
+
const msg = `data: ${JSON.stringify(obj)}\n\n`;
|
|
45
|
+
for (const client of sseClients) {
|
|
46
|
+
try { client.write(msg); } catch { sseClients.delete(client); }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let _fileWatchTimer = null;
|
|
51
|
+
|
|
52
|
+
function startFileWatcher() {
|
|
53
|
+
if (_fileWatchTimer) return; // already running
|
|
54
|
+
// Prime mtimes so the first tick doesn't fire 'data-changed' spuriously.
|
|
55
|
+
for (const p of WATCH_PATHS) {
|
|
56
|
+
try { lastMtimes.set(p, fs.statSync(p).mtimeMs); } catch { /* file not yet present */ }
|
|
57
|
+
}
|
|
58
|
+
_fileWatchTimer = setInterval(() => {
|
|
59
|
+
if (sseClients.size === 0) return; // no connected browsers — skip stat I/O
|
|
60
|
+
let changed = false;
|
|
61
|
+
for (const p of WATCH_PATHS) {
|
|
62
|
+
try {
|
|
63
|
+
const m = fs.statSync(p).mtimeMs;
|
|
64
|
+
if (lastMtimes.get(p) !== m) { changed = true; lastMtimes.set(p, m); }
|
|
65
|
+
} catch { /* file doesn't exist yet — that's fine */ }
|
|
66
|
+
}
|
|
67
|
+
broadcastSSE({ type: changed ? 'data-changed' : 'heartbeat', time: new Date().toISOString() });
|
|
68
|
+
}, FILE_CHECK_MS);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stopFileWatcher() {
|
|
72
|
+
if (_fileWatchTimer) { clearInterval(_fileWatchTimer); _fileWatchTimer = null; }
|
|
73
|
+
}
|
|
74
|
+
process.on('exit', stopFileWatcher);
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const STATIC_DIR = path.join(__dirname, 'static');
|
|
79
|
+
|
|
80
|
+
const MIME = {
|
|
81
|
+
'.html': 'text/html; charset=utf-8',
|
|
82
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
83
|
+
'.css': 'text/css; charset=utf-8',
|
|
84
|
+
'.svg': 'image/svg+xml',
|
|
85
|
+
'.json': 'application/json; charset=utf-8',
|
|
86
|
+
'.ico': 'image/x-icon',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function jsonResponse(res, statusCode, payload) {
|
|
90
|
+
const body = JSON.stringify(payload, null, 2);
|
|
91
|
+
res.writeHead(statusCode, {
|
|
92
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
93
|
+
'Content-Length': Buffer.byteLength(body),
|
|
94
|
+
// Reinforce localhost-only posture: refuse to be embedded in foreign frames.
|
|
95
|
+
'Content-Security-Policy': "default-src 'none'",
|
|
96
|
+
'X-Frame-Options': 'DENY',
|
|
97
|
+
'X-Content-Type-Options': 'nosniff',
|
|
98
|
+
'Referrer-Policy': 'no-referrer',
|
|
99
|
+
// Dashboard is read-only; no CORS allowed.
|
|
100
|
+
'Cache-Control': 'no-store',
|
|
101
|
+
});
|
|
102
|
+
res.end(body);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function notFound(res) {
|
|
106
|
+
jsonResponse(res, 404, { ok: false, error: 'not found' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function badRequest(res, msg) {
|
|
110
|
+
jsonResponse(res, 400, { ok: false, error: msg });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function serveStaticFile(res, filePath) {
|
|
114
|
+
fs.readFile(filePath, (err, body) => {
|
|
115
|
+
if (err) return notFound(res);
|
|
116
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
117
|
+
res.writeHead(200, {
|
|
118
|
+
'Content-Type': MIME[ext] || 'application/octet-stream',
|
|
119
|
+
'Content-Length': body.length,
|
|
120
|
+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self';",
|
|
121
|
+
'X-Frame-Options': 'DENY',
|
|
122
|
+
'X-Content-Type-Options': 'nosniff',
|
|
123
|
+
'Referrer-Policy': 'no-referrer',
|
|
124
|
+
'Cache-Control': 'no-store',
|
|
125
|
+
});
|
|
126
|
+
res.end(body);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Defensive — block path traversal, only allow files under STATIC_DIR.
|
|
131
|
+
function resolveStatic(reqPath) {
|
|
132
|
+
const decoded = decodeURIComponent(reqPath.replace(/^\/+/, ''));
|
|
133
|
+
if (decoded.includes('..')) return null;
|
|
134
|
+
if (decoded === '' || decoded === '/') return path.join(STATIC_DIR, 'index.html');
|
|
135
|
+
const abs = path.resolve(STATIC_DIR, decoded);
|
|
136
|
+
if (!abs.startsWith(STATIC_DIR + path.sep) && abs !== path.join(STATIC_DIR, 'index.html')) return null;
|
|
137
|
+
return abs;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── route handlers ───────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function handleRequest(req, res) {
|
|
143
|
+
// DNS-rebinding defence: only respond to requests targeting localhost.
|
|
144
|
+
const reqHost = (req.headers.host || '').split(':')[0];
|
|
145
|
+
if (reqHost !== '' && reqHost !== '127.0.0.1' && reqHost !== 'localhost') {
|
|
146
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
147
|
+
return res.end(JSON.stringify({ ok: false, error: 'bad request' }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Only GET is supported. The dashboard is purely read-only.
|
|
151
|
+
if (req.method !== 'GET') {
|
|
152
|
+
const body = JSON.stringify({ ok: false, error: 'method not allowed' });
|
|
153
|
+
res.writeHead(405, {
|
|
154
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
155
|
+
'Content-Length': Buffer.byteLength(body),
|
|
156
|
+
'Allow': 'GET',
|
|
157
|
+
'Content-Security-Policy':"default-src 'none'",
|
|
158
|
+
'X-Frame-Options': 'DENY',
|
|
159
|
+
'X-Content-Type-Options': 'nosniff',
|
|
160
|
+
'Cache-Control': 'no-store',
|
|
161
|
+
});
|
|
162
|
+
return res.end(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
166
|
+
const p = url.pathname;
|
|
167
|
+
const q = url.searchParams;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// ── API routes ────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
// SSE keep-alive: must be handled before the standard json routes because
|
|
173
|
+
// we never call res.end() here — the connection stays open until the client
|
|
174
|
+
// disconnects or the process exits.
|
|
175
|
+
if (p === '/api/events') {
|
|
176
|
+
if (sseClients.size >= MAX_SSE_CLIENTS) {
|
|
177
|
+
return jsonResponse(res, 503, { ok: false, error: 'too many SSE connections' });
|
|
178
|
+
}
|
|
179
|
+
res.writeHead(200, {
|
|
180
|
+
'Content-Type': 'text/event-stream',
|
|
181
|
+
'Cache-Control': 'no-cache',
|
|
182
|
+
'Connection': 'keep-alive',
|
|
183
|
+
'X-Accel-Buffering': 'no', // prevent nginx from buffering SSE
|
|
184
|
+
'X-Content-Type-Options':'nosniff',
|
|
185
|
+
'X-Frame-Options': 'DENY',
|
|
186
|
+
'Referrer-Policy': 'no-referrer',
|
|
187
|
+
});
|
|
188
|
+
res.flushHeaders();
|
|
189
|
+
res.write(`data: ${JSON.stringify({ type: 'connected', time: new Date().toISOString() })}\n\n`);
|
|
190
|
+
sseClients.add(res);
|
|
191
|
+
req.on('close', () => sseClients.delete(res));
|
|
192
|
+
return; // intentionally no res.end() — keep connection alive
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (p === '/api/health') {
|
|
196
|
+
return jsonResponse(res, 200, { ok: true, time: new Date().toISOString() });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (p === '/api/overview') {
|
|
200
|
+
return jsonResponse(res, 200, data.overview());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (p === '/api/memory') {
|
|
204
|
+
return jsonResponse(res, 200, {
|
|
205
|
+
stats: data.memoryStats(),
|
|
206
|
+
recent: data.recentMemories({ limit: Number(q.get('limit')) || 20 }),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (p === '/api/memory/search') {
|
|
211
|
+
const query = q.get('q') || '';
|
|
212
|
+
if (!query) return badRequest(res, 'query parameter `q` is required');
|
|
213
|
+
return jsonResponse(res, 200, {
|
|
214
|
+
query,
|
|
215
|
+
results: data.memorySearch({ query, limit: Number(q.get('limit')) || 10 }),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (p === '/api/evolve') {
|
|
220
|
+
return jsonResponse(res, 200, data.evolveSnapshot({
|
|
221
|
+
proposalLimit: Math.max(1, Math.min(200, Number(q.get('limit')) || 50)),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (p === '/api/catalog') {
|
|
226
|
+
const kind = q.get('kind');
|
|
227
|
+
if (!kind) return badRequest(res, '`kind` parameter is required: agents | skills | commands | rules | bundles');
|
|
228
|
+
if (!['agents', 'skills', 'commands', 'rules', 'bundles'].includes(kind)) {
|
|
229
|
+
return badRequest(res, `kind must be one of agents | skills | commands | rules | bundles`);
|
|
230
|
+
}
|
|
231
|
+
return jsonResponse(res, 200, {
|
|
232
|
+
kind,
|
|
233
|
+
counts: data.catalogCounts(),
|
|
234
|
+
items: data.catalogList({ kind, query: q.get('q') || '', limit: Number(q.get('limit')) || 50 }),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (p === '/api/sessions') {
|
|
239
|
+
return jsonResponse(res, 200, {
|
|
240
|
+
sessions: data.sessionsList({ limit: Number(q.get('limit')) || 30 }),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sessMatch = p.match(/^\/api\/sessions\/([A-Za-z0-9._-]+)$/);
|
|
245
|
+
if (sessMatch) {
|
|
246
|
+
const sessionName = sessMatch[1];
|
|
247
|
+
if (sessionName === '..' || sessionName === '.') return notFound(res);
|
|
248
|
+
const detail = data.sessionDetail({ session: sessionName });
|
|
249
|
+
if (!detail) return notFound(res);
|
|
250
|
+
return jsonResponse(res, 200, detail);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (p === '/api/token-budget') {
|
|
254
|
+
return jsonResponse(res, 200, data.tokenBudgetSnapshot());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (p.startsWith('/api/')) return notFound(res);
|
|
258
|
+
|
|
259
|
+
// ── static fallback ───────────────────────────────────────────────────
|
|
260
|
+
const filePath = resolveStatic(p);
|
|
261
|
+
if (!filePath) return notFound(res);
|
|
262
|
+
return serveStaticFile(res, filePath);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
process.stderr.write(`[dashboard] route ${p}: ${err.message}\n`);
|
|
265
|
+
return jsonResponse(res, 500, { ok: false, error: 'internal server error' });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── public entry: createServer / start ───────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
function createServer() {
|
|
272
|
+
return http.createServer(handleRequest);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function chooseOpenCommand() {
|
|
276
|
+
switch (process.platform) {
|
|
277
|
+
case 'darwin': return 'open';
|
|
278
|
+
case 'win32': return 'start ""';
|
|
279
|
+
default: return 'xdg-open';
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function tryOpenBrowser(url) {
|
|
284
|
+
try {
|
|
285
|
+
// Use execFileSync (no shell) to prevent shell injection via crafted --host values.
|
|
286
|
+
if (process.platform === 'win32') {
|
|
287
|
+
execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
|
|
288
|
+
} else {
|
|
289
|
+
execFileSync(chooseOpenCommand(), [url], { stdio: 'ignore' });
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
} catch { return false; }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function start({ port = 5747, host = '127.0.0.1', openBrowser = true, log = console.log } = {}) {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
if (host !== '127.0.0.1' && host !== 'localhost' && !process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE) {
|
|
298
|
+
return reject(new Error(
|
|
299
|
+
`[dashboard] refusing to bind host=${host}. Dashboard is localhost-only by default.\n` +
|
|
300
|
+
`To override (UNSAFE — exposes your memory + evolve data), set KODELYTH_DASHBOARD_ALLOW_REMOTE=1.`
|
|
301
|
+
));
|
|
302
|
+
}
|
|
303
|
+
const server = createServer();
|
|
304
|
+
startFileWatcher();
|
|
305
|
+
server.once('close', stopFileWatcher);
|
|
306
|
+
server.once('error', reject);
|
|
307
|
+
server.listen(port, host, () => {
|
|
308
|
+
const addr = server.address();
|
|
309
|
+
const url = `http://${host}:${addr.port}/`;
|
|
310
|
+
log(`✓ Kodelyth ECC dashboard\n ${url}`);
|
|
311
|
+
log(` Press Ctrl+C to stop. Localhost only — zero telemetry.`);
|
|
312
|
+
if (openBrowser) tryOpenBrowser(url);
|
|
313
|
+
resolve({ server, url, port: addr.port });
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
start,
|
|
320
|
+
createServer,
|
|
321
|
+
handleRequest,
|
|
322
|
+
// exposed for tests
|
|
323
|
+
_internals: { resolveStatic, chooseOpenCommand, startFileWatcher, stopFileWatcher },
|
|
324
|
+
};
|