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.
- package/package.json +1 -1
- package/public/assets/{_basePickBy-BpcK6jhC.js → _basePickBy-DCvHj1tL.js} +1 -1
- package/public/assets/{_baseUniq-CmudMx6k.js → _baseUniq-DGw4cJlK.js} +1 -1
- package/public/assets/{arc-BBjpe_hN.js → arc-DLvVNt9m.js} +1 -1
- package/public/assets/{architectureDiagram-2XIMDMQ5-BFCoYW28.js → architectureDiagram-2XIMDMQ5-DaSwKKd-.js} +1 -1
- package/public/assets/{blockDiagram-WCTKOSBZ-Dh31wJRm.js → blockDiagram-WCTKOSBZ-CRwNB00r.js} +1 -1
- package/public/assets/{c4Diagram-IC4MRINW-CPV76ZUM.js → c4Diagram-IC4MRINW-CCmKN7ux.js} +1 -1
- package/public/assets/channel-BQic63yO.js +1 -0
- package/public/assets/{chunk-4BX2VUAB-eU5lnLp-.js → chunk-4BX2VUAB-Dh23UX0H.js} +1 -1
- package/public/assets/{chunk-55IACEB6-CRSrEoDB.js → chunk-55IACEB6-BqjWVrdn.js} +1 -1
- package/public/assets/{chunk-FMBD7UC4-cK52FAx0.js → chunk-FMBD7UC4-8ijlV6SK.js} +1 -1
- package/public/assets/{chunk-JSJVCQXG-CudZY5_F.js → chunk-JSJVCQXG-Be_n9538.js} +1 -1
- package/public/assets/{chunk-KX2RTZJC-BZlXzthI.js → chunk-KX2RTZJC-CK3ACqcV.js} +1 -1
- package/public/assets/{chunk-NQ4KR5QH-BP6XJFdX.js → chunk-NQ4KR5QH-VSIbq9EC.js} +1 -1
- package/public/assets/{chunk-QZHKN3VN-DDY6gqIZ.js → chunk-QZHKN3VN-DkZVr5JY.js} +1 -1
- package/public/assets/{chunk-WL4C6EOR-DY15q7Tk.js → chunk-WL4C6EOR-OBys6SHI.js} +1 -1
- package/public/assets/classDiagram-VBA2DB6C-C10-rscO.js +1 -0
- package/public/assets/classDiagram-v2-RAHNMMFH-C10-rscO.js +1 -0
- package/public/assets/clone-BYsz4F6o.js +1 -0
- package/public/assets/{cose-bilkent-S5V4N54A-CTeXZoSq.js → cose-bilkent-S5V4N54A-Bf-Q2Dai.js} +1 -1
- package/public/assets/{dagre-KLK3FWXG-BOfJIpm3.js → dagre-KLK3FWXG-BRNk9lyj.js} +1 -1
- package/public/assets/{diagram-E7M64L7V-yhW3nXxA.js → diagram-E7M64L7V-DInoR231.js} +1 -1
- package/public/assets/{diagram-IFDJBPK2-B7r-HJa0.js → diagram-IFDJBPK2-B51QXDV7.js} +1 -1
- package/public/assets/{diagram-P4PSJMXO-Cl1dGq__.js → diagram-P4PSJMXO-Cfc7EB4m.js} +1 -1
- package/public/assets/{erDiagram-INFDFZHY-xb7jd7iV.js → erDiagram-INFDFZHY-JRM0U5JE.js} +1 -1
- package/public/assets/{flowDiagram-PKNHOUZH-7__LLCFp.js → flowDiagram-PKNHOUZH-DRC-Oqat.js} +1 -1
- package/public/assets/{ganttDiagram-A5KZAMGK-BRH4oYpz.js → ganttDiagram-A5KZAMGK-CpKY4Q6u.js} +1 -1
- package/public/assets/{gitGraphDiagram-K3NZZRJ6-CZ0c5437.js → gitGraphDiagram-K3NZZRJ6-BL8oYTEs.js} +1 -1
- package/public/assets/{graph-CaPDDY3I.js → graph-D3t8CMJL.js} +1 -1
- package/public/assets/{index-pqtccC7s.js → index-Czq2gCNB.js} +64 -64
- package/public/assets/{index-7DPrKRHX.css → index-OLhvO-lo.css} +1 -1
- package/public/assets/{infoDiagram-LFFYTUFH-B50sX0Jk.js → infoDiagram-LFFYTUFH-Cc1iNILI.js} +1 -1
- package/public/assets/{ishikawaDiagram-PHBUUO56-BTx9nUR_.js → ishikawaDiagram-PHBUUO56-DsnXdy3q.js} +1 -1
- package/public/assets/{journeyDiagram-4ABVD52K-DOKjshE4.js → journeyDiagram-4ABVD52K--N9Bw9-c.js} +1 -1
- package/public/assets/{kanban-definition-K7BYSVSG-DlsqM5ac.js → kanban-definition-K7BYSVSG-Bk5BTNdd.js} +1 -1
- package/public/assets/{layout-C4hgRWBc.js → layout-oyYeo2mL.js} +1 -1
- package/public/assets/{linear-pPVtYfoA.js → linear-Coxej_rX.js} +1 -1
- package/public/assets/{mindmap-definition-YRQLILUH-KXwvxrd9.js → mindmap-definition-YRQLILUH-B1Nw4K7L.js} +1 -1
- package/public/assets/{pieDiagram-SKSYHLDU-Db_hnpTO.js → pieDiagram-SKSYHLDU-CvHR1MHu.js} +1 -1
- package/public/assets/{quadrantDiagram-337W2JSQ-DBgEkZee.js → quadrantDiagram-337W2JSQ-CbLjdfkl.js} +1 -1
- package/public/assets/{requirementDiagram-Z7DCOOCP-DzYp2J9t.js → requirementDiagram-Z7DCOOCP-37nK-QYT.js} +1 -1
- package/public/assets/{sankeyDiagram-WA2Y5GQK-DqRGFuVJ.js → sankeyDiagram-WA2Y5GQK-BiiDdp5b.js} +1 -1
- package/public/assets/{sequenceDiagram-2WXFIKYE-CCATgMDC.js → sequenceDiagram-2WXFIKYE-Cv__Xn_r.js} +1 -1
- package/public/assets/{stateDiagram-RAJIS63D-DBbQtnkh.js → stateDiagram-RAJIS63D-DSH6he3A.js} +1 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-q2imzMdl.js +1 -0
- package/public/assets/{timeline-definition-YZTLITO2-AB47RPpS.js → timeline-definition-YZTLITO2-CkfQvidI.js} +1 -1
- package/public/assets/{treemap-KZPCXAKY-BwGtQefY.js → treemap-KZPCXAKY-BPaSCjJy.js} +1 -1
- package/public/assets/{vennDiagram-LZ73GAT5-CQy8NUxW.js → vennDiagram-LZ73GAT5-B8AYQN5y.js} +1 -1
- package/public/assets/{xychartDiagram-JWTSCODW-D1U0PBps.js → xychartDiagram-JWTSCODW-D7m1euVy.js} +1 -1
- package/public/index.html +2 -2
- package/public/sw.js +1 -1
- package/src/server/index.js +18 -1
- package/src/tunnel/index.js +234 -28
- package/src/utils/git.js +38 -4
- package/public/assets/channel-spGqRIlf.js +0 -1
- package/public/assets/classDiagram-VBA2DB6C-CwKQYDdP.js +0 -1
- package/public/assets/classDiagram-v2-RAHNMMFH-CwKQYDdP.js +0 -1
- package/public/assets/clone-BQhUsBH5.js +0 -1
- package/public/assets/stateDiagram-v2-FVOUBMTO-Dr2a5PWq.js +0 -1
package/src/server/index.js
CHANGED
|
@@ -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}`);
|
package/src/tunnel/index.js
CHANGED
|
@@ -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
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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,
|
|
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],
|
|
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,
|
|
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};
|