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