viberadar 0.3.1 → 0.3.3
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/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +73 -6
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +165 -1
- package/package.json +1 -1
package/dist/server/index.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ interface ServerOptions {
|
|
|
5
5
|
port: number;
|
|
6
6
|
projectRoot: string;
|
|
7
7
|
}
|
|
8
|
-
export declare function startServer({ data, port, projectRoot }: ServerOptions): Promise<http.Server>;
|
|
8
|
+
export declare function startServer({ data: initialData, port, projectRoot }: ServerOptions): Promise<http.Server>;
|
|
9
9
|
export {};
|
|
10
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAI7B,OAAO,EAAE,UAAU,EAAe,MAAM,YAAY,CAAC;AAErD,UAAU,aAAa;IACrB,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAwGzG"}
|
package/dist/server/index.js
CHANGED
|
@@ -32,14 +32,72 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.startServer = startServer;
|
|
37
40
|
const http = __importStar(require("http"));
|
|
38
41
|
const fs = __importStar(require("fs"));
|
|
39
42
|
const path = __importStar(require("path"));
|
|
43
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
44
|
+
const scanner_1 = require("../scanner");
|
|
40
45
|
const DASHBOARD_HTML = fs.readFileSync(path.join(__dirname, '../ui/dashboard.html'), 'utf-8');
|
|
41
|
-
function startServer({ data, port, projectRoot }) {
|
|
46
|
+
function startServer({ data: initialData, port, projectRoot }) {
|
|
42
47
|
return new Promise((resolve, reject) => {
|
|
48
|
+
// ── Mutable data reference ──────────────────────────────────────────────────
|
|
49
|
+
let currentData = initialData;
|
|
50
|
+
// ── SSE clients ─────────────────────────────────────────────────────────────
|
|
51
|
+
const sseClients = new Set();
|
|
52
|
+
function broadcast(event) {
|
|
53
|
+
const msg = `event: ${event}\ndata: {}\n\n`;
|
|
54
|
+
for (const client of sseClients) {
|
|
55
|
+
try {
|
|
56
|
+
client.write(msg);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
sseClients.delete(client);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ── File watcher + re-scan ──────────────────────────────────────────────────
|
|
64
|
+
let scanDebounce = null;
|
|
65
|
+
async function scheduleRescan(changedFile) {
|
|
66
|
+
if (scanDebounce)
|
|
67
|
+
clearTimeout(scanDebounce);
|
|
68
|
+
scanDebounce = setTimeout(async () => {
|
|
69
|
+
try {
|
|
70
|
+
const label = changedFile
|
|
71
|
+
? path.relative(projectRoot, changedFile).replace(/\\/g, '/')
|
|
72
|
+
: '…';
|
|
73
|
+
process.stdout.write(`\r 🔄 ${label} changed, rescanning... `);
|
|
74
|
+
currentData = await (0, scanner_1.scanProject)(projectRoot);
|
|
75
|
+
process.stdout.write(`\r ✅ ${currentData.modules.length} modules` +
|
|
76
|
+
(currentData.features ? `, ${currentData.features.length} features` : '') +
|
|
77
|
+
' \n');
|
|
78
|
+
broadcast('data-updated');
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.error('\nRescan error:', err.message);
|
|
82
|
+
}
|
|
83
|
+
}, 600);
|
|
84
|
+
}
|
|
85
|
+
chokidar_1.default.watch([
|
|
86
|
+
'**/*.{ts,tsx,js,jsx,vue,svelte}',
|
|
87
|
+
'viberadar.config.json',
|
|
88
|
+
], {
|
|
89
|
+
cwd: projectRoot,
|
|
90
|
+
ignored: [
|
|
91
|
+
'**/node_modules/**', '**/dist/**', '**/.git/**',
|
|
92
|
+
'**/coverage/**', '**/.next/**', '**/.turbo/**',
|
|
93
|
+
],
|
|
94
|
+
ignoreInitial: true,
|
|
95
|
+
persistent: true,
|
|
96
|
+
})
|
|
97
|
+
.on('add', f => scheduleRescan(path.join(projectRoot, f)))
|
|
98
|
+
.on('change', f => scheduleRescan(path.join(projectRoot, f)))
|
|
99
|
+
.on('unlink', f => scheduleRescan(path.join(projectRoot, f)));
|
|
100
|
+
// ── HTTP server ─────────────────────────────────────────────────────────────
|
|
43
101
|
const server = http.createServer((req, res) => {
|
|
44
102
|
const url = req.url ?? '/';
|
|
45
103
|
if (url === '/') {
|
|
@@ -49,7 +107,19 @@ function startServer({ data, port, projectRoot }) {
|
|
|
49
107
|
}
|
|
50
108
|
if (url === '/api/data') {
|
|
51
109
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
52
|
-
res.end(JSON.stringify(
|
|
110
|
+
res.end(JSON.stringify(currentData));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Server-Sent Events endpoint
|
|
114
|
+
if (url === '/api/events') {
|
|
115
|
+
res.writeHead(200, {
|
|
116
|
+
'Content-Type': 'text/event-stream',
|
|
117
|
+
'Cache-Control': 'no-cache',
|
|
118
|
+
'Connection': 'keep-alive',
|
|
119
|
+
});
|
|
120
|
+
res.write('data: connected\n\n'); // initial ping
|
|
121
|
+
sseClients.add(res);
|
|
122
|
+
req.on('close', () => sseClients.delete(res));
|
|
53
123
|
return;
|
|
54
124
|
}
|
|
55
125
|
res.writeHead(404);
|
|
@@ -63,10 +133,7 @@ function startServer({ data, port, projectRoot }) {
|
|
|
63
133
|
reject(err);
|
|
64
134
|
}
|
|
65
135
|
});
|
|
66
|
-
server.listen(port, '127.0.0.1', () =>
|
|
67
|
-
resolve(server);
|
|
68
|
-
});
|
|
69
|
-
// Graceful shutdown
|
|
136
|
+
server.listen(port, '127.0.0.1', () => resolve(server));
|
|
70
137
|
process.on('SIGINT', () => {
|
|
71
138
|
console.log('\n👋 VibeRadar stopped.');
|
|
72
139
|
server.close(() => process.exit(0));
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/dashboard.html
CHANGED
|
@@ -351,6 +351,12 @@
|
|
|
351
351
|
<h1>VibeRadar</h1>
|
|
352
352
|
<span class="header-project" id="projectName">—</span>
|
|
353
353
|
<span class="header-time" id="scannedAt"></span>
|
|
354
|
+
<span id="liveDot" title="Connecting…" style="
|
|
355
|
+
width:8px; height:8px; border-radius:50%;
|
|
356
|
+
background:var(--dim); display:inline-block;
|
|
357
|
+
margin-left:8px; transition:background 0.3s;
|
|
358
|
+
flex-shrink:0;
|
|
359
|
+
"></span>
|
|
354
360
|
</header>
|
|
355
361
|
|
|
356
362
|
<div class="stats-bar" id="statsBar"></div>
|
|
@@ -563,6 +569,37 @@ function renderFeatureCards(c) {
|
|
|
563
569
|
card.onclick = () => openFeaturePanel(f.key);
|
|
564
570
|
grid.appendChild(card);
|
|
565
571
|
});
|
|
572
|
+
|
|
573
|
+
// ── Unmapped card ──────────────────────────────────────────────────────────
|
|
574
|
+
if (!q) {
|
|
575
|
+
const unmappedSrc = D.modules.filter(m =>
|
|
576
|
+
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
577
|
+
);
|
|
578
|
+
if (unmappedSrc.length > 0) {
|
|
579
|
+
const isActive = activePanelKey === '__unmapped__';
|
|
580
|
+
const card = document.createElement('div');
|
|
581
|
+
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
582
|
+
card.style.borderStyle = 'dashed';
|
|
583
|
+
card.style.opacity = '0.75';
|
|
584
|
+
card.innerHTML = `
|
|
585
|
+
<div class="feature-accent" style="background:var(--yellow)"></div>
|
|
586
|
+
<div class="feature-body">
|
|
587
|
+
<div class="feature-title">
|
|
588
|
+
<span style="color:var(--yellow)">⚠ Unmapped</span>
|
|
589
|
+
<span class="feature-file-count">${unmappedSrc.length} ${pluralFiles(unmappedSrc.length)}</span>
|
|
590
|
+
</div>
|
|
591
|
+
<div class="feature-desc">Файлы вне карты фич — не входят ни в одну фичу</div>
|
|
592
|
+
<div class="feature-progress-wrap">
|
|
593
|
+
<div class="feature-progress-bar">
|
|
594
|
+
<div class="feature-progress-fill" style="width:100%;background:var(--border)"></div>
|
|
595
|
+
</div>
|
|
596
|
+
<span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
|
|
597
|
+
</div>
|
|
598
|
+
</div>`;
|
|
599
|
+
card.onclick = () => openUnmappedPanel(unmappedSrc);
|
|
600
|
+
grid.appendChild(card);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
566
603
|
}
|
|
567
604
|
|
|
568
605
|
function renderModuleGrid(c) {
|
|
@@ -720,6 +757,71 @@ function openModulePanel(m) {
|
|
|
720
757
|
document.getElementById('panel').classList.add('open');
|
|
721
758
|
}
|
|
722
759
|
|
|
760
|
+
function openUnmappedPanel(files) {
|
|
761
|
+
activePanelKey = '__unmapped__';
|
|
762
|
+
renderContent();
|
|
763
|
+
|
|
764
|
+
// Group by top-level directory
|
|
765
|
+
const byDir = {};
|
|
766
|
+
files.forEach(m => {
|
|
767
|
+
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
768
|
+
const dir = parts.length > 1 ? parts[0] : '(root)';
|
|
769
|
+
if (!byDir[dir]) byDir[dir] = [];
|
|
770
|
+
byDir[dir].push(m);
|
|
771
|
+
});
|
|
772
|
+
const dirs = Object.keys(byDir).sort();
|
|
773
|
+
|
|
774
|
+
// Build plain-text list for copying to AI agent
|
|
775
|
+
const plainList = files
|
|
776
|
+
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'))
|
|
777
|
+
.join('\n');
|
|
778
|
+
|
|
779
|
+
const promptText =
|
|
780
|
+
`В проекте ${files.length} файлов без привязки к фичам (unmapped).\n` +
|
|
781
|
+
`Изучи список ниже и обнови viberadar.config.json:\n` +
|
|
782
|
+
`добавь эти файлы в существующие фичи или создай новые.\n\n` +
|
|
783
|
+
plainList;
|
|
784
|
+
|
|
785
|
+
document.getElementById('panelContent').innerHTML = `
|
|
786
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
|
787
|
+
<span style="font-size:16px">⚠</span>
|
|
788
|
+
<div class="panel-title" style="color:var(--yellow)">Unmapped файлы</div>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="panel-subtitle">
|
|
791
|
+
Не входят ни в одну фичу.<br>
|
|
792
|
+
Запусти <code style="color:var(--blue)">npx viberadar init</code> — агент предложит куда добавить.
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
<button id="copyUnmapped" style="
|
|
796
|
+
width:100%; padding:8px 12px; margin-bottom:16px;
|
|
797
|
+
background:var(--bg); border:1px solid var(--border);
|
|
798
|
+
border-radius:6px; color:var(--blue); font-size:12px;
|
|
799
|
+
cursor:pointer; text-align:left;
|
|
800
|
+
">📋 Скопировать список для AI-агента</button>
|
|
801
|
+
|
|
802
|
+
${dirs.map(dir => `
|
|
803
|
+
<div class="panel-section">
|
|
804
|
+
<div class="panel-section-label">${dir}/ (${byDir[dir].length})</div>
|
|
805
|
+
<div class="file-list">
|
|
806
|
+
${byDir[dir].map(m => fileItem(m)).join('')}
|
|
807
|
+
</div>
|
|
808
|
+
</div>
|
|
809
|
+
`).join('')}`;
|
|
810
|
+
|
|
811
|
+
document.getElementById('copyUnmapped').onclick = function() {
|
|
812
|
+
navigator.clipboard.writeText(promptText).then(() => {
|
|
813
|
+
this.textContent = '✅ Скопировано! Вставь в AI-агента';
|
|
814
|
+
this.style.color = 'var(--green)';
|
|
815
|
+
setTimeout(() => {
|
|
816
|
+
this.textContent = '📋 Скопировать список для AI-агента';
|
|
817
|
+
this.style.color = 'var(--blue)';
|
|
818
|
+
}, 3000);
|
|
819
|
+
});
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
document.getElementById('panel').classList.add('open');
|
|
823
|
+
}
|
|
824
|
+
|
|
723
825
|
function closePanel() {
|
|
724
826
|
activePanelKey = null;
|
|
725
827
|
document.getElementById('panel').classList.remove('open');
|
|
@@ -749,7 +851,69 @@ document.getElementById('searchInput').oninput = e => {
|
|
|
749
851
|
document.getElementById('panelClose').onclick = closePanel;
|
|
750
852
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
|
|
751
853
|
|
|
752
|
-
|
|
854
|
+
// ─── Live reload ──────────────────────────────────────────────────────────────
|
|
855
|
+
function setLiveDot(color, title) {
|
|
856
|
+
const dot = document.getElementById('liveDot');
|
|
857
|
+
dot.style.background = color;
|
|
858
|
+
dot.title = title;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
async function refreshData() {
|
|
862
|
+
try {
|
|
863
|
+
const res = await fetch('/api/data');
|
|
864
|
+
D = await res.json();
|
|
865
|
+
|
|
866
|
+
// Update header timestamp
|
|
867
|
+
document.getElementById('scannedAt').textContent =
|
|
868
|
+
new Date(D.scannedAt).toLocaleTimeString();
|
|
869
|
+
|
|
870
|
+
renderStats();
|
|
871
|
+
renderSidebar();
|
|
872
|
+
renderContent();
|
|
873
|
+
|
|
874
|
+
// Re-open panel if it was open
|
|
875
|
+
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
876
|
+
if (panelOpen && activePanelKey) {
|
|
877
|
+
if (activePanelKey === '__unmapped__') {
|
|
878
|
+
const unmapped = D.modules.filter(m =>
|
|
879
|
+
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
880
|
+
);
|
|
881
|
+
openUnmappedPanel(unmapped);
|
|
882
|
+
} else if (view === 'features' && D.features) {
|
|
883
|
+
openFeaturePanel(activePanelKey);
|
|
884
|
+
} else {
|
|
885
|
+
const m = D.modules.find(m => m.id === activePanelKey);
|
|
886
|
+
if (m) openModulePanel(m);
|
|
887
|
+
else closePanel();
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Brief green flash on the dot to signal fresh data
|
|
892
|
+
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
893
|
+
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
console.error('Refresh failed:', err);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function connectSSE() {
|
|
900
|
+
const es = new EventSource('/api/events');
|
|
901
|
+
|
|
902
|
+
es.onopen = () => setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
903
|
+
|
|
904
|
+
es.addEventListener('data-updated', () => {
|
|
905
|
+
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
906
|
+
refreshData();
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
es.onerror = () => {
|
|
910
|
+
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
911
|
+
es.close();
|
|
912
|
+
setTimeout(connectSSE, 3000);
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
init().then(() => connectSSE());
|
|
753
917
|
</script>
|
|
754
918
|
</body>
|
|
755
919
|
</html>
|