viagen 0.0.17 → 0.0.18
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 +3 -1
- package/dist/index.js +428 -29
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Add a file editor panel to the chat UI:
|
|
|
98
98
|
|
|
99
99
|
```ts
|
|
100
100
|
viagen({
|
|
101
|
-
editable: ['src/components', '
|
|
101
|
+
editable: ['src/components', 'vite.config.ts']
|
|
102
102
|
})
|
|
103
103
|
```
|
|
104
104
|
|
|
@@ -153,6 +153,8 @@ GET /via/iframe — split view (app + chat side by side)
|
|
|
153
153
|
GET /via/files — list editable files (when configured)
|
|
154
154
|
GET /via/file?path= — read file content
|
|
155
155
|
POST /via/file — write file content { path, content }
|
|
156
|
+
GET /via/git/status — list changed files (git status)
|
|
157
|
+
GET /via/git/diff — full diff, or single file with ?path=
|
|
156
158
|
```
|
|
157
159
|
|
|
158
160
|
When `VIAGEN_AUTH_TOKEN` is set (always on in sandboxes), pass the token as a `Bearer` header or `?token=` query param.
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3
|
-
import { join as
|
|
3
|
+
import { join as join6 } from "path";
|
|
4
4
|
import { loadEnv } from "vite";
|
|
5
5
|
|
|
6
6
|
// src/logger.ts
|
|
@@ -112,12 +112,12 @@ async function refreshAccessToken(refresh) {
|
|
|
112
112
|
|
|
113
113
|
// src/chat.ts
|
|
114
114
|
function readBody(req) {
|
|
115
|
-
return new Promise((
|
|
115
|
+
return new Promise((resolve3, reject) => {
|
|
116
116
|
let body = "";
|
|
117
117
|
req.on("data", (chunk) => {
|
|
118
118
|
body += chunk.toString();
|
|
119
119
|
});
|
|
120
|
-
req.on("end", () =>
|
|
120
|
+
req.on("end", () => resolve3(body));
|
|
121
121
|
req.on("error", reject);
|
|
122
122
|
});
|
|
123
123
|
}
|
|
@@ -494,6 +494,8 @@ function buildClientScript(opts) {
|
|
|
494
494
|
// src/ui.ts
|
|
495
495
|
function buildUiHtml(opts) {
|
|
496
496
|
const hasEditor = opts?.editable ?? false;
|
|
497
|
+
const hasGit = opts?.git ?? false;
|
|
498
|
+
const hasTabs = hasEditor || hasGit;
|
|
497
499
|
return `<!DOCTYPE html>
|
|
498
500
|
<html lang="en">
|
|
499
501
|
<head>
|
|
@@ -607,7 +609,7 @@ function buildUiHtml(opts) {
|
|
|
607
609
|
gap: 8px;
|
|
608
610
|
}
|
|
609
611
|
.messages:empty::after {
|
|
610
|
-
content: 'Ask Claude to build something...';
|
|
612
|
+
content: 'Ask Claude to build features or change something...';
|
|
611
613
|
color: #3f3f46;
|
|
612
614
|
font-size: 13px;
|
|
613
615
|
text-align: center;
|
|
@@ -879,6 +881,86 @@ function buildUiHtml(opts) {
|
|
|
879
881
|
background: #0a0a0c;
|
|
880
882
|
border-right: 1px solid #1e1e22;
|
|
881
883
|
}
|
|
884
|
+
.changes-file {
|
|
885
|
+
padding: 8px 16px;
|
|
886
|
+
font-family: ui-monospace, monospace;
|
|
887
|
+
font-size: 12px;
|
|
888
|
+
color: #a1a1aa;
|
|
889
|
+
cursor: pointer;
|
|
890
|
+
display: flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
gap: 8px;
|
|
893
|
+
transition: background 0.1s;
|
|
894
|
+
border-bottom: 1px solid #1e1e22;
|
|
895
|
+
}
|
|
896
|
+
.changes-file:hover { background: #18181b; color: #e4e4e7; }
|
|
897
|
+
.changes-file .file-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
898
|
+
.changes-dot {
|
|
899
|
+
width: 6px;
|
|
900
|
+
height: 6px;
|
|
901
|
+
border-radius: 50%;
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
}
|
|
904
|
+
.changes-dot.M { background: #facc15; }
|
|
905
|
+
.changes-dot.A { background: #4ade80; }
|
|
906
|
+
.changes-dot.q { background: #4ade80; }
|
|
907
|
+
.changes-dot.D { background: #f87171; }
|
|
908
|
+
.changes-dot.R { background: #60a5fa; }
|
|
909
|
+
.changes-badge {
|
|
910
|
+
font-size: 10px;
|
|
911
|
+
color: #52525b;
|
|
912
|
+
font-family: ui-monospace, monospace;
|
|
913
|
+
}
|
|
914
|
+
.changes-summary {
|
|
915
|
+
padding: 8px 16px;
|
|
916
|
+
border-bottom: 1px solid #27272a;
|
|
917
|
+
background: #18181b;
|
|
918
|
+
font-family: ui-monospace, monospace;
|
|
919
|
+
font-size: 11px;
|
|
920
|
+
color: #71717a;
|
|
921
|
+
display: flex;
|
|
922
|
+
align-items: center;
|
|
923
|
+
gap: 10px;
|
|
924
|
+
flex-shrink: 0;
|
|
925
|
+
}
|
|
926
|
+
.changes-summary .stat-add { color: #4ade80; }
|
|
927
|
+
.changes-summary .stat-del { color: #f87171; }
|
|
928
|
+
.changes-summary .stat-files { color: #a1a1aa; }
|
|
929
|
+
.file-delta {
|
|
930
|
+
font-family: ui-monospace, monospace;
|
|
931
|
+
font-size: 10px;
|
|
932
|
+
display: flex;
|
|
933
|
+
gap: 4px;
|
|
934
|
+
flex-shrink: 0;
|
|
935
|
+
}
|
|
936
|
+
.file-delta .d-add { color: #4ade80; }
|
|
937
|
+
.file-delta .d-del { color: #f87171; }
|
|
938
|
+
.diff-view {
|
|
939
|
+
flex: 1;
|
|
940
|
+
overflow-y: auto;
|
|
941
|
+
padding: 0;
|
|
942
|
+
font-family: ui-monospace, monospace;
|
|
943
|
+
font-size: 11px;
|
|
944
|
+
line-height: 1.6;
|
|
945
|
+
}
|
|
946
|
+
.diff-line {
|
|
947
|
+
padding: 0 12px;
|
|
948
|
+
white-space: pre-wrap;
|
|
949
|
+
word-break: break-all;
|
|
950
|
+
}
|
|
951
|
+
.diff-add { color: #4ade80; background: rgba(74,222,128,0.08); }
|
|
952
|
+
.diff-del { color: #f87171; background: rgba(248,113,113,0.08); }
|
|
953
|
+
.diff-hunk { color: #a78bfa; background: rgba(167,139,250,0.06); padding-top: 6px; margin-top: 4px; }
|
|
954
|
+
.diff-meta { color: #52525b; }
|
|
955
|
+
.diff-ctx { color: #71717a; }
|
|
956
|
+
.changes-empty {
|
|
957
|
+
padding: 16px;
|
|
958
|
+
color: #52525b;
|
|
959
|
+
font-size: 12px;
|
|
960
|
+
font-family: ui-monospace, monospace;
|
|
961
|
+
text-align: center;
|
|
962
|
+
margin-top: 40%;
|
|
963
|
+
}
|
|
882
964
|
</style>
|
|
883
965
|
</head>
|
|
884
966
|
<body>
|
|
@@ -895,9 +977,10 @@ function buildUiHtml(opts) {
|
|
|
895
977
|
<button class="btn" id="reset-btn">Reset</button>
|
|
896
978
|
</div>
|
|
897
979
|
</div>
|
|
898
|
-
${
|
|
980
|
+
${hasTabs ? `<div class="tab-bar" id="tab-bar">
|
|
899
981
|
<button class="tab active" data-tab="chat">Chat</button>
|
|
900
|
-
<button class="tab" data-tab="files">Files</button>
|
|
982
|
+
${hasEditor ? '<button class="tab" data-tab="files">Files</button>' : ""}
|
|
983
|
+
${hasGit ? '<button class="tab" data-tab="changes" id="changes-tab">Changes</button>' : ""}
|
|
901
984
|
</div>` : ""}
|
|
902
985
|
<div id="chat-view" style="display:flex;flex-direction:column;flex:1;overflow:hidden;">
|
|
903
986
|
<div class="setup-banner" id="setup-banner"></div>
|
|
@@ -924,6 +1007,19 @@ function buildUiHtml(opts) {
|
|
|
924
1007
|
</div>
|
|
925
1008
|
</div>
|
|
926
1009
|
</div>` : ""}
|
|
1010
|
+
${hasGit ? `<div id="changes-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
1011
|
+
<div class="changes-summary" id="changes-summary" style="display:none;"></div>
|
|
1012
|
+
<div id="changes-list-view" style="flex:1;overflow-y:auto;">
|
|
1013
|
+
<div id="changes-list" style="padding:0;"></div>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div id="changes-diff-view" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
|
|
1016
|
+
<div class="editor-header">
|
|
1017
|
+
<button class="editor-back" id="diff-back" title="Back to changes">←</button>
|
|
1018
|
+
<span class="editor-filename" id="diff-filename"></span>
|
|
1019
|
+
</div>
|
|
1020
|
+
<div class="diff-view" id="diff-content"></div>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>` : ""}
|
|
927
1023
|
<script>
|
|
928
1024
|
var STORAGE_KEY = 'viagen_chatLog';
|
|
929
1025
|
var SOUND_KEY = 'viagen_sound';
|
|
@@ -1371,36 +1467,45 @@ function buildUiHtml(opts) {
|
|
|
1371
1467
|
|
|
1372
1468
|
loadHistory();
|
|
1373
1469
|
|
|
1374
|
-
// \u2500\u2500
|
|
1375
|
-
${
|
|
1470
|
+
// \u2500\u2500 Tab switching \u2500\u2500
|
|
1471
|
+
${hasTabs ? `
|
|
1376
1472
|
(function() {
|
|
1377
1473
|
var chatView = document.getElementById('chat-view');
|
|
1378
1474
|
var filesView = document.getElementById('files-view');
|
|
1475
|
+
var changesView = document.getElementById('changes-view');
|
|
1379
1476
|
var tabs = document.querySelectorAll('.tab');
|
|
1380
|
-
var fileListView = document.getElementById('file-list-view');
|
|
1381
|
-
var fileEditorView = document.getElementById('file-editor-view');
|
|
1382
|
-
var editorTextarea = document.getElementById('editor-textarea');
|
|
1383
|
-
var lineNumbersEl = document.getElementById('line-numbers');
|
|
1384
|
-
var editorWrap = document.getElementById('editor-wrap');
|
|
1385
|
-
var editorSave = document.getElementById('editor-save');
|
|
1386
|
-
var editorFilename = document.getElementById('editor-filename');
|
|
1387
1477
|
|
|
1388
|
-
var editorState = { path: '', original: '', modified: false };
|
|
1389
|
-
|
|
1390
|
-
// Tab switching
|
|
1391
1478
|
tabs.forEach(function(tab) {
|
|
1392
1479
|
tab.addEventListener('click', function() {
|
|
1393
1480
|
tabs.forEach(function(t) { t.classList.remove('active'); });
|
|
1394
1481
|
tab.classList.add('active');
|
|
1395
1482
|
var target = tab.dataset.tab;
|
|
1396
1483
|
chatView.style.display = target === 'chat' ? 'flex' : 'none';
|
|
1397
|
-
filesView.style.display = target === 'files' ? 'flex' : 'none';
|
|
1398
|
-
if (target === '
|
|
1484
|
+
if (filesView) filesView.style.display = target === 'files' ? 'flex' : 'none';
|
|
1485
|
+
if (changesView) changesView.style.display = target === 'changes' ? 'flex' : 'none';
|
|
1486
|
+
if (target === 'files' && window._viagenLoadFiles) window._viagenLoadFiles();
|
|
1487
|
+
if (target === 'changes' && window._viagenLoadChanges) window._viagenLoadChanges();
|
|
1399
1488
|
if (target === 'chat') inputEl.focus();
|
|
1400
1489
|
});
|
|
1401
1490
|
});
|
|
1491
|
+
})();
|
|
1492
|
+
` : ""}
|
|
1493
|
+
|
|
1494
|
+
// \u2500\u2500 File editor panel \u2500\u2500
|
|
1495
|
+
${hasEditor ? `
|
|
1496
|
+
(function() {
|
|
1497
|
+
var fileListView = document.getElementById('file-list-view');
|
|
1498
|
+
var fileEditorView = document.getElementById('file-editor-view');
|
|
1499
|
+
var editorTextarea = document.getElementById('editor-textarea');
|
|
1500
|
+
var lineNumbersEl = document.getElementById('line-numbers');
|
|
1501
|
+
var editorWrap = document.getElementById('editor-wrap');
|
|
1502
|
+
var editorSave = document.getElementById('editor-save');
|
|
1503
|
+
var editorFilename = document.getElementById('editor-filename');
|
|
1504
|
+
|
|
1505
|
+
var editorState = { path: '', original: '', modified: false };
|
|
1402
1506
|
|
|
1403
1507
|
// File list
|
|
1508
|
+
window._viagenLoadFiles = loadFileList;
|
|
1404
1509
|
async function loadFileList() {
|
|
1405
1510
|
var listEl = document.getElementById('file-list');
|
|
1406
1511
|
listEl.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading...</div>';
|
|
@@ -1535,6 +1640,129 @@ function buildUiHtml(opts) {
|
|
|
1535
1640
|
|
|
1536
1641
|
})();
|
|
1537
1642
|
` : ""}
|
|
1643
|
+
|
|
1644
|
+
// \u2500\u2500 Changes panel (git diff) \u2500\u2500
|
|
1645
|
+
${hasGit ? `
|
|
1646
|
+
(function() {
|
|
1647
|
+
var changesListView = document.getElementById('changes-list-view');
|
|
1648
|
+
var changesDiffView = document.getElementById('changes-diff-view');
|
|
1649
|
+
var changesListEl = document.getElementById('changes-list');
|
|
1650
|
+
var diffContent = document.getElementById('diff-content');
|
|
1651
|
+
var diffFilename = document.getElementById('diff-filename');
|
|
1652
|
+
var changesTab = document.getElementById('changes-tab');
|
|
1653
|
+
var changesSummary = document.getElementById('changes-summary');
|
|
1654
|
+
|
|
1655
|
+
window._viagenLoadChanges = loadChanges;
|
|
1656
|
+
|
|
1657
|
+
async function loadChanges() {
|
|
1658
|
+
changesListView.style.display = 'block';
|
|
1659
|
+
changesDiffView.style.display = 'none';
|
|
1660
|
+
changesSummary.style.display = 'none';
|
|
1661
|
+
changesListEl.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading...</div>';
|
|
1662
|
+
try {
|
|
1663
|
+
var res = await fetch('/via/git/status');
|
|
1664
|
+
var data = await res.json();
|
|
1665
|
+
if (!data.git) {
|
|
1666
|
+
changesListEl.innerHTML = '<div class="changes-empty">Not a git repository</div>';
|
|
1667
|
+
if (changesTab) changesTab.textContent = 'Changes';
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
renderSummary(data);
|
|
1671
|
+
renderChanges(data.files);
|
|
1672
|
+
} catch(e) {
|
|
1673
|
+
changesListEl.innerHTML = '<div style="padding:16px;color:#f87171;font-size:12px;">Failed to load changes</div>';
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function renderSummary(data) {
|
|
1678
|
+
var ins = data.insertions || 0;
|
|
1679
|
+
var del = data.deletions || 0;
|
|
1680
|
+
var count = data.files ? data.files.length : 0;
|
|
1681
|
+
if (count === 0) { changesSummary.style.display = 'none'; return; }
|
|
1682
|
+
changesSummary.style.display = 'flex';
|
|
1683
|
+
changesSummary.innerHTML =
|
|
1684
|
+
'<span class="stat-files">' + count + (count === 1 ? ' file' : ' files') + '</span>' +
|
|
1685
|
+
(ins > 0 ? '<span class="stat-add">+' + ins + '</span>' : '') +
|
|
1686
|
+
(del > 0 ? '<span class="stat-del">-' + del + '</span>' : '');
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function renderChanges(files) {
|
|
1690
|
+
changesListEl.innerHTML = '';
|
|
1691
|
+
if (changesTab) changesTab.textContent = files.length > 0 ? 'Changes (' + files.length + ')' : 'Changes';
|
|
1692
|
+
if (files.length === 0) {
|
|
1693
|
+
changesListEl.innerHTML = '<div class="changes-empty">No changes</div>';
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
files.forEach(function(f) {
|
|
1697
|
+
var item = document.createElement('div');
|
|
1698
|
+
item.className = 'changes-file';
|
|
1699
|
+
var dotClass = f.status === '?' ? 'q' : f.status;
|
|
1700
|
+
var statusLabel = f.status === '?' ? 'Untracked' : f.status === 'M' ? 'Modified' : f.status === 'A' ? 'Added' : f.status === 'D' ? 'Deleted' : f.status === 'R' ? 'Renamed' : f.status;
|
|
1701
|
+
var deltaHtml = '';
|
|
1702
|
+
if (f.insertions > 0 || f.deletions > 0) {
|
|
1703
|
+
deltaHtml = '<span class="file-delta">' +
|
|
1704
|
+
(f.insertions > 0 ? '<span class="d-add">+' + f.insertions + '</span>' : '') +
|
|
1705
|
+
(f.deletions > 0 ? '<span class="d-del">-' + f.deletions + '</span>' : '') +
|
|
1706
|
+
'</span>';
|
|
1707
|
+
}
|
|
1708
|
+
item.innerHTML = '<span class="changes-dot ' + dotClass + '" title="' + statusLabel + '"></span>' +
|
|
1709
|
+
'<span class="file-path" title="' + escapeHtml(f.path) + '">' + escapeHtml(f.path) + '</span>' +
|
|
1710
|
+
deltaHtml;
|
|
1711
|
+
item.addEventListener('click', function() { openDiff(f.path); });
|
|
1712
|
+
changesListEl.appendChild(item);
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function openDiff(path) {
|
|
1717
|
+
changesListView.style.display = 'none';
|
|
1718
|
+
changesDiffView.style.display = 'flex';
|
|
1719
|
+
diffFilename.textContent = path;
|
|
1720
|
+
diffContent.innerHTML = '<div style="padding:16px;color:#52525b;font-size:12px;font-family:ui-monospace,monospace;">Loading diff...</div>';
|
|
1721
|
+
|
|
1722
|
+
try {
|
|
1723
|
+
var res = await fetch('/via/git/diff?path=' + encodeURIComponent(path));
|
|
1724
|
+
var data = await res.json();
|
|
1725
|
+
renderDiff(data.diff);
|
|
1726
|
+
} catch(e) {
|
|
1727
|
+
diffContent.innerHTML = '<div style="padding:16px;color:#f87171;font-size:12px;">Failed to load diff</div>';
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
function renderDiff(diff) {
|
|
1732
|
+
diffContent.innerHTML = '';
|
|
1733
|
+
if (!diff) {
|
|
1734
|
+
diffContent.innerHTML = '<div class="changes-empty">No diff available</div>';
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
var lines = diff.split('\\n');
|
|
1738
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1739
|
+
var line = lines[i];
|
|
1740
|
+
var div = document.createElement('div');
|
|
1741
|
+
div.className = 'diff-line';
|
|
1742
|
+
if (line.charAt(0) === '+' && !line.startsWith('+++')) {
|
|
1743
|
+
div.className += ' diff-add';
|
|
1744
|
+
} else if (line.charAt(0) === '-' && !line.startsWith('---')) {
|
|
1745
|
+
div.className += ' diff-del';
|
|
1746
|
+
} else if (line.startsWith('@@')) {
|
|
1747
|
+
div.className += ' diff-hunk';
|
|
1748
|
+
} else if (line.startsWith('diff ') || line.startsWith('index ') || line.startsWith('---') || line.startsWith('+++')) {
|
|
1749
|
+
div.className += ' diff-meta';
|
|
1750
|
+
} else {
|
|
1751
|
+
div.className += ' diff-ctx';
|
|
1752
|
+
}
|
|
1753
|
+
div.textContent = line;
|
|
1754
|
+
diffContent.appendChild(div);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// Back button
|
|
1759
|
+
document.getElementById('diff-back').addEventListener('click', function() {
|
|
1760
|
+
changesDiffView.style.display = 'none';
|
|
1761
|
+
changesListView.style.display = 'block';
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
})();
|
|
1765
|
+
` : ""}
|
|
1538
1766
|
</script>
|
|
1539
1767
|
</body>
|
|
1540
1768
|
</html>`;
|
|
@@ -1690,12 +1918,12 @@ import {
|
|
|
1690
1918
|
} from "fs";
|
|
1691
1919
|
import { join as join3, resolve, relative } from "path";
|
|
1692
1920
|
function readBody2(req) {
|
|
1693
|
-
return new Promise((
|
|
1921
|
+
return new Promise((resolve3, reject) => {
|
|
1694
1922
|
let body = "";
|
|
1695
1923
|
req.on("data", (chunk) => {
|
|
1696
1924
|
body += chunk.toString();
|
|
1697
1925
|
});
|
|
1698
|
-
req.on("end", () =>
|
|
1926
|
+
req.on("end", () => resolve3(body));
|
|
1699
1927
|
req.on("error", reject);
|
|
1700
1928
|
});
|
|
1701
1929
|
}
|
|
@@ -1892,10 +2120,180 @@ function createInjectionMiddleware() {
|
|
|
1892
2120
|
};
|
|
1893
2121
|
}
|
|
1894
2122
|
|
|
2123
|
+
// src/git.ts
|
|
2124
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2125
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
2126
|
+
import {
|
|
2127
|
+
simpleGit
|
|
2128
|
+
} from "simple-git";
|
|
2129
|
+
function mapStatus(result, stats) {
|
|
2130
|
+
const files = [];
|
|
2131
|
+
const push = (path, status) => {
|
|
2132
|
+
const s = stats.get(path) ?? { ins: 0, del: 0 };
|
|
2133
|
+
files.push({ path, status, insertions: s.ins, deletions: s.del });
|
|
2134
|
+
};
|
|
2135
|
+
for (const f of result.modified) push(f, "M");
|
|
2136
|
+
for (const f of result.created) push(f, "A");
|
|
2137
|
+
for (const f of result.deleted) push(f, "D");
|
|
2138
|
+
for (const f of result.renamed) push(f.to, "R");
|
|
2139
|
+
for (const f of result.not_added) push(f, "?");
|
|
2140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2141
|
+
return files.filter((f) => {
|
|
2142
|
+
if (seen.has(f.path)) return false;
|
|
2143
|
+
seen.add(f.path);
|
|
2144
|
+
return true;
|
|
2145
|
+
}).sort((a, b) => a.path.localeCompare(b.path));
|
|
2146
|
+
}
|
|
2147
|
+
async function getDiffStats(git, repoRoot, untrackedFiles) {
|
|
2148
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2149
|
+
let totalInsertions = 0;
|
|
2150
|
+
let totalDeletions = 0;
|
|
2151
|
+
try {
|
|
2152
|
+
const [staged, unstaged] = await Promise.all([
|
|
2153
|
+
git.diffSummary(["--cached"]),
|
|
2154
|
+
git.diffSummary()
|
|
2155
|
+
]);
|
|
2156
|
+
for (const summary of [staged, unstaged]) {
|
|
2157
|
+
for (const f of summary.files) {
|
|
2158
|
+
if (f.binary) continue;
|
|
2159
|
+
const tf = f;
|
|
2160
|
+
const existing = stats.get(tf.file) ?? { ins: 0, del: 0 };
|
|
2161
|
+
existing.ins += tf.insertions;
|
|
2162
|
+
existing.del += tf.deletions;
|
|
2163
|
+
stats.set(tf.file, existing);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
for (const filePath of untrackedFiles) {
|
|
2167
|
+
try {
|
|
2168
|
+
const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
|
|
2169
|
+
const lines = content.split("\n").length;
|
|
2170
|
+
stats.set(filePath, { ins: lines, del: 0 });
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
for (const { ins, del } of stats.values()) {
|
|
2175
|
+
totalInsertions += ins;
|
|
2176
|
+
totalDeletions += del;
|
|
2177
|
+
}
|
|
2178
|
+
} catch {
|
|
2179
|
+
}
|
|
2180
|
+
return { stats, totalInsertions, totalDeletions };
|
|
2181
|
+
}
|
|
2182
|
+
async function getFileDiff(git, repoRoot, filePath) {
|
|
2183
|
+
const abs = resolve2(repoRoot, filePath);
|
|
2184
|
+
if (!abs.startsWith(repoRoot + "/") && abs !== repoRoot) {
|
|
2185
|
+
return "";
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
const staged = await git.diff(["--cached", "--", filePath]);
|
|
2189
|
+
const unstaged = await git.diff(["--", filePath]);
|
|
2190
|
+
if (staged && unstaged) return staged + "\n" + unstaged;
|
|
2191
|
+
if (staged) return staged;
|
|
2192
|
+
if (unstaged) return unstaged;
|
|
2193
|
+
try {
|
|
2194
|
+
const content = readFileSync3(join4(repoRoot, filePath), "utf-8");
|
|
2195
|
+
const lines = content.split("\n");
|
|
2196
|
+
const added = lines.map((l) => `+${l}`).join("\n");
|
|
2197
|
+
return `--- /dev/null
|
|
2198
|
+
+++ b/${filePath}
|
|
2199
|
+
@@ -0,0 +1,${lines.length} @@
|
|
2200
|
+
${added}`;
|
|
2201
|
+
} catch {
|
|
2202
|
+
return "";
|
|
2203
|
+
}
|
|
2204
|
+
} catch {
|
|
2205
|
+
return "";
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
async function findRepoRoot(cwd) {
|
|
2209
|
+
try {
|
|
2210
|
+
const root = await simpleGit(cwd).revparse(["--show-toplevel"]);
|
|
2211
|
+
return root.trim();
|
|
2212
|
+
} catch {
|
|
2213
|
+
return null;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
function registerGitRoutes(server, opts) {
|
|
2217
|
+
let repoRoot = null;
|
|
2218
|
+
let git = null;
|
|
2219
|
+
async function ensureGit() {
|
|
2220
|
+
if (git && repoRoot) return { git, root: repoRoot };
|
|
2221
|
+
repoRoot = await findRepoRoot(opts.projectRoot);
|
|
2222
|
+
if (!repoRoot) return null;
|
|
2223
|
+
git = simpleGit(repoRoot);
|
|
2224
|
+
return { git, root: repoRoot };
|
|
2225
|
+
}
|
|
2226
|
+
server.middlewares.use("/via/git/status", (req, res) => {
|
|
2227
|
+
if (req.method !== "GET") {
|
|
2228
|
+
res.statusCode = 405;
|
|
2229
|
+
res.setHeader("Content-Type", "application/json");
|
|
2230
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
res.setHeader("Content-Type", "application/json");
|
|
2234
|
+
ensureGit().then(async (ctx) => {
|
|
2235
|
+
if (!ctx) {
|
|
2236
|
+
res.end(JSON.stringify({ files: [], git: false }));
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const result = await ctx.git.status();
|
|
2240
|
+
const { stats, totalInsertions, totalDeletions } = await getDiffStats(
|
|
2241
|
+
ctx.git,
|
|
2242
|
+
ctx.root,
|
|
2243
|
+
result.not_added
|
|
2244
|
+
);
|
|
2245
|
+
const files = mapStatus(result, stats);
|
|
2246
|
+
res.end(
|
|
2247
|
+
JSON.stringify({
|
|
2248
|
+
files,
|
|
2249
|
+
git: true,
|
|
2250
|
+
insertions: totalInsertions,
|
|
2251
|
+
deletions: totalDeletions
|
|
2252
|
+
})
|
|
2253
|
+
);
|
|
2254
|
+
}).catch(() => {
|
|
2255
|
+
res.end(JSON.stringify({ files: [], git: false }));
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
server.middlewares.use("/via/git/diff", (req, res) => {
|
|
2259
|
+
if (req.method !== "GET") {
|
|
2260
|
+
res.statusCode = 405;
|
|
2261
|
+
res.setHeader("Content-Type", "application/json");
|
|
2262
|
+
res.end(JSON.stringify({ error: "Method not allowed" }));
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
res.setHeader("Content-Type", "application/json");
|
|
2266
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2267
|
+
const filePath = url.searchParams.get("path");
|
|
2268
|
+
ensureGit().then(async (ctx) => {
|
|
2269
|
+
if (!ctx) {
|
|
2270
|
+
res.end(JSON.stringify({ diff: "", git: false }));
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
if (filePath) {
|
|
2274
|
+
if (filePath.startsWith("/")) {
|
|
2275
|
+
res.statusCode = 400;
|
|
2276
|
+
res.end(JSON.stringify({ error: "Absolute paths not allowed" }));
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const diff = await getFileDiff(ctx.git, ctx.root, filePath);
|
|
2280
|
+
res.end(JSON.stringify({ diff, path: filePath }));
|
|
2281
|
+
} else {
|
|
2282
|
+
const staged = await ctx.git.diff(["--cached"]);
|
|
2283
|
+
const unstaged = await ctx.git.diff();
|
|
2284
|
+
const combined = staged && unstaged ? staged + "\n" + unstaged : staged || unstaged || "";
|
|
2285
|
+
res.end(JSON.stringify({ diff: combined }));
|
|
2286
|
+
}
|
|
2287
|
+
}).catch(() => {
|
|
2288
|
+
res.end(JSON.stringify({ diff: "" }));
|
|
2289
|
+
});
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
|
|
1895
2293
|
// src/sandbox.ts
|
|
1896
2294
|
import { randomUUID } from "crypto";
|
|
1897
|
-
import { readFileSync as
|
|
1898
|
-
import { join as
|
|
2295
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
2296
|
+
import { join as join5, relative as relative2 } from "path";
|
|
1899
2297
|
import { Sandbox } from "@vercel/sandbox";
|
|
1900
2298
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1901
2299
|
"node_modules",
|
|
@@ -1911,13 +2309,13 @@ function collectFiles2(dir, base) {
|
|
|
1911
2309
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
1912
2310
|
if (entry.name.startsWith(".") && SKIP_DIRS.has(entry.name)) continue;
|
|
1913
2311
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1914
|
-
const fullPath =
|
|
2312
|
+
const fullPath = join5(dir, entry.name);
|
|
1915
2313
|
const relPath = relative2(base, fullPath);
|
|
1916
2314
|
if (entry.isDirectory()) {
|
|
1917
2315
|
files.push(...collectFiles2(fullPath, base));
|
|
1918
2316
|
} else if (entry.isFile()) {
|
|
1919
2317
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
1920
|
-
files.push({ path: relPath, content:
|
|
2318
|
+
files.push({ path: relPath, content: readFileSync4(fullPath) });
|
|
1921
2319
|
}
|
|
1922
2320
|
}
|
|
1923
2321
|
return files;
|
|
@@ -2094,10 +2492,10 @@ function viagen(options) {
|
|
|
2094
2492
|
claudeBin = findClaudeBin();
|
|
2095
2493
|
logBuffer.init(projectRoot);
|
|
2096
2494
|
wrapLogger(config.logger, logBuffer);
|
|
2097
|
-
const viagenDir =
|
|
2495
|
+
const viagenDir = join6(projectRoot, ".viagen");
|
|
2098
2496
|
mkdirSync2(viagenDir, { recursive: true });
|
|
2099
2497
|
writeFileSync4(
|
|
2100
|
-
|
|
2498
|
+
join6(viagenDir, "config.json"),
|
|
2101
2499
|
JSON.stringify({
|
|
2102
2500
|
sandboxFiles: options?.sandboxFiles ?? [],
|
|
2103
2501
|
editable: options?.editable ?? []
|
|
@@ -2156,7 +2554,7 @@ ${payload.err.frame || ""}`
|
|
|
2156
2554
|
});
|
|
2157
2555
|
server.middlewares.use("/via/ui", (_req, res) => {
|
|
2158
2556
|
res.setHeader("Content-Type", "text/html");
|
|
2159
|
-
res.end(buildUiHtml({ editable: hasEditor }));
|
|
2557
|
+
res.end(buildUiHtml({ editable: hasEditor, git: true }));
|
|
2160
2558
|
});
|
|
2161
2559
|
server.middlewares.use("/via/iframe", (_req, res) => {
|
|
2162
2560
|
res.setHeader("Content-Type", "text/html");
|
|
@@ -2179,6 +2577,7 @@ ${payload.err.frame || ""}`
|
|
|
2179
2577
|
projectRoot
|
|
2180
2578
|
});
|
|
2181
2579
|
}
|
|
2580
|
+
registerGitRoutes(server, { projectRoot });
|
|
2182
2581
|
if (opts.ui) {
|
|
2183
2582
|
return () => {
|
|
2184
2583
|
server.middlewares.use(createInjectionMiddleware());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viagen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Vite dev server plugin that exposes endpoints for chatting with Claude Code SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"@anthropic-ai/claude-code": "^2.1.42",
|
|
43
43
|
"@vercel/sandbox": "^1",
|
|
44
44
|
"lucide-react": "^0.564.0",
|
|
45
|
+
"simple-git": "^3.31.1",
|
|
45
46
|
"viagen-sdk": "^0.0.0"
|
|
46
47
|
},
|
|
47
48
|
"license": "MIT",
|