diff --git a/package.json b/package.json
index 6998ead8..cca10388 100644
--- a/package.json
+++ b/package.json
@@ -95,6 +95,7 @@
"scripts/preview-pack-smoke.js",
"scripts/release-approval-gate.js",
"scripts/release-video-suite.js",
+ "scripts/dashboard-web.js",
"scripts/skills-health.js",
"scripts/hooks/",
"scripts/install-apply.js",
@@ -350,7 +351,8 @@
"coverage": "c8 --all --include=\"scripts/**/*.js\" --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 --reporter=text --reporter=lcov node tests/run-all.js",
"build:opencode": "node scripts/build-opencode.js",
"prepack": "npm run build:opencode",
- "dashboard": "python3 ./ecc_dashboard.py"
+ "dashboard": "python3 ./ecc_dashboard.py",
+ "dashboard:web": "node scripts/dashboard-web.js"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
diff --git a/scripts/dashboard-web.js b/scripts/dashboard-web.js
new file mode 100644
index 00000000..011e24da
--- /dev/null
+++ b/scripts/dashboard-web.js
@@ -0,0 +1,775 @@
+#!/usr/bin/env node
+/**
+ * ECC Capabilities Dashboard — agents, skills, commands, MCPs, rules & hooks
+ * With multi-language, routing, search suggestions, recently viewed, fine UI
+ *
+ * Usage: node scripts/dashboard-web.js [port]
+ * Open http://localhost:3456
+ *
+ * Contribution: https://github.com/affaan-m/ECC
+ */
+
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+
+function parsePort(v) {
+ const n = parseInt(String(v), 10);
+ if (isNaN(n) || n < 1 || n > 65535) { console.error('[ECC] Invalid port: ' + v + ' — using 3456'); return 3456; }
+ return n;
+}
+const PORT = parsePort(process.argv[2] || process.env.ECC_DASHBOARD_PORT || '3456');
+const ROOT = path.resolve(__dirname, '..');
+
+function readFrontmatter(p) {
+ try {
+ const c = fs.readFileSync(p, 'utf8');
+ const m = c.match(/^---\n([\s\S]*?)\n---/);
+ if (!m) return {};
+ const fm = {};
+ for (const l of m[1].split('\n')) {
+ const s = l.indexOf(':'); if (s <= 0) continue;
+ let k = l.slice(0, s).trim(), v = l.slice(s + 1).trim();
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
+ if (v.startsWith('[') && v.endsWith(']')) { try { v = JSON.parse(v); } catch { v = v.slice(1, -1).split(',').map(x => x.trim().replace(/["']/g, '')); } }
+ fm[k] = v;
+ }
+ fm._body = c.replace(/^---[\s\S]*?---\n*/, '').trim();
+ return fm;
+ } catch { return {}; }
+}
+function readSkill(p) { try { const c = fs.readFileSync(p, 'utf8'); const fm = readFrontmatter(p); return { d: fm.description || '', b: c.replace(/^---[\s\S]*?---\n*/, '').trim() }; } catch { return { d: '', b: '' }; } }
+
+function loadAgents(_root) {
+ const root = _root || ROOT;
+ const dir = path.join(root, 'agents'); if (!fs.existsSync(dir)) return [];
+ return fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort().map(f => {
+ const fm = readFrontmatter(path.join(dir, f));
+ return { n: fm.name || f.replace('.md', ''), d: fm.description || '', m: fm.model || 'default', t: Array.isArray(fm.tools) ? fm.tools : [], b: (fm._body || '').slice(0, 1200), f };
+ });
+}
+function loadSkills(_root) {
+ const root = _root || ROOT;
+ const dir = path.join(root, 'skills'); if (!fs.existsSync(dir)) return [];
+ return fs.readdirSync(dir).filter(d => { try { return fs.statSync(path.join(dir, d)).isDirectory(); } catch { return false; } }).sort().map(d => {
+ const r = readSkill(path.join(dir, d, 'SKILL.md')); return { n: d, d: r.d, b: r.b.slice(0, 1000) };
+ });
+}
+function loadCommands(_root) {
+ const root = _root || ROOT;
+ const dir = path.join(root, 'commands'); if (!fs.existsSync(dir)) return [];
+ const cm = { plan: 'Planning', 'plan-': 'Planning', 'prp-': 'Git & PR', pr: 'Git & PR', 'review-': 'Review', 'code-': 'Review', build: 'Build', fix: 'Build', test: 'Testing', 'e2e': 'Testing', coverage: 'Testing', quality: 'Testing', session: 'Session', save: 'Session', resume: 'Session', skill: 'Knowledge', learn: 'Knowledge', instinct: 'Knowledge', evolve: 'Knowledge', ecc: 'System', hookify: 'System', model: 'System', setup: 'System', multi: 'Multi-Agent', security: 'Security', harness: 'Security', 'go-': 'Languages', 'rust-': 'Languages', 'cpp-': 'Languages', 'kotlin-': 'Languages', 'flutter-': 'Languages', 'react-': 'Languages', 'python-': 'Languages', 'fastapi-': 'Languages', 'gradle-': 'Languages', gan: 'GAN', marketing: 'Marketing', jira: 'Project', pm2: 'Process', cost: 'Analytics', promote: 'Project', aside: 'Other', santa: 'Fun' };
+ return fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort().map(f => {
+ const fm = readFrontmatter(path.join(dir, f));
+ const n = '/' + f.replace('.md', ''); let c = 'Other';
+ for (const [p, cat] of Object.entries(cm)) if (f.startsWith(p)) { c = cat; break; }
+ return { n, f, d: fm.description || fm['argument-hint'] || '', c, b: (fm._body || '').slice(0, 600) };
+ });
+}
+function loadRules(_root) {
+ const root = _root || ROOT;
+ const dir = path.join(root, 'rules'); if (!fs.existsSync(dir)) return [];
+ return fs.readdirSync(dir).filter(d => { try { return fs.statSync(path.join(dir, d)).isDirectory(); } catch { return false; } }).sort().map(l => ({ l, f: fs.readdirSync(path.join(dir, l)).filter(f => f.endsWith('.md')).sort().map(f => f.replace('.md', '')) }));
+}
+function loadMcps(_root) {
+ const root = _root || ROOT;
+ const r = [];
+ const m = path.join(root, '.mcp.json');
+ if (fs.existsSync(m)) { try { const d = JSON.parse(fs.readFileSync(m, 'utf8')); r.push({ f: '.mcp.json', s: Object.entries(d.mcpServers || {}).map(([k, v]) => ({ n: k, cmd: typeof v === 'object' ? (v.command || v.url || '') : String(v), args: v.args || [], env: v.env ? Object.keys(v.env).reduce((a,k)=>{a[k]='••••••'; return a;}, {}) : {}, type: v.type || 'stdio' })) }); } catch (e) { console.error('[ECC] Failed to parse .mcp.json:', e.message); } }
+ const dir = path.join(root, 'mcp-configs');
+ if (fs.existsSync(dir)) { for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) { try { const d = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); r.push({ f, s: Object.entries(d.mcpServers || {}).map(([k, v]) => ({ n: k, cmd: typeof v === 'object' ? (v.command || v.url || '') : String(v), args: v.args || [], env: v.env ? Object.keys(v.env).reduce((a,k)=>{a[k]='••••••'; return a;}, {}) : {}, type: v.type || 'stdio' })) }); } catch (e) { console.error('[ECC] Failed to parse mcp-configs/' + f + ':', e.message); } } }
+ return r;
+}
+function loadHooks(_root) {
+ const root = _root || ROOT;
+ const p = path.join(root, 'hooks', 'hooks.json'); if (!fs.existsSync(p)) return [];
+ try { const d = JSON.parse(fs.readFileSync(p, 'utf8')); const h = []; for (const [ev, es] of Object.entries(d.hooks || {})) for (const e of es || []) h.push({ ev, m: e.matcher || '*', id: e.id || '', d: e.description || '' }); return h; } catch (e) { console.error('[ECC] Failed to parse hooks/hooks.json:', e.message); return []; }
+}
+
+const LANG = {
+ en: { name:'English', title:'ECC Capabilities', search:'Search agents, skills, commands...', agents:'Agents', skills:'Skills', commands:'Commands', rules:'Rules', mcps:'MCPs', hooks:'Hooks', ruleSets:'Rule Sets', mcpConfigs:'MCP Configs', all:'All', reviewers:'Reviewers', buildResolvers:'Build Resolvers', architects:'Architects', security:'Security', testing:'Testing', patterns:'Patterns', design:'Design', research:'Research', data:'Data', agent:'Agent', devops:'DevOps', description:'Description', details:'Details', tools:'Tools', copied:'Copied', noMcps:'No MCP configs found', checkMcps:'Check mcp-configs/ directory', noHooks:'No hooks configured', recentlyViewed:'Recently Viewed', clearHistory:'Clear', ruleFiles:'rule files', more:'more', servers:'servers', skill:'Skill', workflow:'workflow', event:'Event', matcher:'Matcher', id:'ID', contribution:'Contribution to ECC' },
+ pt: { name:'Português', title:'Recursos do ECC', search:'Pesquisar agentes, skills, comandos...', agents:'Agentes', skills:'Skills', commands:'Comandos', rules:'Regras', mcps:'MCPs', hooks:'Hooks', ruleSets:'Conjuntos de Regras', mcpConfigs:'Configs MCP', all:'Todos', reviewers:'Revisores', buildResolvers:'Resolvedores', architects:'Arquitetos', security:'Segurança', testing:'Testes', patterns:'Padrões', design:'Design', research:'Pesquisa', data:'Dados', agent:'Agente', devops:'DevOps', description:'Descrição', details:'Detalhes', tools:'Ferramentas', copied:'Copiado', noMcps:'Nenhuma config MCP encontrada', checkMcps:'Verifique mcp-configs/', noHooks:'Nenhum hook configurado', recentlyViewed:'Vistos Recentemente', clearHistory:'Limpar', ruleFiles:'arquivos de regras', more:'mais', servers:'servidores', skill:'Skill', workflow:'workflow', event:'Evento', matcher:'Corresp.', id:'ID', contribution:'Contribuição ao ECC' },
+ zh: { name:'简体中文', title:'ECC 能力', search:'搜索代理、技能、命令...', agents:'代理', skills:'技能', commands:'命令', rules:'规则', mcps:'MCP', hooks:'钩子', ruleSets:'规则集', mcpConfigs:'MCP 配置', all:'全部', reviewers:'审查者', buildResolvers:'构建解析器', architects:'架构师', security:'安全', testing:'测试', patterns:'模式', design:'设计', research:'研究', data:'数据', agent:'代理', devops:'运维', description:'描述', details:'详情', tools:'工具', copied:'已复制', noMcps:'未找到 MCP 配置', checkMcps:'检查 mcp-configs/ 目录', noHooks:'未配置钩子', recentlyViewed:'最近查看', clearHistory:'清除', ruleFiles:'规则文件', more:'更多', servers:'服务器', skill:'技能', workflow:'工作流', event:'事件', matcher:'匹配器', id:'ID', contribution:'对 ECC 的贡献' },
+ zht: { name:'繁體中文', title:'ECC 能力', search:'搜索代理、技能、命令...', agents:'代理', skills:'技能', commands:'命令', rules:'規則', mcps:'MCP', hooks:'鉤子', ruleSets:'規則集', mcpConfigs:'MCP 配置', all:'全部', reviewers:'審查者', buildResolvers:'構建解析器', architects:'架構師', security:'安全', testing:'測試', patterns:'模式', design:'設計', research:'研究', data:'數據', agent:'代理', devops:'運維', description:'描述', details:'詳情', tools:'工具', copied:'已複製', noMcps:'未找到 MCP 配置', checkMcps:'檢查 mcp-configs/ 目錄', noHooks:'未配置鉤子', recentlyViewed:'最近查看', clearHistory:'清除', ruleFiles:'規則文件', more:'更多', servers:'服務器', skill:'技能', workflow:'工作流', event:'事件', matcher:'匹配器', id:'ID', contribution:'對 ECC 的貢獻' },
+ ja: { name:'日本語', title:'ECC 機能一覧', search:'エージェント、スキル、コマンドを検索...', agents:'エージェント', skills:'スキル', commands:'コマンド', rules:'ルール', mcps:'MCP', hooks:'フック', ruleSets:'ルールセット', mcpConfigs:'MCP設定', all:'すべて', reviewers:'レビュアー', buildResolvers:'ビルド解決', architects:'アーキテクト', security:'セキュリティ', testing:'テスト', patterns:'パターン', design:'デザイン', research:'研究', data:'データ', agent:'エージェント', devops:'DevOps', description:'説明', details:'詳細', tools:'ツール', copied:'コピーしました', noMcps:'MCP設定が見つかりません', checkMcps:'mcp-configs/を確認', noHooks:'フックが設定されていません', recentlyViewed:'最近見た項目', clearHistory:'クリア', ruleFiles:'ルールファイル', more:'もっと見る', servers:'サーバー', skill:'スキル', workflow:'ワークフロー', event:'イベント', matcher:'マッチャー', id:'ID', contribution:'ECCへの貢献' },
+ ko: { name:'한국어', title:'ECC 기능', search:'에이전트, 스킬, 명령어 검색...', agents:'에이전트', skills:'스킬', commands:'명령어', rules:'규칙', mcps:'MCP', hooks:'훅', ruleSets:'규칙 세트', mcpConfigs:'MCP 설정', all:'전체', reviewers:'리뷰어', buildResolvers:'빌드 해결사', architects:'아키텍트', security:'보안', testing:'테스트', patterns:'패턴', design:'디자인', research:'연구', data:'데이터', agent:'에이전트', devops:'DevOps', description:'설명', details:'세부정보', tools:'도구', copied:'복사됨', noMcps:'MCP 설정을 찾을 수 없음', checkMcps:'mcp-configs/ 확인', noHooks:'훅이 설정되지 않음', recentlyViewed:'최근 본 항목', clearHistory:'지우기', ruleFiles:'규칙 파일', more:'더보기', servers:'서버', skill:'스킬', workflow:'워크플로우', event:'이벤트', matcher:'매처', id:'ID', contribution:'ECC에 기여' },
+ tr: { name:'Türkçe', title:'ECC Yetenekleri', search:'Ajan, beceri, komut ara...', agents:'Ajanlar', skills:'Beceriler', commands:'Komutlar', rules:'Kurallar', mcps:'MCP\'ler', hooks:'Kancalar', ruleSets:'Kural Setleri', mcpConfigs:'MCP Yapılandırmaları', all:'Tümü', reviewers:'İnceleyenler', buildResolvers:'Derleme Çözücüler', architects:'Mimarlar', security:'Güvenlik', testing:'Test', patterns:'Desenler', design:'Tasarım', research:'Araştırma', data:'Veri', agent:'Ajan', devops:'DevOps', description:'Açıklama', details:'Detaylar', tools:'Araçlar', copied:'Kopyalandı', noMcps:'MCP yapılandırması bulunamadı', checkMcps:'mcp-configs/ dizinini kontrol edin', noHooks:'Kanca yapılandırılmamış', recentlyViewed:'Son Görüntülenenler', clearHistory:'Temizle', ruleFiles:'kural dosyası', more:'daha fazla', servers:'sunucu', skill:'Beceri', workflow:'iş akışı', event:'Olay', matcher:'Eşleştirici', id:'ID', contribution:'ECC\'ye Katkı' },
+ ru: { name:'Русский', title:'Возможности ECC', search:'Поиск агентов, навыков, команд...', agents:'Агенты', skills:'Навыки', commands:'Команды', rules:'Правила', mcps:'MCP', hooks:'Хуки', ruleSets:'Наборы правил', mcpConfigs:'MCP конфиги', all:'Все', reviewers:'Ревьюеры', buildResolvers:'Сборщики', architects:'Архитекторы', security:'Безопасность', testing:'Тестирование', patterns:'Паттерны', design:'Дизайн', research:'Исследования', data:'Данные', agent:'Агент', devops:'DevOps', description:'Описание', details:'Детали', tools:'Инструменты', copied:'Скопировано', noMcps:'MCP конфиги не найдены', checkMcps:'Проверьте mcp-configs/', noHooks:'Хуки не настроены', recentlyViewed:'Недавние', clearHistory:'Очистить', ruleFiles:'файлов правил', more:'ещё', servers:'серверов', skill:'Навык', workflow:'воркфлоу', event:'Событие', matcher:'Матчер', id:'ID', contribution:'Вклад в ECC' },
+ vi: { name:'Tiếng Việt', title:'Năng lực ECC', search:'Tìm kiếm agent, kỹ năng, lệnh...', agents:'Agent', skills:'Kỹ năng', commands:'Lệnh', rules:'Luật', mcps:'MCP', hooks:'Hook', ruleSets:'Bộ luật', mcpConfigs:'Cấu hình MCP', all:'Tất cả', reviewers:'Người đánh giá', buildResolvers:'Trình giải quyết build', architects:'Kiến trúc sư', security:'Bảo mật', testing:'Kiểm thử', patterns:'Mẫu', design:'Thiết kế', research:'Nghiên cứu', data:'Dữ liệu', agent:'Agent', devops:'DevOps', description:'Mô tả', details:'Chi tiết', tools:'Công cụ', copied:'Đã sao chép', noMcps:'Không tìm thấy cấu hình MCP', checkMcps:'Kiểm tra mcp-configs/', noHooks:'Chưa có hook nào', recentlyViewed:'Đã xem gần đây', clearHistory:'Xóa', ruleFiles:'tệp luật', more:'thêm', servers:'máy chủ', skill:'Kỹ năng', workflow:'quy trình', event:'Sự kiện', matcher:'Bộ so khớp', id:'ID', contribution:'Đóng góp cho ECC' },
+ th: { name:'ไทย', title:'ความสามารถ ECC', search:'ค้นหาเอเจนต์ ทักษะ คำสั่ง...', agents:'เอเจนต์', skills:'ทักษะ', commands:'คำสั่ง', rules:'กฎ', mcps:'MCP', hooks:'ฮุค', ruleSets:'ชุดกฎ', mcpConfigs:'การตั้งค่า MCP', all:'ทั้งหมด', reviewers:'ผู้ตรวจสอบ', buildResolvers:'ตัวแก้ไขบิลด์', architects:'สถาปนิก', security:'ความปลอดภัย', testing:'การทดสอบ', patterns:'รูปแบบ', design:'ออกแบบ', research:'วิจัย', data:'ข้อมูล', agent:'เอเจนต์', devops:'DevOps', description:'คำอธิบาย', details:'รายละเอียด', tools:'เครื่องมือ', copied:'คัดลอกแล้ว', noMcps:'ไม่พบการตั้งค่า MCP', checkMcps:'ตรวจสอบ mcp-configs/', noHooks:'ไม่มีการตั้งค่าฮุค', recentlyViewed:'ที่ดูล่าสุด', clearHistory:'ล้าง', ruleFiles:'ไฟล์กฎ', more:'เพิ่มเติม', servers:'เซิร์ฟเวอร์', skill:'ทักษะ', workflow:'เวิร์กโฟลว์', event:'เหตุการณ์', matcher:'ตัวจับคู่', id:'ID', contribution:'มีส่วนร่วมกับ ECC' },
+ de: { name:'Deutsch', title:'ECC-Funktionen', search:'Agenten, Fähigkeiten, Befehle suchen...', agents:'Agenten', skills:'Fähigkeiten', commands:'Befehle', rules:'Regeln', mcps:'MCPs', hooks:'Hooks', ruleSets:'Regelsätze', mcpConfigs:'MCP-Konfigurationen', all:'Alle', reviewers:'Prüfer', buildResolvers:'Build-Resolver', architects:'Architekten', security:'Sicherheit', testing:'Tests', patterns:'Muster', design:'Design', research:'Forschung', data:'Daten', agent:'Agent', devops:'DevOps', description:'Beschreibung', details:'Details', tools:'Werkzeuge', copied:'Kopiert', noMcps:'Keine MCP-Konfigurationen gefunden', checkMcps:'Prüfen Sie mcp-configs/', noHooks:'Keine Hooks konfiguriert', recentlyViewed:'Zuletzt angesehen', clearHistory:'Löschen', ruleFiles:'Regeldateien', more:'mehr', servers:'Server', skill:'Fähigkeit', workflow:'Workflow', event:'Ereignis', matcher:'Matcher', id:'ID', contribution:'Beitrag zu ECC' },
+};
+const LANG_KEYS = Object.keys(LANG);
+
+function renderHTML(data) {
+ // data passed from Node.js - use for static template values
+ const ag = JSON.stringify(data.agents).replace(/
+
+
+
+
+ECC Capabilities
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+✓
+
+
+
+`;
+ /* eslint-enable no-useless-escape */
+}
+
+const server = http.createServer((req, res) => {
+ const url = new URL(req.url, 'http://localhost');
+ if (url.pathname === '/api/data') {
+ res.writeHead(200, { 'Content-Type': 'application/json' });
+ return res.end(JSON.stringify({ agents: loadAgents(), skills: loadSkills(), commands: loadCommands(), rules: loadRules(), mcps: loadMcps(), hooks: loadHooks() }));
+ }
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+ res.end(renderHTML({ agents: loadAgents(), skills: loadSkills(), commands: loadCommands(), rules: loadRules(), mcps: loadMcps(), hooks: loadHooks() }));
+});
+
+if (require.main === module) {
+ server.listen(PORT, () => {
+ console.log(`\n 🧩 ECC Capabilities → http://localhost:${PORT}\n`);
+ try { const { spawn } = require('child_process'); const p = process.platform; const c = p === 'darwin' ? 'open' : p === 'win32' ? 'start' : 'xdg-open'; if (c === 'start') spawn('cmd', ['/c', 'start', `http://localhost:${PORT}`], { stdio: 'ignore' }); else spawn(c, [`http://localhost:${PORT}`], { stdio: 'ignore' }); } catch { /* best-effort auto-open */ }
+ });
+}
+
+module.exports = { parsePort, readFrontmatter, readSkill, loadAgents, loadSkills, loadCommands, loadRules, loadMcps, loadHooks, renderHTML, LANG, LANG_KEYS, server };
diff --git a/tests/scripts/dashboard-web.test.js b/tests/scripts/dashboard-web.test.js
new file mode 100644
index 00000000..8118a06c
--- /dev/null
+++ b/tests/scripts/dashboard-web.test.js
@@ -0,0 +1,785 @@
+/**
+ * Tests for scripts/dashboard-web.js
+ */
+
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const http = require('http');
+
+const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'dashboard-web.js');
+
+let testRoot;
+let testPassed = 0;
+let testFailed = 0;
+
+function test(name, fn) {
+ try {
+ fn();
+ console.log(` ✓ ${name}`);
+ testPassed++;
+ return true;
+ } catch (error) {
+ console.log(` ✗ ${name}`);
+ console.log(` Error: ${error.message}`);
+ testFailed++;
+ return false;
+ }
+}
+
+function createTempDir(prefix) {
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
+}
+
+function cleanup(dirPath) {
+ fs.rmSync(dirPath, { recursive: true, force: true });
+}
+
+function writeFile(rootDir, relativePath, content) {
+ const targetPath = path.join(rootDir, relativePath);
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
+ fs.writeFileSync(targetPath, content);
+}
+
+// ===================== parsePort =====================
+
+test('parsePort returns 3456 for undefined', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort(undefined), 3456);
+});
+
+test('parsePort returns numeric port for valid string', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort('8080'), 8080);
+ assert.strictEqual(parsePort('3456'), 3456);
+});
+
+test('parsePort returns numeric port for numeric input', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort(8080), 8080);
+});
+
+test('parsePort returns 3456 for port below 1', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort('-1'), 3456);
+ assert.strictEqual(parsePort('0'), 3456);
+});
+
+test('parsePort returns 3456 for port above 65535', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort('70000'), 3456);
+ assert.strictEqual(parsePort('65536'), 3456);
+});
+
+test('parsePort accepts boundary ports 1 and 65535', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort('1'), 1);
+ assert.strictEqual(parsePort('65535'), 65535);
+});
+
+test('parsePort returns 3456 for non-numeric string', () => {
+ const { parsePort } = require(SCRIPT);
+ assert.strictEqual(parsePort('abc'), 3456);
+ assert.strictEqual(parsePort(''), 3456);
+});
+
+// ===================== readFrontmatter =====================
+
+test('readFrontmatter parses simple frontmatter', () => {
+ const { readFrontmatter } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'test.md', [
+ '---',
+ 'name: test-agent',
+ 'description: A test agent description',
+ 'model: claude-sonnet-4-6',
+ '---',
+ '# Body content',
+ 'This is the body.',
+ ].join('\n'));
+
+ const fm = readFrontmatter(path.join(testRoot, 'test.md'));
+ assert.strictEqual(fm.name, 'test-agent');
+ assert.strictEqual(fm.description, 'A test agent description');
+ assert.strictEqual(fm.model, 'claude-sonnet-4-6');
+ assert.ok(fm._body.includes('# Body content'));
+ cleanup(testRoot);
+});
+
+test('readFrontmatter parses array tools field', () => {
+ const { readFrontmatter } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'agent.md', [
+ '---',
+ 'name: array-agent',
+ 'tools: [Bash, Read, Write]',
+ '---',
+ 'body',
+ ].join('\n'));
+
+ const fm = readFrontmatter(path.join(testRoot, 'agent.md'));
+ assert.strictEqual(fm.name, 'array-agent');
+ assert.ok(Array.isArray(fm.tools));
+ assert.deepStrictEqual(fm.tools, ['Bash', 'Read', 'Write']);
+ cleanup(testRoot);
+});
+
+test('readFrontmatter handles quoted values', () => {
+ const { readFrontmatter } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'test.md', [
+ '---',
+ 'name: "quoted-name"',
+ "description: 'single-quoted-desc'",
+ '---',
+ 'body',
+ ].join('\n'));
+
+ const fm = readFrontmatter(path.join(testRoot, 'test.md'));
+ assert.strictEqual(fm.name, 'quoted-name');
+ assert.strictEqual(fm.description, 'single-quoted-desc');
+ cleanup(testRoot);
+});
+
+test('readFrontmatter returns empty object for file without frontmatter', () => {
+ const { readFrontmatter } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'no-fm.md', '# Just a heading\nNo frontmatter here.');
+
+ const fm = readFrontmatter(path.join(testRoot, 'no-fm.md'));
+ assert.deepStrictEqual(fm, {});
+ cleanup(testRoot);
+});
+
+test('readFrontmatter returns empty object for missing file', () => {
+ const { readFrontmatter } = require(SCRIPT);
+ const fm = readFrontmatter('/nonexistent/path/test.md');
+ assert.deepStrictEqual(fm, {});
+});
+
+// ===================== readSkill =====================
+
+test('readSkill parses skill frontmatter and body', () => {
+ const { readSkill } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'SKILL.md', [
+ '---',
+ 'name: test-skill',
+ 'description: A test skill',
+ '---',
+ '# Skill Workflow',
+ 'Step 1: Do this.',
+ ].join('\n'));
+
+ const skill = readSkill(path.join(testRoot, 'SKILL.md'));
+ assert.strictEqual(skill.d, 'A test skill');
+ assert.ok(skill.b.includes('# Skill Workflow'));
+ assert.ok(!skill.b.includes('---')); // frontmatter stripped from body
+ cleanup(testRoot);
+});
+
+test('readSkill returns empty defaults for missing file', () => {
+ const { readSkill } = require(SCRIPT);
+ const skill = readSkill('/nonexistent/skill/SKILL.md');
+ assert.strictEqual(skill.d, '');
+ assert.strictEqual(skill.b, '');
+});
+
+// ===================== loadAgents =====================
+
+test('loadAgents returns empty array for missing directory', () => {
+ const { loadAgents } = require(SCRIPT);
+ const agents = loadAgents('/nonexistent/path');
+ assert.deepStrictEqual(agents, []);
+});
+
+test('loadAgents loads agent markdown files', () => {
+ const { loadAgents } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'agents/typescript-reviewer.md', [
+ '---',
+ 'name: typescript-reviewer',
+ 'description: Reviews TypeScript code',
+ 'model: claude-sonnet-4-6',
+ 'tools: [Bash, Read, Write, Grep]',
+ '---',
+ '# TypeScript Reviewer',
+ 'You are a TypeScript code reviewer.',
+ ].join('\n'));
+ writeFile(testRoot, 'agents/python-reviewer.md', [
+ '---',
+ 'name: python-reviewer',
+ 'description: Reviews Python code',
+ 'model: claude-opus-4-8',
+ 'tools: [Bash, Read]',
+ '---',
+ '# Python Reviewer',
+ ].join('\n'));
+
+ const agents = loadAgents(testRoot);
+ assert.strictEqual(agents.length, 2);
+ assert.strictEqual(agents[0].n, 'python-reviewer'); // alphabetical sort
+ assert.strictEqual(agents[1].n, 'typescript-reviewer');
+ assert.strictEqual(agents[1].m, 'claude-sonnet-4-6');
+ assert.strictEqual(agents[1].d, 'Reviews TypeScript code');
+ assert.deepStrictEqual(agents[1].t, ['Bash', 'Read', 'Write', 'Grep']);
+ assert.ok(agents[1].b.length > 0);
+ cleanup(testRoot);
+});
+
+test('loadAgents defaults missing fields', () => {
+ const { loadAgents } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'agents/minimal.md', [
+ '# Minimal Agent',
+ 'No frontmatter at all.',
+ ].join('\n'));
+
+ const agents = loadAgents(testRoot);
+ assert.strictEqual(agents.length, 1);
+ assert.strictEqual(agents[0].n, 'minimal');
+ assert.strictEqual(agents[0].m, 'default');
+ assert.strictEqual(agents[0].d, '');
+ assert.deepStrictEqual(agents[0].t, []);
+ cleanup(testRoot);
+});
+
+test('loadAgents ignores non-markdown files', () => {
+ const { loadAgents } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'agents/agent.md', '---\nname: real-agent\n---\nbody');
+ writeFile(testRoot, 'agents/README.txt', 'not an agent');
+
+ const agents = loadAgents(testRoot);
+ assert.strictEqual(agents.length, 1);
+ assert.strictEqual(agents[0].n, 'real-agent');
+ cleanup(testRoot);
+});
+
+// ===================== loadSkills =====================
+
+test('loadSkills returns empty array for missing directory', () => {
+ const { loadSkills } = require(SCRIPT);
+ const skills = loadSkills('/nonexistent/path');
+ assert.deepStrictEqual(skills, []);
+});
+
+test('loadSkills loads skill directories with SKILL.md', () => {
+ const { loadSkills } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'skills/seo-audit/SKILL.md', [
+ '---',
+ 'name: seo-audit',
+ 'description: Full website SEO audit',
+ '---',
+ '# SEO Audit Workflow',
+ ].join('\n'));
+ writeFile(testRoot, 'skills/code-review/SKILL.md', [
+ '---',
+ 'name: code-review',
+ 'description: Review code changes',
+ '---',
+ '# Code Review Workflow',
+ ].join('\n'));
+
+ const skills = loadSkills(testRoot);
+ assert.strictEqual(skills.length, 2);
+ assert.strictEqual(skills[0].n, 'code-review'); // alphabetical sort
+ assert.strictEqual(skills[1].n, 'seo-audit');
+ assert.strictEqual(skills[1].d, 'Full website SEO audit');
+ assert.ok(skills[1].b.length > 0);
+ cleanup(testRoot);
+});
+
+test('loadSkills ignores non-directories', () => {
+ const { loadSkills } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'skills/README.md', 'no skill here');
+ writeFile(testRoot, 'skills/real-skill/SKILL.md', '---\ndescription: A real skill\n---\nbody');
+
+ const skills = loadSkills(testRoot);
+ assert.strictEqual(skills.length, 1);
+ assert.strictEqual(skills[0].n, 'real-skill');
+ cleanup(testRoot);
+});
+
+// ===================== loadCommands =====================
+
+test('loadCommands returns empty array for missing directory', () => {
+ const { loadCommands } = require(SCRIPT);
+ const commands = loadCommands('/nonexistent/path');
+ assert.deepStrictEqual(commands, []);
+});
+
+test('loadCommands loads command markdown files with category detection', () => {
+ const { loadCommands } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'commands/pr.md', [
+ '---',
+ 'description: Create a pull request',
+ '---',
+ 'body',
+ ].join('\n'));
+ writeFile(testRoot, 'commands/go-test.md', [
+ '---',
+ 'description: Run Go tests',
+ '---',
+ 'body',
+ ].join('\n'));
+ writeFile(testRoot, 'commands/unknown-cmd.md', [
+ '---',
+ 'description: Some unknown command',
+ '---',
+ 'body',
+ ].join('\n'));
+
+ const commands = loadCommands(testRoot);
+ assert.strictEqual(commands.length, 3);
+
+ const prCmd = commands.find(c => c.n === '/pr');
+ assert.ok(prCmd);
+ assert.strictEqual(prCmd.c, 'Git & PR');
+ assert.strictEqual(prCmd.d, 'Create a pull request');
+
+ const goCmd = commands.find(c => c.n === '/go-test');
+ assert.ok(goCmd);
+ assert.strictEqual(goCmd.c, 'Languages');
+
+ const unknownCmd = commands.find(c => c.n === '/unknown-cmd');
+ assert.ok(unknownCmd);
+ assert.strictEqual(unknownCmd.c, 'Other');
+ cleanup(testRoot);
+});
+
+// ===================== loadRules =====================
+
+test('loadRules returns empty array for missing directory', () => {
+ const { loadRules } = require(SCRIPT);
+ const rules = loadRules('/nonexistent/path');
+ assert.deepStrictEqual(rules, []);
+});
+
+test('loadRules loads language directories with rule files', () => {
+ const { loadRules } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'rules/python/coding-style.md', '');
+ writeFile(testRoot, 'rules/python/testing.md', '');
+ writeFile(testRoot, 'rules/python/patterns.md', '');
+ writeFile(testRoot, 'rules/typescript/coding-style.md', '');
+ writeFile(testRoot, 'rules/typescript/testing.md', '');
+
+ const rules = loadRules(testRoot);
+ assert.strictEqual(rules.length, 2);
+
+ const pyRules = rules.find(r => r.l === 'python');
+ assert.ok(pyRules);
+ assert.strictEqual(pyRules.f.length, 3);
+ assert.ok(pyRules.f.includes('coding-style'));
+ assert.ok(pyRules.f.includes('testing'));
+ assert.ok(pyRules.f.includes('patterns'));
+
+ const tsRules = rules.find(r => r.l === 'typescript');
+ assert.ok(tsRules);
+ assert.strictEqual(tsRules.f.length, 2);
+ cleanup(testRoot);
+});
+
+test('loadRules ignores non-directories in rules folder', () => {
+ const { loadRules } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'rules/README.md', 'no rules here');
+ writeFile(testRoot, 'rules/go/testing.md', '');
+
+ const rules = loadRules(testRoot);
+ assert.strictEqual(rules.length, 1);
+ assert.strictEqual(rules[0].l, 'go');
+ assert.strictEqual(rules[0].f.length, 1);
+ assert.strictEqual(rules[0].f[0], 'testing');
+ cleanup(testRoot);
+});
+
+// ===================== loadMcps =====================
+
+test('loadMcps returns empty array when no configs exist', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+
+ const mcps = loadMcps(testRoot);
+ assert.deepStrictEqual(mcps, []);
+ cleanup(testRoot);
+});
+
+test('loadMcps loads .mcp.json config', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, '.mcp.json', JSON.stringify({
+ mcpServers: {
+ 'test-server': {
+ command: 'node',
+ args: ['server.js'],
+ env: { SECRET: 'real-secret' },
+ type: 'stdio',
+ },
+ },
+ }));
+
+ const mcps = loadMcps(testRoot);
+ assert.strictEqual(mcps.length, 1);
+ assert.strictEqual(mcps[0].f, '.mcp.json');
+ assert.strictEqual(mcps[0].s.length, 1);
+ assert.strictEqual(mcps[0].s[0].n, 'test-server');
+ assert.strictEqual(mcps[0].s[0].cmd, 'node');
+ assert.deepStrictEqual(mcps[0].s[0].args, ['server.js']);
+ assert.strictEqual(mcps[0].s[0].type, 'stdio');
+ // Env vars should be masked
+ assert.strictEqual(mcps[0].s[0].env.SECRET, '••••••');
+ cleanup(testRoot);
+});
+
+test('loadMcps loads mcp-configs/ directory files', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'mcp-configs/brave.json', JSON.stringify({
+ mcpServers: {
+ 'brave-search': {
+ command: 'npx',
+ args: ['@anthropic/mcp-brave'],
+ type: 'stdio',
+ },
+ },
+ }));
+
+ const mcps = loadMcps(testRoot);
+ assert.strictEqual(mcps.length, 1);
+ assert.strictEqual(mcps[0].f, 'brave.json');
+ assert.strictEqual(mcps[0].s.length, 1);
+ assert.strictEqual(mcps[0].s[0].n, 'brave-search');
+ cleanup(testRoot);
+});
+
+test('loadMcps masks environment variables', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'mcp-configs/with-env.json', JSON.stringify({
+ mcpServers: {
+ server: {
+ command: 'python',
+ env: { API_KEY: 'super-secret-key', DEBUG: 'true' },
+ },
+ },
+ }));
+
+ const mcps = loadMcps(testRoot);
+ assert.strictEqual(mcps[0].s[0].env.API_KEY, '••••••');
+ assert.strictEqual(mcps[0].s[0].env.DEBUG, '••••••');
+ cleanup(testRoot);
+});
+
+test('loadMcps handles url-based MCP servers', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, '.mcp.json', JSON.stringify({
+ mcpServers: {
+ 'remote-server': {
+ url: 'https://example.com/mcp',
+ type: 'sse',
+ },
+ },
+ }));
+
+ const mcps = loadMcps(testRoot);
+ assert.strictEqual(mcps[0].s[0].cmd, 'https://example.com/mcp');
+ assert.strictEqual(mcps[0].s[0].type, 'sse');
+ cleanup(testRoot);
+});
+
+test('loadMcps handles malformed JSON gracefully', () => {
+ const { loadMcps } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, '.mcp.json', '{not valid json}');
+
+ const mcps = loadMcps(testRoot);
+ assert.deepStrictEqual(mcps, []); // returns empty array on parse error
+ cleanup(testRoot);
+});
+
+// ===================== loadHooks =====================
+
+test('loadHooks returns empty array when hooks.json missing', () => {
+ const { loadHooks } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+
+ const hooks = loadHooks(testRoot);
+ assert.deepStrictEqual(hooks, []);
+ cleanup(testRoot);
+});
+
+test('loadHooks loads hook definitions', () => {
+ const { loadHooks } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'hooks/hooks.json', JSON.stringify({
+ hooks: {
+ 'post-commit': [
+ { matcher: '*.js', id: 'lint-js', description: 'Lint JS files after commit' },
+ { matcher: '*.py', id: 'lint-py', description: 'Lint Python files' },
+ ],
+ 'pre-push': [
+ { matcher: '*', id: 'run-tests', description: 'Run all tests before push' },
+ ],
+ },
+ }));
+
+ const hooks = loadHooks(testRoot);
+ assert.strictEqual(hooks.length, 3);
+ const lintJs = hooks.find(h => h.id === 'lint-js');
+ assert.ok(lintJs);
+ assert.strictEqual(lintJs.ev, 'post-commit');
+ assert.strictEqual(lintJs.m, '*.js');
+ assert.strictEqual(lintJs.d, 'Lint JS files after commit');
+ cleanup(testRoot);
+});
+
+test('loadHooks handles malformed JSON gracefully', () => {
+ const { loadHooks } = require(SCRIPT);
+ testRoot = createTempDir('ecc-test-');
+ writeFile(testRoot, 'hooks/hooks.json', '{invalid}');
+
+ const hooks = loadHooks(testRoot);
+ assert.deepStrictEqual(hooks, []);
+ cleanup(testRoot);
+});
+
+// ===================== LANG =====================
+
+test('LANG object has all expected language keys', () => {
+ const { LANG, LANG_KEYS } = require(SCRIPT);
+ assert.ok(LANG_KEYS.length >= 10); // at least 10 languages
+ // Verify key languages are present
+ assert.ok(LANG.en);
+ assert.ok(LANG.pt);
+ assert.ok(LANG.zh);
+ assert.ok(LANG.de);
+ assert.strictEqual(LANG_KEYS.length, Object.keys(LANG).length);
+});
+
+test('LANG English has all required keys', () => {
+ const { LANG } = require(SCRIPT);
+ const en = LANG.en;
+ assert.ok(en.title);
+ assert.ok(en.search);
+ assert.ok(en.agents);
+ assert.ok(en.skills);
+ assert.ok(en.commands);
+ assert.ok(en.rules);
+ assert.ok(en.mcps);
+ assert.ok(en.hooks);
+ assert.ok(en.all);
+ assert.ok(en.description);
+ assert.ok(en.tools);
+ assert.ok(en.copied);
+});
+
+// ===================== renderHTML =====================
+
+test('renderHTML returns valid HTML string', () => {
+ const { renderHTML } = require(SCRIPT);
+ const data = {
+ agents: [],
+ skills: [],
+ commands: [],
+ rules: [],
+ mcps: [],
+ hooks: [],
+ };
+ const html = renderHTML(data);
+ assert.ok(typeof html === 'string');
+ assert.ok(html.startsWith(''));
+ assert.ok(html.includes('