deckide 3.3.1 → 3.5.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.
- package/bin/deckide.js +125 -47
- package/dist/config.js +0 -2
- package/dist/routes/terminals.js +44 -84
- package/dist/routes/workspaces.js +18 -0
- package/dist/server.js +28 -107
- package/dist/utils/database.js +0 -24
- package/package.json +1 -1
- package/web/dist/assets/index-GZruHzSF.css +32 -0
- package/web/dist/assets/{index-C4U90R3-.js → index-lUIgBF3M.js} +29 -29
- package/web/dist/index.html +2 -2
- package/dist/pty-client.js +0 -177
- package/dist/pty-daemon.js +0 -246
- package/web/dist/assets/index-pBx89huB.css +0 -32
package/bin/deckide.js
CHANGED
|
@@ -32,8 +32,7 @@ function getPort() {
|
|
|
32
32
|
return loadSettings().port || 8787;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
function
|
|
36
|
-
const port = getPort();
|
|
35
|
+
function isServerRunningOnPort(port) {
|
|
37
36
|
try {
|
|
38
37
|
execSync(`curl -sf -o /dev/null http://localhost:${port}/health`, {
|
|
39
38
|
timeout: 2000, stdio: 'ignore',
|
|
@@ -44,6 +43,58 @@ function isServerRunning() {
|
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function isServerRunning() {
|
|
47
|
+
return isServerRunningOnPort(getPort());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Get PID from pid file, or null if stale/missing */
|
|
51
|
+
function getRunningPid() {
|
|
52
|
+
if (!fs.existsSync(pidFile)) return null;
|
|
53
|
+
try {
|
|
54
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
55
|
+
process.kill(pid, 0); // throws if not running
|
|
56
|
+
return pid;
|
|
57
|
+
} catch {
|
|
58
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Stop server: try HTTP shutdown on common ports, then fall back to killing the PID */
|
|
64
|
+
function stopServer() {
|
|
65
|
+
const port = getPort();
|
|
66
|
+
// Try HTTP shutdown on configured port
|
|
67
|
+
if (isServerRunningOnPort(port)) {
|
|
68
|
+
try {
|
|
69
|
+
execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{}'`, {
|
|
70
|
+
timeout: 5000, stdio: 'ignore',
|
|
71
|
+
});
|
|
72
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
73
|
+
return true;
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
// Try default port 8787 if different
|
|
77
|
+
if (port !== 8787 && isServerRunningOnPort(8787)) {
|
|
78
|
+
try {
|
|
79
|
+
execSync(`curl -sf -X POST http://localhost:8787/api/shutdown -H "Content-Type: application/json" -d '{}'`, {
|
|
80
|
+
timeout: 5000, stdio: 'ignore',
|
|
81
|
+
});
|
|
82
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
83
|
+
return true;
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
// Fall back to killing by PID
|
|
87
|
+
const pid = getRunningPid();
|
|
88
|
+
if (pid) {
|
|
89
|
+
try {
|
|
90
|
+
process.kill(pid, 'SIGTERM');
|
|
91
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
92
|
+
return true;
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
47
98
|
// ─── CLI ────────────────────────────────────────────────────────
|
|
48
99
|
|
|
49
100
|
const args = process.argv.slice(2);
|
|
@@ -68,23 +119,23 @@ Usage:
|
|
|
68
119
|
deckide status Show server status
|
|
69
120
|
deckide logs Show server logs
|
|
70
121
|
|
|
71
|
-
deckide
|
|
72
|
-
deckide
|
|
73
|
-
deckide config get <key> Get a config value
|
|
74
|
-
deckide config reset Reset all settings
|
|
122
|
+
deckide port Show current port
|
|
123
|
+
deckide port <number> Change port (auto-restarts)
|
|
75
124
|
|
|
76
125
|
deckide auth on [user] [pass] Enable basic auth
|
|
77
126
|
deckide auth off Disable basic auth
|
|
78
127
|
deckide auth status Show auth status
|
|
79
128
|
|
|
129
|
+
deckide config Show all settings
|
|
130
|
+
deckide config set <key> <val> Set a config value
|
|
131
|
+
deckide config get <key> Get a config value
|
|
132
|
+
deckide config reset Reset all settings
|
|
133
|
+
|
|
80
134
|
Options (for start):
|
|
81
135
|
-p, --port <port> Port (default: 8787)
|
|
82
136
|
--host <host> Host (default: 0.0.0.0)
|
|
83
137
|
--no-open Don't open browser
|
|
84
138
|
--fg Run in foreground
|
|
85
|
-
|
|
86
|
-
Config keys:
|
|
87
|
-
port, host, cors, maxFileSize, trustProxy
|
|
88
139
|
`);
|
|
89
140
|
process.exit(0);
|
|
90
141
|
}
|
|
@@ -131,6 +182,7 @@ if (command === 'config') {
|
|
|
131
182
|
settings[key] = value;
|
|
132
183
|
saveSettings(settings);
|
|
133
184
|
console.log(`${key} = ${key === 'basicAuthPassword' ? '********' : value}`);
|
|
185
|
+
if (isServerRunning() || getRunningPid()) console.log('Run "deckide restart" to apply.');
|
|
134
186
|
process.exit(0);
|
|
135
187
|
}
|
|
136
188
|
|
|
@@ -144,6 +196,49 @@ if (command === 'config') {
|
|
|
144
196
|
process.exit(1);
|
|
145
197
|
}
|
|
146
198
|
|
|
199
|
+
// ── deckide port ──
|
|
200
|
+
if (command === 'port') {
|
|
201
|
+
const newPort = args[1];
|
|
202
|
+
const settings = loadSettings();
|
|
203
|
+
const currentPort = settings.port || 8787;
|
|
204
|
+
|
|
205
|
+
// Show current port
|
|
206
|
+
if (!newPort) {
|
|
207
|
+
console.log(`port: ${currentPort}`);
|
|
208
|
+
process.exit(0);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const parsed = parseInt(newPort, 10);
|
|
212
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
|
|
213
|
+
console.error('Error: port must be 1-65535');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (parsed === currentPort) {
|
|
218
|
+
console.log(`Already using port ${parsed}`);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Save new port
|
|
223
|
+
settings.port = parsed;
|
|
224
|
+
saveSettings(settings);
|
|
225
|
+
console.log(`port: ${currentPort} → ${parsed}`);
|
|
226
|
+
|
|
227
|
+
// Auto-restart if server is running
|
|
228
|
+
const wasRunning = isServerRunningOnPort(currentPort) || getRunningPid();
|
|
229
|
+
if (wasRunning) {
|
|
230
|
+
stopServer();
|
|
231
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
232
|
+
// Re-exec as start (background, no open)
|
|
233
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'start', '--no-open'], {
|
|
234
|
+
stdio: 'inherit',
|
|
235
|
+
});
|
|
236
|
+
child.on('exit', (code) => process.exit(code));
|
|
237
|
+
} else {
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
147
242
|
// ── deckide auth ──
|
|
148
243
|
if (command === 'auth') {
|
|
149
244
|
const sub = args[1];
|
|
@@ -211,31 +306,16 @@ if (command === 'status') {
|
|
|
211
306
|
console.log(` port: ${port}`);
|
|
212
307
|
console.log(` auth: ${settings.basicAuthEnabled ? 'enabled' : 'disabled'}`);
|
|
213
308
|
|
|
309
|
+
const pid = getRunningPid();
|
|
214
310
|
if (isServerRunning()) {
|
|
215
311
|
console.log(` server: \x1b[32mrunning\x1b[0m → http://localhost:${port}`);
|
|
312
|
+
if (pid) console.log(` pid: ${pid}`);
|
|
313
|
+
} else if (pid) {
|
|
314
|
+
console.log(` server: \x1b[33mprocess alive (pid ${pid}) but not responding on port ${port}\x1b[0m`);
|
|
216
315
|
} else {
|
|
217
316
|
console.log(' server: \x1b[31mstopped\x1b[0m');
|
|
218
317
|
}
|
|
219
318
|
|
|
220
|
-
// Check PID file
|
|
221
|
-
if (fs.existsSync(pidFile)) {
|
|
222
|
-
try {
|
|
223
|
-
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
224
|
-
process.kill(pid, 0); // Check if process exists
|
|
225
|
-
console.log(` pid: ${pid}`);
|
|
226
|
-
} catch {
|
|
227
|
-
// stale pid file
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const daemonInfoPath = path.join(dataDir, 'pty-daemon.json');
|
|
232
|
-
if (fs.existsSync(daemonInfoPath)) {
|
|
233
|
-
try {
|
|
234
|
-
const info = JSON.parse(fs.readFileSync(daemonInfoPath, 'utf-8'));
|
|
235
|
-
console.log(` pty: pid ${info.pid}, port ${info.port}`);
|
|
236
|
-
} catch {}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
319
|
process.exit(0);
|
|
240
320
|
}
|
|
241
321
|
|
|
@@ -260,19 +340,14 @@ if (command === 'logs') {
|
|
|
260
340
|
|
|
261
341
|
// ── deckide stop ──
|
|
262
342
|
if (command === 'stop') {
|
|
263
|
-
|
|
343
|
+
const pid = getRunningPid();
|
|
344
|
+
if (!isServerRunning() && !pid) {
|
|
264
345
|
console.log('Server is not running.');
|
|
265
346
|
process.exit(0);
|
|
266
347
|
}
|
|
267
|
-
|
|
268
|
-
try {
|
|
269
|
-
execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
|
|
270
|
-
timeout: 5000, stdio: 'ignore',
|
|
271
|
-
});
|
|
272
|
-
// Clean up pid file
|
|
273
|
-
try { fs.unlinkSync(pidFile); } catch {}
|
|
348
|
+
if (stopServer()) {
|
|
274
349
|
console.log('Server stopped.');
|
|
275
|
-
}
|
|
350
|
+
} else {
|
|
276
351
|
console.error('Failed to stop server.');
|
|
277
352
|
}
|
|
278
353
|
process.exit(0);
|
|
@@ -280,15 +355,10 @@ if (command === 'stop') {
|
|
|
280
355
|
|
|
281
356
|
// ── deckide restart ──
|
|
282
357
|
if (command === 'restart') {
|
|
283
|
-
if (isServerRunning()) {
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
execSync(`curl -sf -X POST http://localhost:${port}/api/shutdown -H "Content-Type: application/json" -d '{"terminateDaemon":true}'`, {
|
|
287
|
-
timeout: 5000, stdio: 'ignore',
|
|
288
|
-
});
|
|
289
|
-
try { fs.unlinkSync(pidFile); } catch {}
|
|
358
|
+
if (isServerRunning() || getRunningPid()) {
|
|
359
|
+
if (stopServer()) {
|
|
290
360
|
console.log('Server stopped.');
|
|
291
|
-
}
|
|
361
|
+
}
|
|
292
362
|
// Wait a moment for port to free
|
|
293
363
|
await new Promise(r => setTimeout(r, 1000));
|
|
294
364
|
}
|
|
@@ -332,12 +402,20 @@ const settings = loadSettings();
|
|
|
332
402
|
const port = startOptions.port || settings.port || 8787;
|
|
333
403
|
const host = startOptions.host || settings.host || '0.0.0.0';
|
|
334
404
|
|
|
335
|
-
// Check if already running
|
|
336
|
-
if (
|
|
405
|
+
// Check if already running on the target port
|
|
406
|
+
if (isServerRunningOnPort(port)) {
|
|
337
407
|
console.log(`Server is already running on http://localhost:${port}`);
|
|
338
408
|
process.exit(0);
|
|
339
409
|
}
|
|
340
410
|
|
|
411
|
+
// Kill old server if running on a different port
|
|
412
|
+
const oldPid = getRunningPid();
|
|
413
|
+
if (oldPid) {
|
|
414
|
+
console.log('Stopping old server...');
|
|
415
|
+
stopServer();
|
|
416
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
417
|
+
}
|
|
418
|
+
|
|
341
419
|
// ── Background mode (default) ──
|
|
342
420
|
if (!startOptions.fg) {
|
|
343
421
|
fs.mkdirSync(dataDir, { recursive: true });
|
package/dist/config.js
CHANGED
|
@@ -68,5 +68,3 @@ if (!Number.isFinite(MAX_FILE_SIZE) || MAX_FILE_SIZE < 1024) {
|
|
|
68
68
|
}
|
|
69
69
|
// Ensure data directory exists
|
|
70
70
|
fsSync.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
71
|
-
// PTY daemon info file - written by daemon on startup so server can find its port
|
|
72
|
-
export const daemonInfoPath = path.join(path.dirname(dbPath), 'pty-daemon.json');
|
package/dist/routes/terminals.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { spawn } from 'node-pty';
|
|
3
4
|
import { TERMINAL_BUFFER_LIMIT } from '../config.js';
|
|
4
5
|
import { createHttpError, handleError, readJson } from '../utils/error.js';
|
|
5
6
|
import { getDefaultShell } from '../utils/shell.js';
|
|
6
7
|
import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
|
|
7
8
|
// Track terminal index per deck for unique naming
|
|
8
9
|
const deckTerminalCounters = new Map();
|
|
9
|
-
export function createTerminalRouter(db, decks, terminals
|
|
10
|
+
export function createTerminalRouter(db, decks, terminals) {
|
|
10
11
|
const router = new Hono();
|
|
11
12
|
function appendToTerminalBuffer(session, data) {
|
|
12
13
|
const newBuffer = session.buffer + data;
|
|
@@ -21,13 +22,7 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
21
22
|
deckTerminalCounters.set(deckId, next);
|
|
22
23
|
return next;
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
ptyClient.on('data', (id, data) => {
|
|
26
|
-
const session = terminals.get(id);
|
|
27
|
-
if (!session)
|
|
28
|
-
return;
|
|
29
|
-
appendToTerminalBuffer(session, data);
|
|
30
|
-
session.lastActive = Date.now();
|
|
25
|
+
function broadcastToSockets(session, data) {
|
|
31
26
|
const deadSockets = new Set();
|
|
32
27
|
session.sockets.forEach((socket) => {
|
|
33
28
|
try {
|
|
@@ -43,9 +38,8 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
43
38
|
}
|
|
44
39
|
});
|
|
45
40
|
deadSockets.forEach((s) => session.sockets.delete(s));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
ptyClient.on('exit', (id) => {
|
|
41
|
+
}
|
|
42
|
+
function handleTerminalExit(id) {
|
|
49
43
|
const session = terminals.get(id);
|
|
50
44
|
if (!session)
|
|
51
45
|
return;
|
|
@@ -59,9 +53,9 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
59
53
|
catch { /* ignore */ }
|
|
60
54
|
});
|
|
61
55
|
session.sockets.clear();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const id =
|
|
56
|
+
}
|
|
57
|
+
function createTerminalSession(deck, title, command) {
|
|
58
|
+
const id = crypto.randomUUID();
|
|
65
59
|
// Resolve shell and arguments
|
|
66
60
|
let shell;
|
|
67
61
|
let shellArgs = [];
|
|
@@ -101,9 +95,17 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
101
95
|
}
|
|
102
96
|
env.LC_ALL = env.LC_ALL || 'en_US.UTF-8';
|
|
103
97
|
env.LC_CTYPE = env.LC_CTYPE || 'en_US.UTF-8';
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
// Spawn PTY directly in this process
|
|
99
|
+
const isWindows = process.platform === 'win32';
|
|
100
|
+
const term = spawn(shell, shellArgs, {
|
|
101
|
+
cwd: deck.root,
|
|
102
|
+
cols: 120,
|
|
103
|
+
rows: 32,
|
|
104
|
+
env,
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
...(isWindows ? { useConpty: true } : {}),
|
|
107
|
+
});
|
|
108
|
+
console.log(`[TERMINAL] Created terminal ${id}: shell=${shell}, cwd=${deck.root}, pid=${term.pid}`);
|
|
107
109
|
const resolvedTitle = title || `Terminal ${getNextTerminalIndex(deck.id)}`;
|
|
108
110
|
const createdAt = new Date().toISOString();
|
|
109
111
|
const session = {
|
|
@@ -113,18 +115,32 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
113
115
|
command: command || null,
|
|
114
116
|
createdAt,
|
|
115
117
|
sockets: new Set(),
|
|
116
|
-
buffer:
|
|
118
|
+
buffer: '',
|
|
117
119
|
lastActive: Date.now(),
|
|
118
|
-
write: (data) =>
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
write: (data) => { try {
|
|
121
|
+
term.write(data);
|
|
122
|
+
}
|
|
123
|
+
catch { /* terminal may be dying */ } },
|
|
124
|
+
resize: (cols, rows) => { try {
|
|
125
|
+
term.resize(cols, rows);
|
|
126
|
+
}
|
|
127
|
+
catch { /* terminal may be dying */ } },
|
|
128
|
+
kill: () => { try {
|
|
129
|
+
term.kill();
|
|
130
|
+
}
|
|
131
|
+
catch { /* already dead */ } },
|
|
121
132
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
// Wire up PTY output → buffer + WebSocket broadcast
|
|
134
|
+
term.onData((data) => {
|
|
135
|
+
appendToTerminalBuffer(session, data);
|
|
136
|
+
session.lastActive = Date.now();
|
|
137
|
+
broadcastToSockets(session, data);
|
|
138
|
+
});
|
|
139
|
+
term.onExit(() => {
|
|
140
|
+
handleTerminalExit(id);
|
|
141
|
+
});
|
|
142
|
+
saveTerminal(db, id, deck.id, resolvedTitle, command || null, createdAt);
|
|
125
143
|
terminals.set(id, session);
|
|
126
|
-
// Subscribe to live output from daemon (delta since initialBuffer)
|
|
127
|
-
ptyClient.attach(id, options?.initialBuffer?.length ?? 0);
|
|
128
144
|
return session;
|
|
129
145
|
}
|
|
130
146
|
router.get('/', (c) => {
|
|
@@ -150,7 +166,7 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
150
166
|
const deck = decks.get(deckId);
|
|
151
167
|
if (!deck)
|
|
152
168
|
throw createHttpError('Deck not found', 404);
|
|
153
|
-
const session =
|
|
169
|
+
const session = createTerminalSession(deck, body?.title, body?.command);
|
|
154
170
|
return c.json({ id: session.id, title: session.title }, 201);
|
|
155
171
|
}
|
|
156
172
|
catch (error) {
|
|
@@ -172,7 +188,6 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
172
188
|
catch { /* ignore */ }
|
|
173
189
|
});
|
|
174
190
|
session.sockets.clear();
|
|
175
|
-
// Kill PTY in daemon
|
|
176
191
|
session.kill();
|
|
177
192
|
return c.body(null, 204);
|
|
178
193
|
}
|
|
@@ -180,60 +195,5 @@ export function createTerminalRouter(db, decks, terminals, ptyClient) {
|
|
|
180
195
|
return handleError(c, error);
|
|
181
196
|
}
|
|
182
197
|
});
|
|
183
|
-
|
|
184
|
-
* Restore terminals after a server restart.
|
|
185
|
-
* The daemon is the source of truth: only terminals alive in the daemon are restored.
|
|
186
|
-
* DB provides metadata (title, deckId, buffer) for each daemon terminal.
|
|
187
|
-
* Stale DB entries (no matching daemon terminal) are cleaned up.
|
|
188
|
-
*/
|
|
189
|
-
async function restoreTerminals(persistedTerminals, daemonTerminals) {
|
|
190
|
-
const persistedById = new Map(persistedTerminals.map((t) => [t.id, t]));
|
|
191
|
-
const daemonIds = new Set(daemonTerminals.map((t) => t.id));
|
|
192
|
-
// Iterate daemon terminals — these are the only "real" ones
|
|
193
|
-
for (const daemonInfo of daemonTerminals) {
|
|
194
|
-
const persisted = persistedById.get(daemonInfo.id);
|
|
195
|
-
const deck = persisted ? decks.get(persisted.deckId) : undefined;
|
|
196
|
-
if (!persisted || !deck) {
|
|
197
|
-
// No metadata to attach this terminal to a deck — kill it
|
|
198
|
-
console.log(`[TERMINAL] Killing daemon terminal ${daemonInfo.id} (no deck metadata)`);
|
|
199
|
-
try {
|
|
200
|
-
await ptyClient.kill(daemonInfo.id);
|
|
201
|
-
}
|
|
202
|
-
catch { /* ignore */ }
|
|
203
|
-
if (persisted)
|
|
204
|
-
deleteTerminalFromDb(db, daemonInfo.id);
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
console.log(`[TERMINAL] Re-attaching to live terminal ${persisted.id} (${persisted.title})`);
|
|
209
|
-
const session = {
|
|
210
|
-
id: persisted.id,
|
|
211
|
-
deckId: persisted.deckId,
|
|
212
|
-
title: persisted.title,
|
|
213
|
-
command: persisted.command,
|
|
214
|
-
createdAt: persisted.createdAt,
|
|
215
|
-
sockets: new Set(),
|
|
216
|
-
buffer: '',
|
|
217
|
-
lastActive: Date.now(),
|
|
218
|
-
write: (data) => ptyClient.input(persisted.id, data),
|
|
219
|
-
resize: (cols, rows) => ptyClient.resize(persisted.id, cols, rows),
|
|
220
|
-
kill: () => ptyClient.kill(persisted.id),
|
|
221
|
-
};
|
|
222
|
-
terminals.set(persisted.id, session);
|
|
223
|
-
// Attach with offset 0 to get the full buffer from daemon
|
|
224
|
-
ptyClient.attach(persisted.id, 0);
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
console.error(`[TERMINAL] Failed to restore terminal ${daemonInfo.id}:`, err);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
// Clean up DB entries for terminals no longer alive in the daemon
|
|
231
|
-
for (const persisted of persistedTerminals) {
|
|
232
|
-
if (!daemonIds.has(persisted.id)) {
|
|
233
|
-
console.log(`[TERMINAL] Removing stale terminal ${persisted.id} from DB`);
|
|
234
|
-
deleteTerminalFromDb(db, persisted.id);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return { router, restoreTerminals };
|
|
198
|
+
return router;
|
|
239
199
|
}
|
|
@@ -61,6 +61,24 @@ export function createWorkspaceRouter(db, workspaces, workspacePathIndex) {
|
|
|
61
61
|
return handleError(c, error);
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
+
const deleteWorkspace = db.prepare('DELETE FROM workspaces WHERE id = ?');
|
|
65
|
+
router.delete('/:id', (c) => {
|
|
66
|
+
try {
|
|
67
|
+
const id = c.req.param('id');
|
|
68
|
+
const workspace = workspaces.get(id);
|
|
69
|
+
if (!workspace) {
|
|
70
|
+
throw createHttpError('Workspace not found', 404);
|
|
71
|
+
}
|
|
72
|
+
const key = getWorkspaceKey(workspace.path);
|
|
73
|
+
deleteWorkspace.run(id);
|
|
74
|
+
workspaces.delete(id);
|
|
75
|
+
workspacePathIndex.delete(key);
|
|
76
|
+
return c.json({ deleted: true });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return handleError(c, error);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
64
82
|
return router;
|
|
65
83
|
}
|
|
66
84
|
export function getConfigHandler() {
|