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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/dist/index.js +428 -29
  3. 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', '.env', 'vite.config.ts']
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 join5 } from "path";
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((resolve2, reject) => {
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", () => resolve2(body));
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
- ${hasEditor ? `<div class="tab-bar" id="tab-bar">
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">&#x2190;</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 File editor panel \u2500\u2500
1375
- ${hasEditor ? `
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 === 'files') loadFileList();
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((resolve2, reject) => {
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", () => resolve2(body));
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 readFileSync3, readdirSync as readdirSync2 } from "fs";
1898
- import { join as join4, relative as relative2 } from "path";
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 = join4(dir, entry.name);
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: readFileSync3(fullPath) });
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 = join5(projectRoot, ".viagen");
2495
+ const viagenDir = join6(projectRoot, ".viagen");
2098
2496
  mkdirSync2(viagenDir, { recursive: true });
2099
2497
  writeFileSync4(
2100
- join5(viagenDir, "config.json"),
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.17",
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",