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