@burdenoff/vibe-agent 1.0.0

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 (100) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +22 -0
  3. package/README.md +290 -0
  4. package/dist/app.d.ts +15 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +445 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +1043 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/db/schema.d.ts +145 -0
  13. package/dist/db/schema.d.ts.map +1 -0
  14. package/dist/db/schema.js +536 -0
  15. package/dist/db/schema.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +61 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/middleware/ModuleAuth.d.ts +61 -0
  21. package/dist/middleware/ModuleAuth.d.ts.map +1 -0
  22. package/dist/middleware/ModuleAuth.js +220 -0
  23. package/dist/middleware/ModuleAuth.js.map +1 -0
  24. package/dist/middleware/auth.d.ts +3 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +11 -0
  27. package/dist/middleware/auth.js.map +1 -0
  28. package/dist/migrations/remove-notes-prompts.d.ts +13 -0
  29. package/dist/migrations/remove-notes-prompts.d.ts.map +1 -0
  30. package/dist/migrations/remove-notes-prompts.js +148 -0
  31. package/dist/migrations/remove-notes-prompts.js.map +1 -0
  32. package/dist/routes/bookmarks.d.ts +3 -0
  33. package/dist/routes/bookmarks.d.ts.map +1 -0
  34. package/dist/routes/bookmarks.js +186 -0
  35. package/dist/routes/bookmarks.js.map +1 -0
  36. package/dist/routes/config.d.ts +3 -0
  37. package/dist/routes/config.d.ts.map +1 -0
  38. package/dist/routes/config.js +108 -0
  39. package/dist/routes/config.js.map +1 -0
  40. package/dist/routes/files.d.ts +3 -0
  41. package/dist/routes/files.d.ts.map +1 -0
  42. package/dist/routes/files.js +471 -0
  43. package/dist/routes/files.js.map +1 -0
  44. package/dist/routes/git.d.ts +3 -0
  45. package/dist/routes/git.d.ts.map +1 -0
  46. package/dist/routes/git.js +498 -0
  47. package/dist/routes/git.js.map +1 -0
  48. package/dist/routes/moduleRegistry.d.ts +41 -0
  49. package/dist/routes/moduleRegistry.d.ts.map +1 -0
  50. package/dist/routes/moduleRegistry.js +356 -0
  51. package/dist/routes/moduleRegistry.js.map +1 -0
  52. package/dist/routes/notifications.d.ts +3 -0
  53. package/dist/routes/notifications.d.ts.map +1 -0
  54. package/dist/routes/notifications.js +250 -0
  55. package/dist/routes/notifications.js.map +1 -0
  56. package/dist/routes/port-forward.d.ts +3 -0
  57. package/dist/routes/port-forward.d.ts.map +1 -0
  58. package/dist/routes/port-forward.js +205 -0
  59. package/dist/routes/port-forward.js.map +1 -0
  60. package/dist/routes/projects.d.ts +3 -0
  61. package/dist/routes/projects.d.ts.map +1 -0
  62. package/dist/routes/projects.js +442 -0
  63. package/dist/routes/projects.js.map +1 -0
  64. package/dist/routes/ssh.d.ts +3 -0
  65. package/dist/routes/ssh.d.ts.map +1 -0
  66. package/dist/routes/ssh.js +192 -0
  67. package/dist/routes/ssh.js.map +1 -0
  68. package/dist/routes/tasks.d.ts +3 -0
  69. package/dist/routes/tasks.d.ts.map +1 -0
  70. package/dist/routes/tasks.js +183 -0
  71. package/dist/routes/tasks.js.map +1 -0
  72. package/dist/routes/tmux.d.ts +3 -0
  73. package/dist/routes/tmux.d.ts.map +1 -0
  74. package/dist/routes/tmux.js +1191 -0
  75. package/dist/routes/tmux.js.map +1 -0
  76. package/dist/routes/tunnel.d.ts +25 -0
  77. package/dist/routes/tunnel.d.ts.map +1 -0
  78. package/dist/routes/tunnel.js +449 -0
  79. package/dist/routes/tunnel.js.map +1 -0
  80. package/dist/services/ModulePermissions.d.ts +100 -0
  81. package/dist/services/ModulePermissions.d.ts.map +1 -0
  82. package/dist/services/ModulePermissions.js +312 -0
  83. package/dist/services/ModulePermissions.js.map +1 -0
  84. package/dist/services/ModuleRegistryService.d.ts +152 -0
  85. package/dist/services/ModuleRegistryService.d.ts.map +1 -0
  86. package/dist/services/ModuleRegistryService.js +522 -0
  87. package/dist/services/ModuleRegistryService.js.map +1 -0
  88. package/dist/services/agent.service.d.ts +19 -0
  89. package/dist/services/agent.service.d.ts.map +1 -0
  90. package/dist/services/agent.service.js +88 -0
  91. package/dist/services/agent.service.js.map +1 -0
  92. package/dist/services/bootstrap.d.ts +22 -0
  93. package/dist/services/bootstrap.d.ts.map +1 -0
  94. package/dist/services/bootstrap.js +206 -0
  95. package/dist/services/bootstrap.js.map +1 -0
  96. package/dist/services/service-manager.d.ts +50 -0
  97. package/dist/services/service-manager.d.ts.map +1 -0
  98. package/dist/services/service-manager.js +382 -0
  99. package/dist/services/service-manager.js.map +1 -0
  100. package/package.json +107 -0
@@ -0,0 +1,1191 @@
1
+ import { spawn } from 'child_process';
2
+ import { getAgentTunnelUrl } from './tunnel.js';
3
+ export const tmuxRoutes = async (fastify) => {
4
+ // Get all tmux sessions
5
+ fastify.get('/', async (_request, _reply) => {
6
+ const sessions = fastify.db.getAllTmuxSessions();
7
+ return { sessions };
8
+ });
9
+ // Get all system tmux sessions (including unmanaged ones)
10
+ fastify.get('/system', async (_request, reply) => {
11
+ try {
12
+ const systemSessions = await getSystemTmuxSessions();
13
+ const managedSessions = fastify.db.getAllTmuxSessions();
14
+ // Combine system sessions with managed session metadata
15
+ const enrichedSessions = systemSessions.map(systemSession => {
16
+ const managedSession = managedSessions.find(ms => ms.sessionName === systemSession.name);
17
+ return {
18
+ ...systemSession,
19
+ isManaged: !!managedSession,
20
+ managedSessionId: managedSession?.id,
21
+ projectId: managedSession?.projectId,
22
+ ttydPort: managedSession?.ttydPort,
23
+ ttydPid: managedSession?.ttydPid,
24
+ createdAt: managedSession?.createdAt || systemSession.created,
25
+ };
26
+ });
27
+ return { sessions: enrichedSessions };
28
+ }
29
+ catch (error) {
30
+ return reply.code(500).send({
31
+ error: 'Failed to get system tmux sessions',
32
+ details: error instanceof Error ? error.message : 'Unknown error'
33
+ });
34
+ }
35
+ });
36
+ // Get all system ttyd processes
37
+ fastify.get('/system/ttyd', async (_request, reply) => {
38
+ try {
39
+ const ttydProcesses = await getSystemTtydProcesses();
40
+ return { processes: ttydProcesses };
41
+ }
42
+ catch (error) {
43
+ return reply.code(500).send({
44
+ error: 'Failed to get system ttyd processes',
45
+ details: error instanceof Error ? error.message : 'Unknown error'
46
+ });
47
+ }
48
+ });
49
+ // Kill system tmux sessions (bulk)
50
+ fastify.post('/system/kill', async (request, reply) => {
51
+ const { sessionNames, force } = request.body;
52
+ if (!sessionNames || !Array.isArray(sessionNames)) {
53
+ return reply.code(400).send({ error: 'sessionNames array is required' });
54
+ }
55
+ try {
56
+ const results = await killSystemTmuxSessions(sessionNames, force);
57
+ return { results };
58
+ }
59
+ catch (error) {
60
+ return reply.code(500).send({
61
+ error: 'Failed to kill system tmux sessions',
62
+ details: error instanceof Error ? error.message : 'Unknown error'
63
+ });
64
+ }
65
+ });
66
+ // Kill system ttyd processes (bulk)
67
+ fastify.post('/system/ttyd/kill', async (request, reply) => {
68
+ const { pids, force } = request.body;
69
+ if (!pids || !Array.isArray(pids)) {
70
+ return reply.code(400).send({ error: 'pids array is required' });
71
+ }
72
+ try {
73
+ const results = await killSystemTtydProcesses(pids, force);
74
+ return { results };
75
+ }
76
+ catch (error) {
77
+ return reply.code(500).send({
78
+ error: 'Failed to kill system ttyd processes',
79
+ details: error instanceof Error ? error.message : 'Unknown error'
80
+ });
81
+ }
82
+ });
83
+ // Get session by ID
84
+ fastify.get('/:id', async (request, reply) => {
85
+ const { id } = request.params;
86
+ const session = fastify.db.getTmuxSession(id);
87
+ if (!session) {
88
+ return reply.code(404).send({ error: 'Session not found' });
89
+ }
90
+ return { session };
91
+ });
92
+ // ── Bulk session health check ──────────────────────────────────────
93
+ // POST /health-check — accepts an array of session IDs and returns their
94
+ // actual status by checking if the tmux session and ttyd process are alive.
95
+ // Called periodically by the backend to reconcile DB state.
96
+ fastify.post('/health-check', async (request, _reply) => {
97
+ const { sessionIds } = request.body;
98
+ if (!sessionIds || !Array.isArray(sessionIds)) {
99
+ return { results: [] };
100
+ }
101
+ const results = [];
102
+ for (const sessionId of sessionIds) {
103
+ const dbSession = fastify.db.getTmuxSession(sessionId);
104
+ if (!dbSession) {
105
+ results.push({ sessionId, tmuxAlive: false, ttydAlive: false, ttydPort: null, status: 'unknown' });
106
+ continue;
107
+ }
108
+ // Check if tmux session is alive
109
+ let tmuxAlive = false;
110
+ try {
111
+ const checkProc = spawn('tmux', ['has-session', '-t', dbSession.sessionName]);
112
+ tmuxAlive = await new Promise((resolve) => {
113
+ checkProc.on('close', (code) => resolve(code === 0));
114
+ checkProc.on('error', () => resolve(false));
115
+ });
116
+ }
117
+ catch {
118
+ tmuxAlive = false;
119
+ }
120
+ // Check if ttyd process is alive
121
+ let ttydAlive = false;
122
+ if (dbSession.ttydPid) {
123
+ try {
124
+ process.kill(dbSession.ttydPid, 0);
125
+ ttydAlive = true;
126
+ }
127
+ catch {
128
+ ttydAlive = false;
129
+ }
130
+ }
131
+ const status = tmuxAlive ? 'running' : 'dead';
132
+ results.push({
133
+ sessionId,
134
+ tmuxAlive,
135
+ ttydAlive,
136
+ ttydPort: dbSession.ttydPort ?? null,
137
+ status,
138
+ });
139
+ }
140
+ return { results };
141
+ });
142
+ // Create new tmux session (idempotent — re-adopts existing tmux sessions)
143
+ fastify.post('/create', async (request, reply) => {
144
+ const { sessionId, projectId, sessionName, windowName, command, startDirectory } = request.body;
145
+ let finalSessionName = sessionName || `vibecontrols-${Date.now()}`;
146
+ const finalWindowName = windowName || 'main';
147
+ try {
148
+ // Check if we already have this sessionId in our DB (idempotent re-call)
149
+ const existingDbSession = fastify.db.getTmuxSession(sessionId);
150
+ if (existingDbSession) {
151
+ // Already tracked — verify the tmux session still exists
152
+ const checkProc = spawn('tmux', ['has-session', '-t', existingDbSession.sessionName]);
153
+ const checkOk = await new Promise((resolve) => {
154
+ checkProc.on('close', (code) => resolve(code === 0));
155
+ checkProc.on('error', () => resolve(false));
156
+ });
157
+ if (checkOk) {
158
+ return { session: existingDbSession, reused: true };
159
+ }
160
+ // tmux session is gone — we'll re-create it below
161
+ finalSessionName = existingDbSession.sessionName;
162
+ }
163
+ // Check if tmux session with this name already exists in the system
164
+ // (e.g. from a previous agent run before DB was wiped)
165
+ const hasProc = spawn('tmux', ['has-session', '-t', finalSessionName]);
166
+ const tmuxExists = await new Promise((resolve) => {
167
+ hasProc.on('close', (code) => resolve(code === 0));
168
+ hasProc.on('error', () => resolve(false));
169
+ });
170
+ if (tmuxExists) {
171
+ // tmux session exists but NOT in our DB — adopt it
172
+ console.log(`[tmux/create] Adopting existing tmux session "${finalSessionName}" for sessionId ${sessionId}`);
173
+ }
174
+ else {
175
+ // Check if the name conflicts with another DB entry, make unique
176
+ const existingSessions = fastify.db.getAllTmuxSessions();
177
+ const existingNames = existingSessions.map(s => s.sessionName);
178
+ if (existingNames.includes(finalSessionName)) {
179
+ finalSessionName = `${finalSessionName}-${Date.now()}`;
180
+ }
181
+ // Create new tmux session
182
+ const tmuxCmd = ['tmux', 'new-session', '-d', '-s', finalSessionName];
183
+ if (startDirectory)
184
+ tmuxCmd.push('-c', startDirectory);
185
+ if (command)
186
+ tmuxCmd.push(command);
187
+ const tmuxProcess = spawn(tmuxCmd[0], tmuxCmd.slice(1));
188
+ await new Promise((resolve, reject) => {
189
+ tmuxProcess.on('close', (code) => {
190
+ if (code === 0)
191
+ resolve(undefined);
192
+ else
193
+ reject(new Error(`Tmux exited with code ${code}`));
194
+ });
195
+ tmuxProcess.on('error', reject);
196
+ });
197
+ // Wait for tmux to be ready
198
+ await new Promise(resolve => setTimeout(resolve, 100));
199
+ // Verify
200
+ const verifyProcess = spawn('tmux', ['has-session', '-t', finalSessionName]);
201
+ await new Promise((resolve, reject) => {
202
+ verifyProcess.on('close', (code) => {
203
+ if (code === 0)
204
+ resolve(undefined);
205
+ else
206
+ reject(new Error(`Failed to verify tmux session ${finalSessionName}`));
207
+ });
208
+ verifyProcess.on('error', reject);
209
+ });
210
+ }
211
+ // Upsert database entry (handles both new creation and adoption)
212
+ let session;
213
+ if (existingDbSession) {
214
+ // Update existing record
215
+ fastify.db.updateTmuxSession(sessionId, {
216
+ sessionName: finalSessionName,
217
+ status: 'active',
218
+ });
219
+ session = fastify.db.getTmuxSession(sessionId);
220
+ }
221
+ else {
222
+ session = fastify.db.createTmuxSession({
223
+ id: sessionId,
224
+ sessionName: finalSessionName,
225
+ projectId,
226
+ windowName: finalWindowName,
227
+ command,
228
+ status: 'active',
229
+ ttydPort: undefined,
230
+ ttydPid: undefined,
231
+ });
232
+ }
233
+ // Emit session created event
234
+ fastify.io.emit('session:created', session);
235
+ return { session, reused: tmuxExists };
236
+ }
237
+ catch (error) {
238
+ return reply.code(500).send({
239
+ error: 'Failed to create tmux session',
240
+ details: error instanceof Error ? error.message : 'Unknown error'
241
+ });
242
+ }
243
+ });
244
+ // Execute command in session
245
+ fastify.post('/:sessionId/command', async (request, reply) => {
246
+ const { sessionId } = request.params;
247
+ const { command } = request.body;
248
+ try {
249
+ const session = fastify.db.getTmuxSession(sessionId);
250
+ if (!session) {
251
+ return reply.code(404).send({ error: 'Session not found' });
252
+ }
253
+ // Send command to tmux session
254
+ const tmuxProcess = spawn('tmux', ['send-keys', '-t', session.sessionName, command, 'Enter']);
255
+ await new Promise((resolve, reject) => {
256
+ tmuxProcess.on('close', (code) => {
257
+ if (code === 0)
258
+ resolve(undefined);
259
+ else
260
+ reject(new Error(`Tmux exited with code ${code}`));
261
+ });
262
+ tmuxProcess.on('error', reject);
263
+ });
264
+ // Emit command executed event
265
+ fastify.io.emit('session:command', { sessionId, command });
266
+ return { success: true };
267
+ }
268
+ catch (error) {
269
+ return reply.code(500).send({
270
+ error: 'Failed to execute command',
271
+ details: error instanceof Error ? error.message : 'Unknown error'
272
+ });
273
+ }
274
+ });
275
+ // Send keys to session
276
+ fastify.post('/:sessionId/keys', async (request, reply) => {
277
+ const { sessionId } = request.params;
278
+ const { keys } = request.body;
279
+ try {
280
+ const session = fastify.db.getTmuxSession(sessionId);
281
+ if (!session) {
282
+ return reply.code(404).send({ error: 'Session not found' });
283
+ }
284
+ // Send keys to tmux session
285
+ const tmuxProcess = spawn('tmux', ['send-keys', '-t', session.sessionName, keys]);
286
+ await new Promise((resolve, reject) => {
287
+ tmuxProcess.on('close', (code) => {
288
+ if (code === 0)
289
+ resolve(undefined);
290
+ else
291
+ reject(new Error(`Tmux exited with code ${code}`));
292
+ });
293
+ tmuxProcess.on('error', reject);
294
+ });
295
+ return { success: true };
296
+ }
297
+ catch (error) {
298
+ return reply.code(500).send({
299
+ error: 'Failed to send keys',
300
+ details: error instanceof Error ? error.message : 'Unknown error'
301
+ });
302
+ }
303
+ });
304
+ // Send interrupt (Ctrl+C) to session
305
+ fastify.post('/:sessionId/interrupt', async (request, reply) => {
306
+ const { sessionId } = request.params;
307
+ try {
308
+ const session = fastify.db.getTmuxSession(sessionId);
309
+ if (!session) {
310
+ return reply.code(404).send({ error: 'Session not found' });
311
+ }
312
+ // Send Ctrl+C to tmux session
313
+ const tmuxProcess = spawn('tmux', ['send-keys', '-t', session.sessionName, 'C-c']);
314
+ await new Promise((resolve, reject) => {
315
+ tmuxProcess.on('close', (code) => {
316
+ if (code === 0)
317
+ resolve(undefined);
318
+ else
319
+ reject(new Error(`Tmux exited with code ${code}`));
320
+ });
321
+ tmuxProcess.on('error', reject);
322
+ });
323
+ return { success: true };
324
+ }
325
+ catch (error) {
326
+ return reply.code(500).send({
327
+ error: 'Failed to send interrupt',
328
+ details: error instanceof Error ? error.message : 'Unknown error'
329
+ });
330
+ }
331
+ });
332
+ // Capture session output
333
+ fastify.get('/:sessionId/capture', async (request, reply) => {
334
+ const { sessionId } = request.params;
335
+ try {
336
+ const session = fastify.db.getTmuxSession(sessionId);
337
+ if (!session) {
338
+ return reply.code(404).send({ error: 'Session not found' });
339
+ }
340
+ // Capture tmux pane content
341
+ const captureProcess = spawn('tmux', ['capture-pane', '-t', session.sessionName, '-p']);
342
+ let output = '';
343
+ captureProcess.stdout.on('data', (data) => {
344
+ output += data.toString();
345
+ });
346
+ await new Promise((resolve, reject) => {
347
+ captureProcess.on('close', (code) => {
348
+ if (code === 0)
349
+ resolve(undefined);
350
+ else
351
+ reject(new Error(`Tmux exited with code ${code}`));
352
+ });
353
+ captureProcess.on('error', reject);
354
+ });
355
+ return { output };
356
+ }
357
+ catch (error) {
358
+ return reply.code(500).send({
359
+ error: 'Failed to capture output',
360
+ details: error instanceof Error ? error.message : 'Unknown error'
361
+ });
362
+ }
363
+ });
364
+ // Kill session
365
+ fastify.delete('/:sessionId', async (request, reply) => {
366
+ const { sessionId } = request.params;
367
+ const startTime = Date.now();
368
+ try {
369
+ const session = fastify.db.getTmuxSession(sessionId);
370
+ if (!session) {
371
+ console.warn(`[Session Termination] Session not found: ${sessionId}`);
372
+ return reply.code(404).send({ error: 'Session not found' });
373
+ }
374
+ console.log(`[Session Termination] Starting termination process for session ${session.sessionName} (ID: ${sessionId})`);
375
+ const cleanup = {
376
+ ttydKilled: false,
377
+ orphanedTtydCleaned: false,
378
+ tmuxKilled: false,
379
+ databaseUpdated: false
380
+ };
381
+ // Enhanced TTYd process cleanup with verification
382
+ if (session.ttydPid) {
383
+ console.log(`[Session Termination] Killing TTYd process ${session.ttydPid} for session ${session.sessionName}`);
384
+ try {
385
+ // First try graceful termination
386
+ process.kill(session.ttydPid, 'SIGTERM');
387
+ // Wait a moment and verify process is killed
388
+ await new Promise(resolve => setTimeout(resolve, 1000));
389
+ try {
390
+ process.kill(session.ttydPid, 0); // Check if process still exists
391
+ console.warn(`[Session Termination] TTYd process ${session.ttydPid} still running after SIGTERM, using SIGKILL`);
392
+ process.kill(session.ttydPid, 'SIGKILL');
393
+ }
394
+ catch (killError) {
395
+ // Process doesn't exist anymore, which is what we want
396
+ console.log(`[Session Termination] TTYd process ${session.ttydPid} successfully terminated`);
397
+ }
398
+ cleanup.ttydKilled = true;
399
+ }
400
+ catch (error) {
401
+ console.error(`[Session Termination] Failed to kill TTYd process ${session.ttydPid}:`, error);
402
+ // Continue with session cleanup even if ttyd kill fails
403
+ }
404
+ }
405
+ // Clean up any orphaned TTYd processes for this session
406
+ console.log(`[Session Termination] Cleaning up orphaned TTYd processes for session ${session.sessionName}`);
407
+ try {
408
+ await cleanupOrphanedTtydProcesses(session.sessionName);
409
+ cleanup.orphanedTtydCleaned = true;
410
+ console.log(`[Session Termination] Orphaned TTYd processes cleaned up for session ${session.sessionName}`);
411
+ }
412
+ catch (error) {
413
+ console.error(`[Session Termination] Failed to clean up orphaned TTYd processes:`, error);
414
+ }
415
+ // Kill tmux session with verification
416
+ console.log(`[Session Termination] Killing tmux session ${session.sessionName}`);
417
+ try {
418
+ const tmuxProcess = spawn('tmux', ['kill-session', '-t', session.sessionName]);
419
+ await new Promise((resolve, reject) => {
420
+ let tmuxOutput = '';
421
+ let tmuxError = '';
422
+ tmuxProcess.stdout?.on('data', (data) => {
423
+ tmuxOutput += data.toString();
424
+ });
425
+ tmuxProcess.stderr?.on('data', (data) => {
426
+ tmuxError += data.toString();
427
+ });
428
+ tmuxProcess.on('close', (code) => {
429
+ if (code === 0) {
430
+ console.log(`[Session Termination] Tmux session ${session.sessionName} successfully killed`);
431
+ resolve(undefined);
432
+ }
433
+ else {
434
+ console.error(`[Session Termination] Tmux kill failed with code ${code}. Output: ${tmuxOutput}. Error: ${tmuxError}`);
435
+ reject(new Error(`Tmux exited with code ${code}. Error: ${tmuxError}`));
436
+ }
437
+ });
438
+ tmuxProcess.on('error', (error) => {
439
+ console.error(`[Session Termination] Tmux process error:`, error);
440
+ reject(error);
441
+ });
442
+ });
443
+ cleanup.tmuxKilled = true;
444
+ // Verify tmux session is actually gone
445
+ await new Promise(resolve => setTimeout(resolve, 500));
446
+ try {
447
+ const verifyProcess = spawn('tmux', ['has-session', '-t', session.sessionName]);
448
+ await new Promise((resolve, reject) => {
449
+ verifyProcess.on('close', (code) => {
450
+ if (code === 0) {
451
+ console.warn(`[Session Termination] WARNING: Tmux session ${session.sessionName} still exists after kill command`);
452
+ }
453
+ else {
454
+ console.log(`[Session Termination] Verified: Tmux session ${session.sessionName} has been successfully terminated`);
455
+ }
456
+ resolve(undefined);
457
+ });
458
+ verifyProcess.on('error', () => resolve(undefined));
459
+ });
460
+ }
461
+ catch (verifyError) {
462
+ console.warn(`[Session Termination] Could not verify tmux session termination:`, verifyError);
463
+ }
464
+ }
465
+ catch (error) {
466
+ console.error(`[Session Termination] Failed to kill tmux session ${session.sessionName}:`, error);
467
+ throw error;
468
+ }
469
+ // Update database - clear ttyd info and set status to terminated
470
+ console.log(`[Session Termination] Updating database status for session ${session.sessionName}`);
471
+ try {
472
+ fastify.db.updateTmuxSession(session.id, {
473
+ status: 'terminated',
474
+ ttydPort: undefined,
475
+ ttydPid: undefined
476
+ });
477
+ cleanup.databaseUpdated = true;
478
+ console.log(`[Session Termination] Database updated for session ${session.sessionName}`);
479
+ }
480
+ catch (error) {
481
+ console.error(`[Session Termination] Failed to update database for session ${session.sessionName}:`, error);
482
+ throw error;
483
+ }
484
+ // Emit session terminated event
485
+ fastify.io.emit('session:terminated', { sessionId, sessionName: session.sessionName });
486
+ // Final verification that everything is cleaned up
487
+ const verification = await verifySessionTermination(sessionId, session.sessionName, session.ttydPid);
488
+ const executionTime = Date.now() - startTime;
489
+ console.log(`[Session Termination] Successfully terminated session ${session.sessionName} in ${executionTime}ms. Cleanup status:`, cleanup);
490
+ if (!verification.allCleaned) {
491
+ console.warn(`[Session Termination] WARNING: Session ${session.sessionName} termination may be incomplete. Verification:`, verification);
492
+ }
493
+ return {
494
+ success: true,
495
+ cleanup,
496
+ verification,
497
+ executionTime,
498
+ sessionName: session.sessionName
499
+ };
500
+ }
501
+ catch (error) {
502
+ const executionTime = Date.now() - startTime;
503
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
504
+ console.error(`[Session Termination] Failed to terminate session ${sessionId} after ${executionTime}ms:`, errorMessage);
505
+ return reply.code(500).send({
506
+ error: 'Failed to kill session',
507
+ details: errorMessage,
508
+ executionTime
509
+ });
510
+ }
511
+ });
512
+ // Rename session by ID
513
+ fastify.put('/:sessionId/rename', async (request, reply) => {
514
+ const { sessionId } = request.params;
515
+ const { newName } = request.body;
516
+ try {
517
+ const session = fastify.db.getTmuxSession(sessionId);
518
+ if (!session) {
519
+ return reply.code(404).send({ error: 'Session not found' });
520
+ }
521
+ // Rename tmux session using current sessionName
522
+ const tmuxProcess = spawn('tmux', ['rename-session', '-t', session.sessionName, newName]);
523
+ await new Promise((resolve, reject) => {
524
+ tmuxProcess.on('close', (code) => {
525
+ if (code === 0)
526
+ resolve(undefined);
527
+ else
528
+ reject(new Error(`Tmux exited with code ${code}`));
529
+ });
530
+ tmuxProcess.on('error', reject);
531
+ });
532
+ // Update database
533
+ fastify.db.updateTmuxSession(session.id, { sessionName: newName });
534
+ return { success: true };
535
+ }
536
+ catch (error) {
537
+ return reply.code(500).send({
538
+ error: 'Failed to rename session',
539
+ details: error instanceof Error ? error.message : 'Unknown error'
540
+ });
541
+ }
542
+ });
543
+ // Toggle mouse mode in tmux session
544
+ fastify.post('/:sessionId/toggle-mouse', async (request, reply) => {
545
+ const { sessionId } = request.params;
546
+ try {
547
+ const session = fastify.db.getTmuxSession(sessionId);
548
+ if (!session) {
549
+ return reply.code(404).send({ error: 'Session not found' });
550
+ }
551
+ // Toggle mouse mode
552
+ const tmuxProcess = spawn('tmux', ['set-option', '-t', session.sessionName, 'mouse']);
553
+ await new Promise((resolve, reject) => {
554
+ tmuxProcess.on('close', (code) => {
555
+ if (code === 0)
556
+ resolve(undefined);
557
+ else
558
+ reject(new Error(`Tmux exited with code ${code}`));
559
+ });
560
+ tmuxProcess.on('error', reject);
561
+ });
562
+ return { success: true };
563
+ }
564
+ catch (error) {
565
+ return reply.code(500).send({
566
+ error: 'Failed to toggle mouse mode',
567
+ details: error instanceof Error ? error.message : 'Unknown error'
568
+ });
569
+ }
570
+ });
571
+ // Get ttyd URL for session
572
+ fastify.get('/:sessionId/ttyd', async (request, reply) => {
573
+ const { sessionId } = request.params;
574
+ try {
575
+ const session = fastify.db.getTmuxSession(sessionId);
576
+ if (!session) {
577
+ return reply.code(404).send({ error: 'Session not found' });
578
+ }
579
+ if (!session.ttydPort || !session.ttydPid) {
580
+ return reply.code(404).send({ error: 'TTYd not running for this session' });
581
+ }
582
+ // Check if process is still running
583
+ try {
584
+ process.kill(session.ttydPid, 0);
585
+ return {
586
+ port: session.ttydPort,
587
+ url: `http://localhost:${session.ttydPort}`,
588
+ pid: session.ttydPid
589
+ };
590
+ }
591
+ catch {
592
+ // Process is not running, clean up
593
+ fastify.db.updateTmuxSession(session.id, {
594
+ ttydPort: undefined,
595
+ ttydPid: undefined
596
+ });
597
+ return reply.code(404).send({ error: 'TTYd process is no longer running' });
598
+ }
599
+ }
600
+ catch (error) {
601
+ return reply.code(500).send({
602
+ error: 'Failed to get ttyd URL',
603
+ details: error instanceof Error ? error.message : 'Unknown error'
604
+ });
605
+ }
606
+ });
607
+ // Start ttyd for browser terminal access
608
+ fastify.post('/:sessionId/ttyd', async (request, reply) => {
609
+ const { sessionId } = request.params;
610
+ try {
611
+ const session = fastify.db.getTmuxSession(sessionId);
612
+ if (!session) {
613
+ return reply.code(404).send({ error: 'Session not found' });
614
+ }
615
+ // Check if ttyd is already running for this session
616
+ if (session.ttydPort && session.ttydPid) {
617
+ // Check if process is still running
618
+ try {
619
+ process.kill(session.ttydPid, 0);
620
+ return { port: session.ttydPort, url: `http://localhost:${session.ttydPort}` };
621
+ }
622
+ catch {
623
+ // Process is not running, clean up database
624
+ fastify.db.updateTmuxSession(session.id, {
625
+ ttydPort: undefined,
626
+ ttydPid: undefined
627
+ });
628
+ }
629
+ }
630
+ // Clean up any orphaned TTYd processes for this session
631
+ await cleanupOrphanedTtydProcesses(session.sessionName);
632
+ // Find available port with retry logic
633
+ let port;
634
+ let ttydProcess;
635
+ let attempts = 0;
636
+ const maxAttempts = 5;
637
+ while (attempts < maxAttempts) {
638
+ try {
639
+ port = await findAvailablePort(7700 + attempts, 7800);
640
+ // Verify tmux session exists before starting TTYd
641
+ const tmuxCheckProcess = spawn('tmux', ['has-session', '-t', session.sessionName]);
642
+ await new Promise((resolve, reject) => {
643
+ tmuxCheckProcess.on('close', (code) => {
644
+ if (code === 0)
645
+ resolve(undefined);
646
+ else
647
+ reject(new Error(`Tmux session ${session.sessionName} does not exist`));
648
+ });
649
+ tmuxCheckProcess.on('error', reject);
650
+ });
651
+ // Start ttyd with proper error capturing and safer theme parameter
652
+ ttydProcess = spawn('ttyd', [
653
+ '-p', port.toString(),
654
+ '-t', 'fontSize=14',
655
+ '-t', 'theme={"background":"#1e1e1e","foreground":"#cccccc"}',
656
+ '--writable',
657
+ 'tmux', 'attach-session', '-t', session.sessionName
658
+ ], {
659
+ detached: true,
660
+ stdio: ['ignore', 'pipe', 'pipe'],
661
+ env: { ...process.env, PATH: process.env.PATH }, // Ensure proper environment
662
+ cwd: process.cwd() // Ensure proper working directory
663
+ });
664
+ // Capture stderr for debugging
665
+ let stderrData = '';
666
+ if (ttydProcess.stderr) {
667
+ ttydProcess.stderr.on('data', (data) => {
668
+ stderrData += data.toString();
669
+ });
670
+ }
671
+ // Wait a moment to see if the process starts successfully
672
+ await new Promise((resolve, reject) => {
673
+ let resolved = false;
674
+ const timeout = setTimeout(() => {
675
+ if (!resolved) {
676
+ resolved = true;
677
+ resolve(true); // Assume success if no immediate error
678
+ }
679
+ }, 2000); // Increased timeout to 2 seconds
680
+ ttydProcess.on('error', (err) => {
681
+ if (!resolved) {
682
+ resolved = true;
683
+ clearTimeout(timeout);
684
+ reject(new Error(`TTYd process error: ${err.message}. Stderr: ${stderrData}`));
685
+ }
686
+ });
687
+ ttydProcess.on('exit', (code) => {
688
+ if (!resolved && code !== 0) {
689
+ resolved = true;
690
+ clearTimeout(timeout);
691
+ reject(new Error(`TTYd exited with code ${code}. Stderr: ${stderrData}`));
692
+ }
693
+ });
694
+ });
695
+ ttydProcess.unref();
696
+ // Update database with ttyd info
697
+ fastify.db.updateTmuxSession(session.id, {
698
+ ttydPort: port,
699
+ ttydPid: ttydProcess.pid
700
+ });
701
+ // Emit ttyd started event
702
+ fastify.io.emit('session:ttyd-started', { sessionId, port });
703
+ // Build the proxy URL: the agent's single tunnel + /terminal/:sessionId/
704
+ // This replaces per-session cloudflared tunnels — the agent proxies ttyd
705
+ // through its own API at /terminal/:sessionId
706
+ const agentTunnel = getAgentTunnelUrl();
707
+ const terminalProxyUrl = agentTunnel
708
+ ? `${agentTunnel}/terminal/${sessionId}/`
709
+ : null;
710
+ console.log(`[TTYd] Started on port ${port}, proxy URL: ${terminalProxyUrl || 'no tunnel (local only)'}`);
711
+ return {
712
+ port,
713
+ pid: ttydProcess.pid,
714
+ url: `http://localhost:${port}`,
715
+ terminalUrl: terminalProxyUrl,
716
+ };
717
+ }
718
+ catch (error) {
719
+ console.log(`TTYd start attempt ${attempts + 1} failed:`, error);
720
+ attempts++;
721
+ if (ttydProcess && ttydProcess.pid) {
722
+ try {
723
+ process.kill(ttydProcess.pid, 'SIGTERM');
724
+ }
725
+ catch {
726
+ // Ignore kill errors
727
+ }
728
+ }
729
+ if (attempts >= maxAttempts) {
730
+ throw error;
731
+ }
732
+ // Wait before retry
733
+ await new Promise(resolve => setTimeout(resolve, 500));
734
+ }
735
+ }
736
+ }
737
+ catch (error) {
738
+ return reply.code(500).send({
739
+ error: 'Failed to start ttyd',
740
+ details: error instanceof Error ? error.message : 'Unknown error'
741
+ });
742
+ }
743
+ });
744
+ // Verify session termination status
745
+ fastify.get('/:sessionId/termination-status', async (request, reply) => {
746
+ const { sessionId } = request.params;
747
+ try {
748
+ const session = fastify.db.getTmuxSession(sessionId);
749
+ if (!session) {
750
+ return reply.code(404).send({ error: 'Session not found' });
751
+ }
752
+ const verification = await verifySessionTermination(sessionId, session.sessionName, session.ttydPid);
753
+ return {
754
+ sessionId,
755
+ sessionName: session.sessionName,
756
+ databaseStatus: session.status,
757
+ verification,
758
+ isFullyTerminated: verification.allCleaned && session.status === 'terminated'
759
+ };
760
+ }
761
+ catch (error) {
762
+ return reply.code(500).send({
763
+ error: 'Failed to verify session termination',
764
+ details: error instanceof Error ? error.message : 'Unknown error'
765
+ });
766
+ }
767
+ });
768
+ // Stop ttyd for session
769
+ fastify.post('/:sessionId/ttyd/stop', async (request, reply) => {
770
+ const { sessionId } = request.params;
771
+ try {
772
+ const session = fastify.db.getTmuxSession(sessionId);
773
+ if (!session) {
774
+ return reply.code(404).send({ error: 'Session not found' });
775
+ }
776
+ if (!session.ttydPid) {
777
+ return reply.code(400).send({ error: 'No ttyd process running for this session' });
778
+ }
779
+ // Kill ttyd process
780
+ try {
781
+ process.kill(session.ttydPid, 'SIGTERM');
782
+ }
783
+ catch {
784
+ // Process might already be dead
785
+ }
786
+ // Update database
787
+ fastify.db.updateTmuxSession(session.id, {
788
+ ttydPort: undefined,
789
+ ttydPid: undefined
790
+ });
791
+ // Emit ttyd stopped event
792
+ fastify.io.emit('session:ttyd-stopped', { sessionId });
793
+ return { success: true };
794
+ }
795
+ catch (error) {
796
+ return reply.code(500).send({
797
+ error: 'Failed to stop ttyd',
798
+ details: error instanceof Error ? error.message : 'Unknown error'
799
+ });
800
+ }
801
+ });
802
+ };
803
+ // Helper function to clean up orphaned TTYd processes
804
+ async function cleanupOrphanedTtydProcesses(sessionName) {
805
+ const { spawn } = await import('child_process');
806
+ try {
807
+ console.log(`[TTYd Cleanup] Searching for orphaned TTYd processes for session ${sessionName}`);
808
+ // Find TTYd processes for this session
809
+ const findProcess = spawn('pgrep', ['-f', `ttyd.*${sessionName}`]);
810
+ let processIds = '';
811
+ findProcess.stdout.on('data', (data) => {
812
+ processIds += data.toString();
813
+ });
814
+ await new Promise((resolve) => {
815
+ findProcess.on('close', resolve);
816
+ });
817
+ if (processIds.trim()) {
818
+ const pids = processIds.trim().split('\n').filter(pid => pid);
819
+ console.log(`[TTYd Cleanup] Found ${pids.length} orphaned TTYd processes for session ${sessionName}, cleaning up...`);
820
+ for (const pid of pids) {
821
+ try {
822
+ const pidNum = parseInt(pid);
823
+ console.log(`[TTYd Cleanup] Killing orphaned TTYd process ${pidNum}`);
824
+ // First try graceful termination
825
+ process.kill(pidNum, 'SIGTERM');
826
+ // Wait a moment and verify process is killed
827
+ await new Promise(resolve => setTimeout(resolve, 1000));
828
+ try {
829
+ process.kill(pidNum, 0); // Check if process still exists
830
+ console.warn(`[TTYd Cleanup] TTYd process ${pidNum} still running after SIGTERM, using SIGKILL`);
831
+ process.kill(pidNum, 'SIGKILL');
832
+ }
833
+ catch (killError) {
834
+ // Process doesn't exist anymore, which is what we want
835
+ console.log(`[TTYd Cleanup] TTYd process ${pidNum} successfully terminated`);
836
+ }
837
+ }
838
+ catch (error) {
839
+ console.error(`[TTYd Cleanup] Failed to kill TTYd process ${pid}:`, error);
840
+ }
841
+ }
842
+ // Wait a moment for processes to terminate
843
+ await new Promise(resolve => setTimeout(resolve, 500));
844
+ // Verify all processes are gone
845
+ try {
846
+ const verifyProcess = spawn('pgrep', ['-f', `ttyd.*${sessionName}`]);
847
+ let remainingProcesses = '';
848
+ verifyProcess.stdout.on('data', (data) => {
849
+ remainingProcesses += data.toString();
850
+ });
851
+ await new Promise((resolve) => {
852
+ verifyProcess.on('close', resolve);
853
+ });
854
+ if (remainingProcesses.trim()) {
855
+ const remainingPids = remainingProcesses.trim().split('\n').filter(pid => pid);
856
+ console.warn(`[TTYd Cleanup] WARNING: ${remainingPids.length} TTYd processes still running after cleanup: ${remainingPids.join(', ')}`);
857
+ }
858
+ else {
859
+ console.log(`[TTYd Cleanup] All orphaned TTYd processes for session ${sessionName} successfully cleaned up`);
860
+ }
861
+ }
862
+ catch (verifyError) {
863
+ console.warn(`[TTYd Cleanup] Could not verify TTYd cleanup:`, verifyError);
864
+ }
865
+ }
866
+ else {
867
+ console.log(`[TTYd Cleanup] No orphaned TTYd processes found for session ${sessionName}`);
868
+ }
869
+ }
870
+ catch (error) {
871
+ console.error(`[TTYd Cleanup] Error cleaning up orphaned TTYd processes for session ${sessionName}:`, error);
872
+ throw error;
873
+ }
874
+ }
875
+ // Helper function to check if a process is still running
876
+ async function isProcessRunning(pid) {
877
+ try {
878
+ process.kill(pid, 0); // Signal 0 just checks if process exists
879
+ return true;
880
+ }
881
+ catch (error) {
882
+ return false;
883
+ }
884
+ }
885
+ // Helper function to check if a tmux session exists
886
+ async function doesTmuxSessionExist(sessionName) {
887
+ const { spawn } = await import('child_process');
888
+ try {
889
+ const checkProcess = spawn('tmux', ['has-session', '-t', sessionName]);
890
+ return new Promise((resolve) => {
891
+ checkProcess.on('close', (code) => {
892
+ resolve(code === 0);
893
+ });
894
+ checkProcess.on('error', () => {
895
+ resolve(false);
896
+ });
897
+ });
898
+ }
899
+ catch (error) {
900
+ return false;
901
+ }
902
+ }
903
+ // Helper function to get all running TTYd processes for a session
904
+ async function getRunningTtydProcesses(sessionName) {
905
+ const { spawn } = await import('child_process');
906
+ try {
907
+ const findProcess = spawn('pgrep', ['-f', `ttyd.*${sessionName}`]);
908
+ let processIds = '';
909
+ findProcess.stdout.on('data', (data) => {
910
+ processIds += data.toString();
911
+ });
912
+ await new Promise((resolve) => {
913
+ findProcess.on('close', resolve);
914
+ });
915
+ if (processIds.trim()) {
916
+ return processIds.trim().split('\n').filter(pid => pid).map(pid => parseInt(pid));
917
+ }
918
+ return [];
919
+ }
920
+ catch (error) {
921
+ console.error(`[Process Check] Error getting TTYd processes for session ${sessionName}:`, error);
922
+ return [];
923
+ }
924
+ }
925
+ // Enhanced health check endpoint for session termination verification
926
+ async function verifySessionTermination(sessionId, sessionName, ttydPid) {
927
+ console.log(`[Session Verification] Verifying termination of session ${sessionName} (ID: ${sessionId})`);
928
+ const tmuxExists = await doesTmuxSessionExist(sessionName);
929
+ const ttydProcesses = await getRunningTtydProcesses(sessionName);
930
+ const targetTtydRunning = ttydPid ? await isProcessRunning(ttydPid) : false;
931
+ const allCleaned = !tmuxExists && ttydProcesses.length === 0 && !targetTtydRunning;
932
+ console.log(`[Session Verification] Session ${sessionName} verification result:`, {
933
+ tmuxExists,
934
+ ttydProcesses,
935
+ targetTtydRunning,
936
+ allCleaned
937
+ });
938
+ return {
939
+ tmuxExists,
940
+ ttydProcesses,
941
+ targetTtydRunning,
942
+ allCleaned
943
+ };
944
+ }
945
+ // Helper function to find available port with better detection
946
+ async function findAvailablePort(startPort, endPort) {
947
+ const net = await import('net');
948
+ const { spawn } = await import('child_process');
949
+ for (let port = startPort; port <= endPort; port++) {
950
+ // First check if the port is bound using netstat/lsof
951
+ const isPortOccupied = await new Promise((resolve) => {
952
+ const checkProcess = spawn('lsof', ['-i', `:${port}`, '-t']);
953
+ let hasOutput = false;
954
+ checkProcess.stdout.on('data', (_data) => {
955
+ hasOutput = true;
956
+ });
957
+ checkProcess.on('close', (code) => {
958
+ // If lsof found processes using the port, it's occupied
959
+ resolve(hasOutput);
960
+ });
961
+ checkProcess.on('error', () => {
962
+ // If lsof fails, fall back to basic check
963
+ resolve(false);
964
+ });
965
+ // Timeout the check
966
+ setTimeout(() => {
967
+ checkProcess.kill();
968
+ resolve(false);
969
+ }, 1000);
970
+ });
971
+ if (isPortOccupied) {
972
+ continue;
973
+ }
974
+ // Double-check by trying to bind to the port
975
+ const available = await new Promise((resolve) => {
976
+ const server = net.createServer();
977
+ server.once('error', () => resolve(false));
978
+ server.once('listening', () => {
979
+ server.close();
980
+ resolve(true);
981
+ });
982
+ server.listen(port, 'localhost');
983
+ });
984
+ if (available) {
985
+ return port;
986
+ }
987
+ }
988
+ throw new Error(`No available ports between ${startPort} and ${endPort}`);
989
+ }
990
+ // Get all tmux sessions on the system
991
+ async function getSystemTmuxSessions() {
992
+ const { spawn } = await import('child_process');
993
+ try {
994
+ console.log('[System Sessions] Discovering all tmux sessions on system');
995
+ const listProcess = spawn('tmux', ['list-sessions', '-F', '#{session_name}|#{session_attached}|#{session_windows}|#{session_created}|#{session_activity}|#{session_id}']);
996
+ let output = '';
997
+ let stderr = '';
998
+ listProcess.stdout.on('data', (data) => {
999
+ output += data.toString();
1000
+ });
1001
+ listProcess.stderr.on('data', (data) => {
1002
+ stderr += data.toString();
1003
+ });
1004
+ await new Promise((resolve, reject) => {
1005
+ listProcess.on('close', (code) => {
1006
+ if (code === 0) {
1007
+ resolve(undefined);
1008
+ }
1009
+ else {
1010
+ // If no sessions exist, tmux returns exit code 1
1011
+ if (code === 1 && stderr.includes('no server running')) {
1012
+ resolve(undefined);
1013
+ }
1014
+ else {
1015
+ reject(new Error(`tmux list-sessions failed with code ${code}: ${stderr}`));
1016
+ }
1017
+ }
1018
+ });
1019
+ listProcess.on('error', reject);
1020
+ });
1021
+ if (!output.trim()) {
1022
+ console.log('[System Sessions] No tmux sessions found on system');
1023
+ return [];
1024
+ }
1025
+ const sessions = output.trim().split('\n').map(line => {
1026
+ const [name, attached, windows, created, lastActivity, sessionId] = line.split('|');
1027
+ return {
1028
+ name,
1029
+ attached: attached === '1',
1030
+ windows: parseInt(windows) || 0,
1031
+ created: new Date(parseInt(created) * 1000).toISOString(),
1032
+ lastActivity: new Date(parseInt(lastActivity) * 1000).toISOString(),
1033
+ sessionId,
1034
+ };
1035
+ });
1036
+ console.log(`[System Sessions] Found ${sessions.length} tmux sessions on system`);
1037
+ return sessions;
1038
+ }
1039
+ catch (error) {
1040
+ console.error('[System Sessions] Error getting system tmux sessions:', error);
1041
+ // Return empty array instead of throwing - no sessions is not an error
1042
+ return [];
1043
+ }
1044
+ }
1045
+ // Get all ttyd processes on the system
1046
+ async function getSystemTtydProcesses() {
1047
+ const { spawn } = await import('child_process');
1048
+ try {
1049
+ console.log('[System Processes] Discovering all ttyd processes on system');
1050
+ // Use ps to find ttyd processes with detailed information
1051
+ const psProcess = spawn('ps', ['aux']);
1052
+ let output = '';
1053
+ psProcess.stdout.on('data', (data) => {
1054
+ output += data.toString();
1055
+ });
1056
+ await new Promise((resolve, reject) => {
1057
+ psProcess.on('close', (code) => {
1058
+ if (code === 0)
1059
+ resolve(undefined);
1060
+ else
1061
+ reject(new Error(`ps command failed with code ${code}`));
1062
+ });
1063
+ psProcess.on('error', reject);
1064
+ });
1065
+ // Parse ps output to find ttyd processes
1066
+ const lines = output.split('\n');
1067
+ const ttydProcesses = [];
1068
+ for (const line of lines) {
1069
+ if (line.includes('ttyd') && !line.includes('grep')) {
1070
+ const parts = line.trim().split(/\s+/);
1071
+ if (parts.length >= 11) {
1072
+ const user = parts[0];
1073
+ const pid = parseInt(parts[1]);
1074
+ const startTime = parts[8];
1075
+ const command = parts.slice(10).join(' ');
1076
+ // Extract port from command line
1077
+ let port = 0;
1078
+ let sessionName = '';
1079
+ const portMatch = command.match(/-p\s+(\d+)/);
1080
+ if (portMatch) {
1081
+ port = parseInt(portMatch[1]);
1082
+ }
1083
+ // Extract session name from tmux attach command
1084
+ const sessionMatch = command.match(/tmux\s+attach-session\s+-t\s+(\S+)/);
1085
+ if (sessionMatch) {
1086
+ sessionName = sessionMatch[1];
1087
+ }
1088
+ ttydProcesses.push({
1089
+ pid,
1090
+ port,
1091
+ sessionName,
1092
+ command,
1093
+ startTime,
1094
+ user,
1095
+ });
1096
+ }
1097
+ }
1098
+ }
1099
+ console.log(`[System Processes] Found ${ttydProcesses.length} ttyd processes on system`);
1100
+ return ttydProcesses;
1101
+ }
1102
+ catch (error) {
1103
+ console.error('[System Processes] Error getting system ttyd processes:', error);
1104
+ return [];
1105
+ }
1106
+ }
1107
+ // Kill multiple tmux sessions
1108
+ async function killSystemTmuxSessions(sessionNames, force = false) {
1109
+ const { spawn } = await import('child_process');
1110
+ const results = [];
1111
+ console.log(`[System Sessions] Killing ${sessionNames.length} tmux sessions, force: ${force}`);
1112
+ for (const sessionName of sessionNames) {
1113
+ try {
1114
+ console.log(`[System Sessions] Killing tmux session: ${sessionName}`);
1115
+ const killProcess = spawn('tmux', ['kill-session', '-t', sessionName]);
1116
+ await new Promise((resolve, reject) => {
1117
+ let stderr = '';
1118
+ killProcess.stderr?.on('data', (data) => {
1119
+ stderr += data.toString();
1120
+ });
1121
+ killProcess.on('close', (code) => {
1122
+ if (code === 0) {
1123
+ resolve(undefined);
1124
+ }
1125
+ else {
1126
+ reject(new Error(`tmux kill-session failed with code ${code}: ${stderr}`));
1127
+ }
1128
+ });
1129
+ killProcess.on('error', reject);
1130
+ });
1131
+ results.push({
1132
+ target: sessionName,
1133
+ success: true,
1134
+ });
1135
+ console.log(`[System Sessions] Successfully killed tmux session: ${sessionName}`);
1136
+ }
1137
+ catch (error) {
1138
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1139
+ console.error(`[System Sessions] Failed to kill tmux session ${sessionName}:`, errorMessage);
1140
+ results.push({
1141
+ target: sessionName,
1142
+ success: false,
1143
+ error: errorMessage,
1144
+ });
1145
+ }
1146
+ }
1147
+ return results;
1148
+ }
1149
+ // Kill multiple ttyd processes
1150
+ async function killSystemTtydProcesses(pids, force = false) {
1151
+ const results = [];
1152
+ console.log(`[System Processes] Killing ${pids.length} ttyd processes, force: ${force}`);
1153
+ for (const pid of pids) {
1154
+ try {
1155
+ console.log(`[System Processes] Killing ttyd process: ${pid}`);
1156
+ const signal = force ? 'SIGKILL' : 'SIGTERM';
1157
+ process.kill(pid, signal);
1158
+ // Wait a moment and verify process is killed
1159
+ await new Promise(resolve => setTimeout(resolve, 1000));
1160
+ try {
1161
+ process.kill(pid, 0); // Check if process still exists
1162
+ if (!force) {
1163
+ console.warn(`[System Processes] Process ${pid} still running after SIGTERM, using SIGKILL`);
1164
+ process.kill(pid, 'SIGKILL');
1165
+ }
1166
+ else {
1167
+ throw new Error(`Process ${pid} still running after SIGKILL`);
1168
+ }
1169
+ }
1170
+ catch (killError) {
1171
+ // Process doesn't exist anymore, which is what we want
1172
+ console.log(`[System Processes] Successfully killed ttyd process: ${pid}`);
1173
+ }
1174
+ results.push({
1175
+ target: pid.toString(),
1176
+ success: true,
1177
+ });
1178
+ }
1179
+ catch (error) {
1180
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1181
+ console.error(`[System Processes] Failed to kill ttyd process ${pid}:`, errorMessage);
1182
+ results.push({
1183
+ target: pid.toString(),
1184
+ success: false,
1185
+ error: errorMessage,
1186
+ });
1187
+ }
1188
+ }
1189
+ return results;
1190
+ }
1191
+ //# sourceMappingURL=tmux.js.map