termbeam 1.17.3 → 1.17.5

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 (59) hide show
  1. package/package.json +1 -1
  2. package/public/assets/{_basePickBy-BpcK6jhC.js → _basePickBy-DCvHj1tL.js} +1 -1
  3. package/public/assets/{_baseUniq-CmudMx6k.js → _baseUniq-DGw4cJlK.js} +1 -1
  4. package/public/assets/{arc-BBjpe_hN.js → arc-DLvVNt9m.js} +1 -1
  5. package/public/assets/{architectureDiagram-2XIMDMQ5-BFCoYW28.js → architectureDiagram-2XIMDMQ5-DaSwKKd-.js} +1 -1
  6. package/public/assets/{blockDiagram-WCTKOSBZ-Dh31wJRm.js → blockDiagram-WCTKOSBZ-CRwNB00r.js} +1 -1
  7. package/public/assets/{c4Diagram-IC4MRINW-CPV76ZUM.js → c4Diagram-IC4MRINW-CCmKN7ux.js} +1 -1
  8. package/public/assets/channel-BQic63yO.js +1 -0
  9. package/public/assets/{chunk-4BX2VUAB-eU5lnLp-.js → chunk-4BX2VUAB-Dh23UX0H.js} +1 -1
  10. package/public/assets/{chunk-55IACEB6-CRSrEoDB.js → chunk-55IACEB6-BqjWVrdn.js} +1 -1
  11. package/public/assets/{chunk-FMBD7UC4-cK52FAx0.js → chunk-FMBD7UC4-8ijlV6SK.js} +1 -1
  12. package/public/assets/{chunk-JSJVCQXG-CudZY5_F.js → chunk-JSJVCQXG-Be_n9538.js} +1 -1
  13. package/public/assets/{chunk-KX2RTZJC-BZlXzthI.js → chunk-KX2RTZJC-CK3ACqcV.js} +1 -1
  14. package/public/assets/{chunk-NQ4KR5QH-BP6XJFdX.js → chunk-NQ4KR5QH-VSIbq9EC.js} +1 -1
  15. package/public/assets/{chunk-QZHKN3VN-DDY6gqIZ.js → chunk-QZHKN3VN-DkZVr5JY.js} +1 -1
  16. package/public/assets/{chunk-WL4C6EOR-DY15q7Tk.js → chunk-WL4C6EOR-OBys6SHI.js} +1 -1
  17. package/public/assets/classDiagram-VBA2DB6C-C10-rscO.js +1 -0
  18. package/public/assets/classDiagram-v2-RAHNMMFH-C10-rscO.js +1 -0
  19. package/public/assets/clone-BYsz4F6o.js +1 -0
  20. package/public/assets/{cose-bilkent-S5V4N54A-CTeXZoSq.js → cose-bilkent-S5V4N54A-Bf-Q2Dai.js} +1 -1
  21. package/public/assets/{dagre-KLK3FWXG-BOfJIpm3.js → dagre-KLK3FWXG-BRNk9lyj.js} +1 -1
  22. package/public/assets/{diagram-E7M64L7V-yhW3nXxA.js → diagram-E7M64L7V-DInoR231.js} +1 -1
  23. package/public/assets/{diagram-IFDJBPK2-B7r-HJa0.js → diagram-IFDJBPK2-B51QXDV7.js} +1 -1
  24. package/public/assets/{diagram-P4PSJMXO-Cl1dGq__.js → diagram-P4PSJMXO-Cfc7EB4m.js} +1 -1
  25. package/public/assets/{erDiagram-INFDFZHY-xb7jd7iV.js → erDiagram-INFDFZHY-JRM0U5JE.js} +1 -1
  26. package/public/assets/{flowDiagram-PKNHOUZH-7__LLCFp.js → flowDiagram-PKNHOUZH-DRC-Oqat.js} +1 -1
  27. package/public/assets/{ganttDiagram-A5KZAMGK-BRH4oYpz.js → ganttDiagram-A5KZAMGK-CpKY4Q6u.js} +1 -1
  28. package/public/assets/{gitGraphDiagram-K3NZZRJ6-CZ0c5437.js → gitGraphDiagram-K3NZZRJ6-BL8oYTEs.js} +1 -1
  29. package/public/assets/{graph-CaPDDY3I.js → graph-D3t8CMJL.js} +1 -1
  30. package/public/assets/{index-pqtccC7s.js → index-Czq2gCNB.js} +64 -64
  31. package/public/assets/{index-7DPrKRHX.css → index-OLhvO-lo.css} +1 -1
  32. package/public/assets/{infoDiagram-LFFYTUFH-B50sX0Jk.js → infoDiagram-LFFYTUFH-Cc1iNILI.js} +1 -1
  33. package/public/assets/{ishikawaDiagram-PHBUUO56-BTx9nUR_.js → ishikawaDiagram-PHBUUO56-DsnXdy3q.js} +1 -1
  34. package/public/assets/{journeyDiagram-4ABVD52K-DOKjshE4.js → journeyDiagram-4ABVD52K--N9Bw9-c.js} +1 -1
  35. package/public/assets/{kanban-definition-K7BYSVSG-DlsqM5ac.js → kanban-definition-K7BYSVSG-Bk5BTNdd.js} +1 -1
  36. package/public/assets/{layout-C4hgRWBc.js → layout-oyYeo2mL.js} +1 -1
  37. package/public/assets/{linear-pPVtYfoA.js → linear-Coxej_rX.js} +1 -1
  38. package/public/assets/{mindmap-definition-YRQLILUH-KXwvxrd9.js → mindmap-definition-YRQLILUH-B1Nw4K7L.js} +1 -1
  39. package/public/assets/{pieDiagram-SKSYHLDU-Db_hnpTO.js → pieDiagram-SKSYHLDU-CvHR1MHu.js} +1 -1
  40. package/public/assets/{quadrantDiagram-337W2JSQ-DBgEkZee.js → quadrantDiagram-337W2JSQ-CbLjdfkl.js} +1 -1
  41. package/public/assets/{requirementDiagram-Z7DCOOCP-DzYp2J9t.js → requirementDiagram-Z7DCOOCP-37nK-QYT.js} +1 -1
  42. package/public/assets/{sankeyDiagram-WA2Y5GQK-DqRGFuVJ.js → sankeyDiagram-WA2Y5GQK-BiiDdp5b.js} +1 -1
  43. package/public/assets/{sequenceDiagram-2WXFIKYE-CCATgMDC.js → sequenceDiagram-2WXFIKYE-Cv__Xn_r.js} +1 -1
  44. package/public/assets/{stateDiagram-RAJIS63D-DBbQtnkh.js → stateDiagram-RAJIS63D-DSH6he3A.js} +1 -1
  45. package/public/assets/stateDiagram-v2-FVOUBMTO-q2imzMdl.js +1 -0
  46. package/public/assets/{timeline-definition-YZTLITO2-AB47RPpS.js → timeline-definition-YZTLITO2-CkfQvidI.js} +1 -1
  47. package/public/assets/{treemap-KZPCXAKY-BwGtQefY.js → treemap-KZPCXAKY-BPaSCjJy.js} +1 -1
  48. package/public/assets/{vennDiagram-LZ73GAT5-CQy8NUxW.js → vennDiagram-LZ73GAT5-B8AYQN5y.js} +1 -1
  49. package/public/assets/{xychartDiagram-JWTSCODW-D1U0PBps.js → xychartDiagram-JWTSCODW-D7m1euVy.js} +1 -1
  50. package/public/index.html +2 -2
  51. package/public/sw.js +1 -1
  52. package/src/server/index.js +18 -1
  53. package/src/tunnel/index.js +234 -28
  54. package/src/utils/git.js +38 -4
  55. package/public/assets/channel-spGqRIlf.js +0 -1
  56. package/public/assets/classDiagram-VBA2DB6C-CwKQYDdP.js +0 -1
  57. package/public/assets/classDiagram-v2-RAHNMMFH-CwKQYDdP.js +0 -1
  58. package/public/assets/clone-BQhUsBH5.js +0 -1
  59. package/public/assets/stateDiagram-v2-FVOUBMTO-Dr2a5PWq.js +0 -1
@@ -13,7 +13,7 @@ const { createAuth } = require('./auth');
13
13
  const { SessionManager } = require('./sessions');
14
14
  const { setupRoutes, cleanupUploadedFiles } = require('./routes');
15
15
  const { setupWebSocket } = require('./websocket');
16
- const { startTunnel, cleanupTunnel, findDevtunnel } = require('../tunnel');
16
+ const { startTunnel, cleanupTunnel, findDevtunnel, tunnelEvents } = require('../tunnel');
17
17
  const { createPreviewProxy } = require('./preview');
18
18
  const { writeConnectionConfig, removeConnectionConfig } = require('../cli/resume');
19
19
  const { checkForUpdate, detectInstallMethod } = require('../utils/update-check');
@@ -110,6 +110,7 @@ function createTermBeamServer(overrides = {}) {
110
110
  auth.cleanup();
111
111
  sessions.shutdown();
112
112
  cleanupUploadedFiles();
113
+ tunnelEvents.removeAllListeners();
113
114
  cleanupTunnel();
114
115
  removeConnectionConfig();
115
116
  for (const client of wss.clients) {
@@ -257,6 +258,22 @@ function createTermBeamServer(overrides = {}) {
257
258
  log.warn('Tunnel failed to start, falling back to LAN-only');
258
259
  console.log(' ⚠️ Tunnel failed to start. Using LAN only.');
259
260
  }
261
+
262
+ // Tunnel watchdog events
263
+ tunnelEvents.on('disconnected', () => {
264
+ log.warn('Tunnel disconnected — watchdog will attempt to reconnect');
265
+ });
266
+ tunnelEvents.on('reconnecting', ({ attempt, delay }) => {
267
+ log.info(`Tunnel reconnecting (attempt ${attempt}, backoff ${delay}ms)`);
268
+ });
269
+ tunnelEvents.on('connected', ({ url }) => {
270
+ log.info(`Tunnel connected: ${url}`);
271
+ });
272
+ tunnelEvents.on('failed', ({ attempts }) => {
273
+ log.error(
274
+ `Tunnel watchdog gave up after ${attempts} attempts — tunnel URL is unreachable`,
275
+ );
276
+ });
260
277
  }
261
278
 
262
279
  console.log(` Shell: ${config.shell}`);
@@ -1,7 +1,8 @@
1
- const { execSync, execFileSync, spawn } = require('child_process');
1
+ const { execSync, execFileSync, execFile, spawn } = require('child_process');
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
4
  const os = require('os');
5
+ const EventEmitter = require('events');
5
6
  const log = require('../utils/logger');
6
7
  const { promptInstall } = require('./install');
7
8
 
@@ -12,6 +13,19 @@ let tunnelId = null;
12
13
  let tunnelProc = null;
13
14
  let devtunnelCmd = 'devtunnel';
14
15
 
16
+ // --- Watchdog state ---
17
+ const tunnelEvents = new EventEmitter();
18
+ let healthCheckInterval = null;
19
+ let consecutiveFailures = 0;
20
+ let restartAttempts = 0;
21
+ let isRestarting = false;
22
+ let restartTimer = null;
23
+
24
+ const HEALTH_CHECK_INTERVAL = 30_000; // 30s between checks
25
+ const HEALTH_CHECK_GRACE = 2; // 2 consecutive failures before restart
26
+ const MAX_RESTART_ATTEMPTS = 10;
27
+ const BACKOFF_DELAYS = [1000, 2000, 5000, 10_000, 15_000, 30_000]; // then stays at 30s
28
+
15
29
  const SAFE_ID_RE = /^[a-zA-Z0-9._-]+$/;
16
30
 
17
31
  const DEVICE_CODE_INITIAL_TIMEOUT = 15000;
@@ -135,6 +149,203 @@ function isTunnelValid(id) {
135
149
 
136
150
  let isPersisted = false;
137
151
 
152
+ // --- Watchdog: health check & auto-restart ---
153
+
154
+ function checkTunnelHealth() {
155
+ if (!tunnelId || !tunnelProc || isRestarting) return;
156
+
157
+ const abortCtrl = new AbortController();
158
+ const timer = setTimeout(() => abortCtrl.abort(), 10_000);
159
+
160
+ execFile(
161
+ devtunnelCmd,
162
+ ['show', tunnelId],
163
+ { encoding: 'utf-8', signal: abortCtrl.signal },
164
+ (err, stdout) => {
165
+ clearTimeout(timer);
166
+
167
+ if (err) {
168
+ consecutiveFailures++;
169
+ log.warn(
170
+ `Tunnel health check error: ${err.message} (${consecutiveFailures}/${HEALTH_CHECK_GRACE})`,
171
+ );
172
+ if (consecutiveFailures >= HEALTH_CHECK_GRACE) {
173
+ handleTunnelFailure();
174
+ }
175
+ return;
176
+ }
177
+
178
+ const match = stdout.match(/Host connections\s*:\s*(\d+)/i);
179
+ if (!match) {
180
+ consecutiveFailures++;
181
+ log.warn(
182
+ `Tunnel health check: could not parse host connections (${consecutiveFailures}/${HEALTH_CHECK_GRACE})`,
183
+ );
184
+ if (consecutiveFailures >= HEALTH_CHECK_GRACE) {
185
+ handleTunnelFailure();
186
+ }
187
+ return;
188
+ }
189
+
190
+ const hostConns = parseInt(match[1], 10);
191
+ if (hostConns > 0) {
192
+ if (consecutiveFailures > 0) {
193
+ log.info(`Tunnel health restored (${hostConns} host connection(s))`);
194
+ }
195
+ consecutiveFailures = 0;
196
+ return;
197
+ }
198
+
199
+ consecutiveFailures++;
200
+ log.warn(
201
+ `Tunnel health check: 0 host connections (${consecutiveFailures}/${HEALTH_CHECK_GRACE})`,
202
+ );
203
+
204
+ if (consecutiveFailures >= HEALTH_CHECK_GRACE) {
205
+ log.warn('Tunnel connection lost — initiating restart');
206
+ handleTunnelFailure();
207
+ }
208
+ },
209
+ );
210
+ }
211
+
212
+ function startHealthCheck() {
213
+ stopHealthCheck();
214
+ consecutiveFailures = 0;
215
+ healthCheckInterval = setInterval(checkTunnelHealth, HEALTH_CHECK_INTERVAL);
216
+ healthCheckInterval.unref();
217
+ }
218
+
219
+ function stopHealthCheck() {
220
+ if (healthCheckInterval) {
221
+ clearInterval(healthCheckInterval);
222
+ healthCheckInterval = null;
223
+ }
224
+ }
225
+
226
+ function handleTunnelFailure() {
227
+ if (isRestarting) return;
228
+ stopHealthCheck();
229
+
230
+ // Kill the zombie process
231
+ if (tunnelProc) {
232
+ try {
233
+ if (process.platform === 'win32' && tunnelProc.pid) {
234
+ try {
235
+ execFileSync('taskkill', ['/pid', String(tunnelProc.pid), '/T', '/F'], {
236
+ stdio: 'pipe',
237
+ timeout: 5000,
238
+ });
239
+ } catch {
240
+ /* best effort */
241
+ }
242
+ } else {
243
+ tunnelProc.kill('SIGKILL');
244
+ }
245
+ } catch {
246
+ /* best effort */
247
+ }
248
+ tunnelProc = null;
249
+ }
250
+
251
+ tunnelEvents.emit('disconnected');
252
+ scheduleRestart();
253
+ }
254
+
255
+ function scheduleRestart() {
256
+ if (restartAttempts >= MAX_RESTART_ATTEMPTS) {
257
+ log.error(
258
+ `Tunnel restart failed after ${MAX_RESTART_ATTEMPTS} attempts — giving up. Tunnel URL is unreachable.`,
259
+ );
260
+ tunnelEvents.emit('failed', { attempts: restartAttempts });
261
+ isRestarting = false;
262
+ return;
263
+ }
264
+
265
+ isRestarting = true;
266
+ const delay = BACKOFF_DELAYS[Math.min(restartAttempts, BACKOFF_DELAYS.length - 1)];
267
+ restartAttempts++;
268
+
269
+ log.info(`Restarting tunnel in ${delay}ms (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})`);
270
+ tunnelEvents.emit('reconnecting', { attempt: restartAttempts, delay });
271
+
272
+ restartTimer = setTimeout(async () => {
273
+ restartTimer = null;
274
+ try {
275
+ const result = await hostTunnel();
276
+ if (result) {
277
+ log.info('Tunnel reconnected successfully');
278
+ restartAttempts = 0;
279
+ isRestarting = false;
280
+ tunnelEvents.emit('connected', { url: result.url });
281
+ startHealthCheck();
282
+ } else {
283
+ log.warn('Tunnel restart returned no URL');
284
+ isRestarting = false;
285
+ scheduleRestart();
286
+ }
287
+ } catch (err) {
288
+ log.error(`Tunnel restart error: ${err.message}`);
289
+ isRestarting = false;
290
+ scheduleRestart();
291
+ }
292
+ }, delay);
293
+ restartTimer.unref();
294
+ }
295
+
296
+ /**
297
+ * Spawn `devtunnel host` for the current tunnelId and wait for the URL.
298
+ * Used by both initial start and watchdog restarts.
299
+ */
300
+ function hostTunnel() {
301
+ const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
302
+ stdio: ['pipe', 'pipe', 'pipe'],
303
+ });
304
+ tunnelProc = hostProc;
305
+
306
+ // Attach exit handler for crash detection
307
+ hostProc.on('exit', (code, signal) => {
308
+ if (tunnelProc !== hostProc) return; // stale reference
309
+ log.warn(`Tunnel process exited (code=${code}, signal=${signal})`);
310
+ tunnelProc = null;
311
+ if (!isRestarting) {
312
+ tunnelEvents.emit('disconnected');
313
+ scheduleRestart();
314
+ }
315
+ });
316
+
317
+ return new Promise((resolve) => {
318
+ let output = '';
319
+ const timeout = setTimeout(() => {
320
+ // Kill the process if URL wasn't detected in time
321
+ try {
322
+ hostProc.kill('SIGKILL');
323
+ } catch {
324
+ /* best effort */
325
+ }
326
+ if (tunnelProc === hostProc) tunnelProc = null;
327
+ resolve(null);
328
+ }, 15_000);
329
+
330
+ hostProc.stdout.on('data', (data) => {
331
+ output += data.toString();
332
+ const match = output.match(/(https:\/\/[^\s]+devtunnels\.ms[^\s]*)/);
333
+ if (match) {
334
+ clearTimeout(timeout);
335
+ resolve({ url: match[1] });
336
+ }
337
+ });
338
+ hostProc.stderr.on('data', (data) => {
339
+ output += data.toString();
340
+ });
341
+ hostProc.on('error', (err) => {
342
+ log.error(`Tunnel process error: ${err.message}`);
343
+ clearTimeout(timeout);
344
+ resolve(null);
345
+ });
346
+ });
347
+ }
348
+
138
349
  async function startTunnel(port, options = {}) {
139
350
  // Check if devtunnel CLI is installed
140
351
  let found = findDevtunnel();
@@ -254,32 +465,14 @@ async function startTunnel(port, options = {}) {
254
465
  log.info('Tunnel access: private (owner-only via Microsoft login)');
255
466
  }
256
467
 
257
- const hostProc = spawn(devtunnelCmd, ['host', tunnelId], {
258
- stdio: ['pipe', 'pipe', 'pipe'],
259
- });
260
- tunnelProc = hostProc;
261
-
262
- return new Promise((resolve) => {
263
- let output = '';
264
- const timeout = setTimeout(() => resolve(null), 15000);
265
-
266
- hostProc.stdout.on('data', (data) => {
267
- output += data.toString();
268
- const match = output.match(/(https:\/\/[^\s]+devtunnels\.ms[^\s]*)/);
269
- if (match) {
270
- clearTimeout(timeout);
271
- resolve({ url: match[1], mode: tunnelMode, expiry: tunnelExpiry });
272
- }
273
- });
274
- hostProc.stderr.on('data', (data) => {
275
- output += data.toString();
276
- });
277
- hostProc.on('error', (err) => {
278
- log.error(`Tunnel process error: ${err.message}`);
279
- clearTimeout(timeout);
280
- resolve(null);
281
- });
282
- });
468
+ const result = await hostTunnel();
469
+ if (result) {
470
+ result.mode = tunnelMode;
471
+ result.expiry = tunnelExpiry;
472
+ startHealthCheck();
473
+ tunnelEvents.emit('connected', { url: result.url });
474
+ }
475
+ return result;
283
476
  } catch (e) {
284
477
  log.error(`Tunnel error: ${e.message}`);
285
478
  return null;
@@ -287,6 +480,14 @@ async function startTunnel(port, options = {}) {
287
480
  }
288
481
 
289
482
  function cleanupTunnel() {
483
+ // Stop watchdog first to prevent restart during cleanup
484
+ stopHealthCheck();
485
+ isRestarting = true; // prevent exit handler from restarting
486
+ if (restartTimer) {
487
+ clearTimeout(restartTimer);
488
+ restartTimer = null;
489
+ }
490
+
290
491
  const id = tunnelId;
291
492
  if (tunnelProc) {
292
493
  try {
@@ -321,6 +522,11 @@ function cleanupTunnel() {
321
522
  }
322
523
  }
323
524
  }
525
+
526
+ // Reset watchdog state
527
+ consecutiveFailures = 0;
528
+ restartAttempts = 0;
529
+ isRestarting = false;
324
530
  }
325
531
 
326
- module.exports = { startTunnel, cleanupTunnel, findDevtunnel };
532
+ module.exports = { startTunnel, cleanupTunnel, findDevtunnel, tunnelEvents };
package/src/utils/git.js CHANGED
@@ -132,6 +132,32 @@ const MAX_DIFF_BUFFER = 1024 * 1024; // 1 MB
132
132
  const MAX_BLAME_BUFFER = 2 * 1024 * 1024; // 2 MB
133
133
  const MAX_LOG_BUFFER = 1024 * 1024; // 1 MB
134
134
 
135
+ // git status --porcelain returns paths relative to the repo root, so
136
+ // commands that accept those paths (diff, blame, log --follow) must also
137
+ // run from the repo root. Cache per-cwd to avoid repeated rev-parse calls.
138
+ const gitRootCache = new Map();
139
+
140
+ async function getGitRoot(cwd) {
141
+ if (gitRootCache.has(cwd)) return gitRootCache.get(cwd);
142
+ try {
143
+ const root = await new Promise((resolve, reject) => {
144
+ require('child_process').execFile(
145
+ 'git',
146
+ ['rev-parse', '--show-toplevel'],
147
+ { cwd, timeout: GIT_TIMEOUT },
148
+ (err, stdout) => {
149
+ if (err) return reject(err);
150
+ resolve(stdout.trim());
151
+ },
152
+ );
153
+ });
154
+ gitRootCache.set(cwd, root);
155
+ return root;
156
+ } catch {
157
+ return cwd; // fallback to session cwd
158
+ }
159
+ }
160
+
135
161
  async function gitAsync(args, cwd, options = {}) {
136
162
  return new Promise((resolve, reject) => {
137
163
  require('child_process').execFile(
@@ -302,6 +328,8 @@ async function parseDiffOutput(raw, filePath) {
302
328
 
303
329
  async function getFileDiff(cwd, filePath, options = {}) {
304
330
  const { staged = false, untracked = false, context = 3 } = options;
331
+ // git status returns paths relative to the repo root, so run diff from there
332
+ const root = await getGitRoot(cwd);
305
333
 
306
334
  try {
307
335
  // Untracked files: use --no-index to diff against the null device
@@ -312,7 +340,7 @@ async function getFileDiff(cwd, filePath, options = {}) {
312
340
  'git',
313
341
  ['diff', '--no-index', '--no-color', `--unified=${context}`, '--', nullDevice, filePath],
314
342
  {
315
- cwd,
343
+ cwd: root,
316
344
  timeout: GIT_TIMEOUT,
317
345
  maxBuffer: MAX_DIFF_BUFFER,
318
346
  },
@@ -330,7 +358,7 @@ async function getFileDiff(cwd, filePath, options = {}) {
330
358
  if (staged) args.push('--cached');
331
359
  args.push('--', filePath);
332
360
 
333
- const raw = await gitAsync(args, cwd, { maxBuffer: MAX_DIFF_BUFFER });
361
+ const raw = await gitAsync(args, root, { maxBuffer: MAX_DIFF_BUFFER });
334
362
  return parseDiffOutput(raw, filePath);
335
363
  } catch (err) {
336
364
  // Empty diff or git error
@@ -350,9 +378,11 @@ async function getFileDiff(cwd, filePath, options = {}) {
350
378
 
351
379
  async function getFileBlame(cwd, filePath) {
352
380
  const result = { file: filePath, lines: [] };
381
+ // git status returns paths relative to the repo root, so run blame from there
382
+ const root = await getGitRoot(cwd);
353
383
 
354
384
  try {
355
- const raw = await gitAsync(['blame', '--porcelain', '--', filePath], cwd, {
385
+ const raw = await gitAsync(['blame', '--porcelain', '--', filePath], root, {
356
386
  maxBuffer: MAX_BLAME_BUFFER,
357
387
  });
358
388
 
@@ -427,11 +457,14 @@ async function getGitLog(cwd, options = {}) {
427
457
 
428
458
  try {
429
459
  const args = ['log', `--format=${LOG_SEPARATOR}${LOG_FORMAT}`, `-n`, String(limit)];
460
+ // When filtering by file, run from repo root since paths are repo-root-relative
461
+ let runCwd = cwd;
430
462
  if (options.file) {
431
463
  args.push('--follow', '--', options.file);
464
+ runCwd = await getGitRoot(cwd);
432
465
  }
433
466
 
434
- const raw = await gitAsync(args, cwd, { maxBuffer: MAX_LOG_BUFFER });
467
+ const raw = await gitAsync(args, runCwd, { maxBuffer: MAX_LOG_BUFFER });
435
468
 
436
469
  const entries = raw.split(LOG_SEPARATOR).filter((e) => e.trim());
437
470
  for (const entry of entries) {
@@ -462,4 +495,5 @@ module.exports = {
462
495
  getFileDiff,
463
496
  getFileBlame,
464
497
  getGitLog,
498
+ getGitRoot,
465
499
  };
@@ -1 +0,0 @@
1
- import{aq as o,ar as n}from"./index-pqtccC7s.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-WL4C6EOR-DY15q7Tk.js";import{_ as i}from"./index-pqtccC7s.js";import"./chunk-FMBD7UC4-cK52FAx0.js";import"./chunk-JSJVCQXG-CudZY5_F.js";import"./chunk-55IACEB6-CRSrEoDB.js";import"./chunk-KX2RTZJC-BZlXzthI.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{s as a,c as s,a as e,C as t}from"./chunk-WL4C6EOR-DY15q7Tk.js";import{_ as i}from"./index-pqtccC7s.js";import"./chunk-FMBD7UC4-cK52FAx0.js";import"./chunk-JSJVCQXG-CudZY5_F.js";import"./chunk-55IACEB6-CRSrEoDB.js";import"./chunk-KX2RTZJC-BZlXzthI.js";var u={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{u as diagram};
@@ -1 +0,0 @@
1
- import{b as r}from"./_baseUniq-CmudMx6k.js";var e=4;function a(o){return r(o,e)}export{a as c};
@@ -1 +0,0 @@
1
- import{s as t,b as r,a,S as s}from"./chunk-NQ4KR5QH-BP6XJFdX.js";import{_ as i}from"./index-pqtccC7s.js";import"./chunk-55IACEB6-CRSrEoDB.js";import"./chunk-KX2RTZJC-BZlXzthI.js";var l={parser:a,get db(){return new s(2)},renderer:r,styles:t,init:i(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")};export{l as diagram};