deckide 3.5.32 → 3.5.34
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 +42 -1
- package/bin/deckide.js +1 -1
- package/dist/middleware/cors.js +1 -1
- package/dist/routes/browser.js +40 -0
- package/dist/routes/decks.js +0 -2
- package/dist/routes/mcp.js +1241 -0
- package/dist/routes/terminals.js +180 -17
- package/dist/server.js +24 -4
- package/dist/utils/agent-browser.js +815 -0
- package/dist/utils/browser-audio.js +381 -0
- package/dist/utils/database.js +0 -40
- package/dist/utils/shell.js +34 -0
- package/dist/websocket.js +50 -3
- package/package.json +11 -7
- package/web/dist/assets/index-BHNlWxgh.js +178 -0
- package/web/dist/assets/index-mimw-Xdi.css +32 -0
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-9q6HTW9q.js +0 -65
- package/web/dist/assets/index-Cajt_oYv.css +0 -32
package/dist/routes/terminals.js
CHANGED
|
@@ -3,12 +3,82 @@ import { Hono } from 'hono';
|
|
|
3
3
|
import { spawn } from 'node-pty';
|
|
4
4
|
import { TERMINAL_BUFFER_LIMIT } from '../config.js';
|
|
5
5
|
import { createHttpError, handleError, readJson } from '../utils/error.js';
|
|
6
|
-
import { getDefaultShell } from '../utils/shell.js';
|
|
7
|
-
import { saveTerminal, deleteTerminal as deleteTerminalFromDb } from '../utils/database.js';
|
|
6
|
+
import { getDefaultShell, getAvailableLocales, getShellBasename } from '../utils/shell.js';
|
|
8
7
|
import { alignToUtf8Start, skipPartialEscapeSequence } from '../utils/utf8.js';
|
|
9
8
|
const DEFAULT_TERMINAL_TITLE = 'ターミナル';
|
|
10
9
|
const MAX_SOCKET_BUFFERED_AMOUNT = 1024 * 1024;
|
|
11
|
-
|
|
10
|
+
// 初期端末サイズ(リクエストで指定がない場合のデフォルト)。
|
|
11
|
+
const DEFAULT_TERMINAL_COLS = 120;
|
|
12
|
+
const DEFAULT_TERMINAL_ROWS = 32;
|
|
13
|
+
// WebSocket の resize と同じ範囲でバリデーションする。
|
|
14
|
+
const MIN_TERMINAL_SIZE = 1;
|
|
15
|
+
const MAX_TERMINAL_SIZE = 500;
|
|
16
|
+
// ログインシェルとして扱う POSIX シェルのベース名。
|
|
17
|
+
const POSIX_LOGIN_SHELLS = new Set(['bash', 'zsh', 'sh', 'fish', 'csh', 'tcsh']);
|
|
18
|
+
function isUtf8Locale(value) {
|
|
19
|
+
if (!value)
|
|
20
|
+
return false;
|
|
21
|
+
return /\.(utf-?8)$/i.test(value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* リクエストボディの cols/rows を整数化し 1..500 の範囲に収める。
|
|
25
|
+
* 数値でない場合は fallback を返す。
|
|
26
|
+
*/
|
|
27
|
+
function normalizeTerminalSize(value, fallback) {
|
|
28
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
const intValue = Math.floor(value);
|
|
32
|
+
if (intValue < MIN_TERMINAL_SIZE)
|
|
33
|
+
return MIN_TERMINAL_SIZE;
|
|
34
|
+
if (intValue > MAX_TERMINAL_SIZE)
|
|
35
|
+
return MAX_TERMINAL_SIZE;
|
|
36
|
+
return intValue;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 継承した環境に有効な UTF-8 ロケールが無い場合のフォールバックを選ぶ。
|
|
40
|
+
* システムに存在しないロケールは決して設定しない。
|
|
41
|
+
* 優先順位: 継承 LANG と同じ言語の UTF-8 → "C.UTF-8" → "C.utf8" → 最初の "*.UTF-8"/"*.utf8"。
|
|
42
|
+
* 該当が無ければ null を返す(何も設定しない)。
|
|
43
|
+
*/
|
|
44
|
+
function pickFallbackUtf8Locale(inheritedLang) {
|
|
45
|
+
const available = getAvailableLocales();
|
|
46
|
+
if (available.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const utf8Locales = available.filter((loc) => isUtf8Locale(loc));
|
|
50
|
+
if (utf8Locales.length === 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// 継承 LANG の言語部分(例: "ja_JP.eucJP" -> "ja_JP" / "ja")に一致する UTF-8 ロケール。
|
|
54
|
+
if (inheritedLang) {
|
|
55
|
+
const langBase = inheritedLang.split('.')[0];
|
|
56
|
+
if (langBase) {
|
|
57
|
+
const exact = utf8Locales.find((loc) => loc.split('.')[0] === langBase);
|
|
58
|
+
if (exact)
|
|
59
|
+
return exact;
|
|
60
|
+
const lang = langBase.split('_')[0];
|
|
61
|
+
if (lang) {
|
|
62
|
+
const byLang = utf8Locales.find((loc) => loc.split(/[._]/)[0] === lang);
|
|
63
|
+
if (byLang)
|
|
64
|
+
return byLang;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const cUtf8 = utf8Locales.find((loc) => loc === 'C.UTF-8');
|
|
69
|
+
if (cUtf8)
|
|
70
|
+
return cUtf8;
|
|
71
|
+
const cUtf8Lower = utf8Locales.find((loc) => loc === 'C.utf8');
|
|
72
|
+
if (cUtf8Lower)
|
|
73
|
+
return cUtf8Lower;
|
|
74
|
+
return utf8Locales[0];
|
|
75
|
+
}
|
|
76
|
+
// PTY flow-control thresholds (per terminal session).
|
|
77
|
+
const FLOW_HIGH_WATER = 512 * 1024; // pause the PTY above this backlog
|
|
78
|
+
const FLOW_LOW_WATER = 64 * 1024; // resume once drained below this
|
|
79
|
+
const FLOW_CHECK_INTERVAL_MS = 100; // how often to poll for drain while paused
|
|
80
|
+
const FLOW_MAX_PAUSE_MS = 10_000; // evict a stuck consumer after this long
|
|
81
|
+
export function createTerminalRouter(decks, terminals) {
|
|
12
82
|
const router = new Hono();
|
|
13
83
|
function toBuffer(data) {
|
|
14
84
|
return Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
@@ -124,11 +194,11 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
124
194
|
deadSockets.add(socket);
|
|
125
195
|
}
|
|
126
196
|
});
|
|
197
|
+
// resizeOwner(主クライアント)の付け替え・昇格は WebSocket の close
|
|
198
|
+
// ハンドラに一元化する。ここで resizeOwner を null にすると、主ソケットが
|
|
199
|
+
// 切断されたときの「残りクライアントへの昇格」が動かなくなるため触らない。
|
|
127
200
|
deadSockets.forEach((s) => {
|
|
128
201
|
session.sockets.delete(s);
|
|
129
|
-
if (session.resizeOwner === s) {
|
|
130
|
-
session.resizeOwner = null;
|
|
131
|
-
}
|
|
132
202
|
});
|
|
133
203
|
}
|
|
134
204
|
function handleTerminalExit(id) {
|
|
@@ -137,7 +207,6 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
137
207
|
return;
|
|
138
208
|
console.log(`[TERMINAL] Terminal ${id} exited`);
|
|
139
209
|
terminals.delete(id);
|
|
140
|
-
deleteTerminalFromDb(db, id);
|
|
141
210
|
session.sockets.forEach((socket) => {
|
|
142
211
|
try {
|
|
143
212
|
socket.close(1000, 'Terminal exited');
|
|
@@ -147,7 +216,7 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
147
216
|
session.sockets.clear();
|
|
148
217
|
session.resizeOwner = null;
|
|
149
218
|
}
|
|
150
|
-
function createTerminalSession(deck, title, command) {
|
|
219
|
+
function createTerminalSession(deck, title, command, cols = DEFAULT_TERMINAL_COLS, rows = DEFAULT_TERMINAL_ROWS) {
|
|
151
220
|
const id = crypto.randomUUID();
|
|
152
221
|
// Resolve shell and arguments
|
|
153
222
|
let shell;
|
|
@@ -169,6 +238,14 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
169
238
|
}
|
|
170
239
|
else {
|
|
171
240
|
shell = getDefaultShell();
|
|
241
|
+
// macOS のターミナルアプリはログインシェル(-l)で起動し
|
|
242
|
+
// ~/.zprofile / ~/.bash_profile から PATH を読み込む。これに合わせる。
|
|
243
|
+
// 一方 Linux の端末は通常「非ログインの対話シェル」で ~/.bashrc を読むため、
|
|
244
|
+
// -l を付けると ~/.bash_profile を持たないユーザーが alias/関数を失う回帰になる。
|
|
245
|
+
// そのため -l は darwin のみに限定する。
|
|
246
|
+
if (process.platform === 'darwin' && POSIX_LOGIN_SHELLS.has(getShellBasename(shell))) {
|
|
247
|
+
shellArgs = ['-l'];
|
|
248
|
+
}
|
|
172
249
|
}
|
|
173
250
|
// Build environment
|
|
174
251
|
const env = {};
|
|
@@ -179,21 +256,32 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
179
256
|
env.TERM = env.TERM || 'xterm-256color';
|
|
180
257
|
env.COLORTERM = 'truecolor';
|
|
181
258
|
env.TERM_PROGRAM = 'xterm.js';
|
|
182
|
-
env.TERM_PROGRAM_VERSION = '5.
|
|
259
|
+
env.TERM_PROGRAM_VERSION = '5.5.0';
|
|
183
260
|
if (process.platform === 'win32') {
|
|
261
|
+
// Windows は従来どおり(locale -a 検出は行わない)。
|
|
184
262
|
env.LANG = 'en_US.UTF-8';
|
|
185
263
|
}
|
|
186
264
|
else {
|
|
187
|
-
|
|
265
|
+
// LC_ALL は設定しない(ユーザーの実ロケールを上書きしない)。
|
|
266
|
+
// 継承した環境に有効な UTF-8 ロケールが無いときだけフォールバックを設定する。
|
|
267
|
+
const hasInheritedUtf8 = isUtf8Locale(env.LC_ALL) ||
|
|
268
|
+
isUtf8Locale(env.LANG) ||
|
|
269
|
+
isUtf8Locale(env.LC_CTYPE);
|
|
270
|
+
if (!hasInheritedUtf8) {
|
|
271
|
+
const fallback = pickFallbackUtf8Locale(env.LANG);
|
|
272
|
+
if (fallback) {
|
|
273
|
+
// 実在するロケールのみを設定する(存在しないロケールは設定しない)。
|
|
274
|
+
env.LANG = env.LANG || fallback;
|
|
275
|
+
env.LC_CTYPE = env.LC_CTYPE || fallback;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
188
278
|
}
|
|
189
|
-
env.LC_ALL = env.LC_ALL || 'en_US.UTF-8';
|
|
190
|
-
env.LC_CTYPE = env.LC_CTYPE || 'en_US.UTF-8';
|
|
191
279
|
// Spawn PTY directly in this process
|
|
192
280
|
const isWindows = process.platform === 'win32';
|
|
193
281
|
const term = spawn(shell, shellArgs, {
|
|
194
282
|
cwd: deck.root,
|
|
195
|
-
cols
|
|
196
|
-
rows
|
|
283
|
+
cols,
|
|
284
|
+
rows,
|
|
197
285
|
env,
|
|
198
286
|
encoding: null,
|
|
199
287
|
...(isWindows ? { useConpty: true } : {}),
|
|
@@ -229,16 +317,90 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
229
317
|
}
|
|
230
318
|
catch { /* already dead */ } },
|
|
231
319
|
};
|
|
320
|
+
// ── Flow control ────────────────────────────────────────────────────
|
|
321
|
+
// Instead of closing a slow client when its send buffer fills (which then
|
|
322
|
+
// reconnects and re-replays, looking like the terminal "freezes" or jumps
|
|
323
|
+
// during heavy output), pause the PTY when the slowest consumer falls
|
|
324
|
+
// behind and resume once it drains. A stuck consumer is evicted after a
|
|
325
|
+
// grace period so it can't deadlock the terminal for everyone else.
|
|
326
|
+
let ptyPaused = false;
|
|
327
|
+
let resumeTimer = null;
|
|
328
|
+
let pausedAt = 0;
|
|
329
|
+
const maxBufferedAmount = () => {
|
|
330
|
+
let max = 0;
|
|
331
|
+
session.sockets.forEach((socket) => {
|
|
332
|
+
if (socket.readyState === 1 && socket.bufferedAmount > max) {
|
|
333
|
+
max = socket.bufferedAmount;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
return max;
|
|
337
|
+
};
|
|
338
|
+
const evictLaggards = (threshold) => {
|
|
339
|
+
session.sockets.forEach((socket) => {
|
|
340
|
+
if (socket.readyState === 1 && socket.bufferedAmount > threshold) {
|
|
341
|
+
// close() で readyState は即 CLOSING になり、maxBufferedAmount の
|
|
342
|
+
// 集計(OPEN のみ)から外れる。socket の削除と resizeOwner の昇格は
|
|
343
|
+
// WebSocket の close ハンドラに任せる(主クライアント昇格のため)。
|
|
344
|
+
try {
|
|
345
|
+
socket.close(1009, 'Terminal output overflow');
|
|
346
|
+
}
|
|
347
|
+
catch { /* ignore */ }
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
const resumePty = () => {
|
|
352
|
+
if (resumeTimer) {
|
|
353
|
+
clearInterval(resumeTimer);
|
|
354
|
+
resumeTimer = null;
|
|
355
|
+
}
|
|
356
|
+
if (!ptyPaused)
|
|
357
|
+
return;
|
|
358
|
+
ptyPaused = false;
|
|
359
|
+
try {
|
|
360
|
+
term.resume();
|
|
361
|
+
}
|
|
362
|
+
catch { /* terminal may be dying */ }
|
|
363
|
+
};
|
|
364
|
+
const pausePty = () => {
|
|
365
|
+
if (ptyPaused)
|
|
366
|
+
return;
|
|
367
|
+
ptyPaused = true;
|
|
368
|
+
pausedAt = Date.now();
|
|
369
|
+
try {
|
|
370
|
+
term.pause();
|
|
371
|
+
}
|
|
372
|
+
catch { /* terminal may be dying */ }
|
|
373
|
+
resumeTimer = setInterval(() => {
|
|
374
|
+
if (maxBufferedAmount() < FLOW_LOW_WATER) {
|
|
375
|
+
resumePty();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (Date.now() - pausedAt > FLOW_MAX_PAUSE_MS) {
|
|
379
|
+
// A consumer is stuck — drop it rather than freeze everyone.
|
|
380
|
+
evictLaggards(FLOW_LOW_WATER);
|
|
381
|
+
if (maxBufferedAmount() < FLOW_LOW_WATER) {
|
|
382
|
+
resumePty();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
pausedAt = Date.now();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, FLOW_CHECK_INTERVAL_MS);
|
|
389
|
+
resumeTimer.unref?.();
|
|
390
|
+
};
|
|
232
391
|
// Wire up PTY output → buffer + WebSocket broadcast
|
|
233
392
|
term.on('data', (data) => {
|
|
234
393
|
appendToTerminalBuffer(session, data);
|
|
235
394
|
session.lastActive = Date.now();
|
|
236
395
|
broadcastToSockets(session, data);
|
|
396
|
+
if (!ptyPaused && maxBufferedAmount() > FLOW_HIGH_WATER) {
|
|
397
|
+
pausePty();
|
|
398
|
+
}
|
|
237
399
|
});
|
|
238
400
|
term.onExit(() => {
|
|
401
|
+
resumePty();
|
|
239
402
|
handleTerminalExit(id);
|
|
240
403
|
});
|
|
241
|
-
saveTerminal(db, id, deck.id, resolvedTitle, command || null, createdAt);
|
|
242
404
|
terminals.set(id, session);
|
|
243
405
|
return session;
|
|
244
406
|
}
|
|
@@ -265,7 +427,9 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
265
427
|
const deck = decks.get(deckId);
|
|
266
428
|
if (!deck)
|
|
267
429
|
throw createHttpError('Deck not found', 404);
|
|
268
|
-
const
|
|
430
|
+
const cols = normalizeTerminalSize(body?.cols, DEFAULT_TERMINAL_COLS);
|
|
431
|
+
const rows = normalizeTerminalSize(body?.rows, DEFAULT_TERMINAL_ROWS);
|
|
432
|
+
const session = createTerminalSession(deck, body?.title, body?.command, cols, rows);
|
|
269
433
|
return c.json({ id: session.id, title: session.title }, 201);
|
|
270
434
|
}
|
|
271
435
|
catch (error) {
|
|
@@ -279,7 +443,6 @@ export function createTerminalRouter(db, decks, terminals) {
|
|
|
279
443
|
if (!session)
|
|
280
444
|
throw createHttpError('Terminal not found', 404);
|
|
281
445
|
terminals.delete(terminalId);
|
|
282
|
-
deleteTerminalFromDb(db, terminalId);
|
|
283
446
|
session.sockets.forEach((socket) => {
|
|
284
447
|
try {
|
|
285
448
|
socket.close(1000, 'Terminal deleted');
|
package/dist/server.js
CHANGED
|
@@ -11,13 +11,16 @@ import { PORT, HOST, NODE_ENV, BASIC_AUTH_USER, BASIC_AUTH_PASSWORD, CORS_ORIGIN
|
|
|
11
11
|
import { securityHeaders } from './middleware/security.js';
|
|
12
12
|
import { corsMiddleware } from './middleware/cors.js';
|
|
13
13
|
import { basicAuthMiddleware, generateWsToken, isBasicAuthEnabled } from './middleware/auth.js';
|
|
14
|
-
import { checkDatabaseIntegrity, handleDatabaseCorruption, initializeDatabase, loadPersistedState,
|
|
14
|
+
import { checkDatabaseIntegrity, handleDatabaseCorruption, initializeDatabase, loadPersistedState, } from './utils/database.js';
|
|
15
15
|
import { createWorkspaceRouter, getConfigHandler } from './routes/workspaces.js';
|
|
16
16
|
import { createDeckRouter } from './routes/decks.js';
|
|
17
17
|
import { createFileRouter } from './routes/files.js';
|
|
18
18
|
import { createTerminalRouter } from './routes/terminals.js';
|
|
19
19
|
import { createGitRouter } from './routes/git.js';
|
|
20
20
|
import { createSettingsRouter } from './routes/settings.js';
|
|
21
|
+
import { createMcpRouter } from './routes/mcp.js';
|
|
22
|
+
import { createBrowserRouter } from './routes/browser.js';
|
|
23
|
+
import { AgentBrowserService } from './utils/agent-browser.js';
|
|
21
24
|
import { setupWebSocketServer, getConnectionLimit, setConnectionLimit, getConnectionStats, clearAllConnections, } from './websocket.js';
|
|
22
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
26
|
// Request ID and logging middleware
|
|
@@ -42,12 +45,12 @@ export async function createServer() {
|
|
|
42
45
|
}
|
|
43
46
|
const db = new DatabaseSync(dbPath);
|
|
44
47
|
initializeDatabase(db);
|
|
45
|
-
clearOrphanedTerminals(db);
|
|
46
48
|
// Initialize state
|
|
47
49
|
const workspaces = new Map();
|
|
48
50
|
const workspacePathIndex = new Map();
|
|
49
51
|
const decks = new Map();
|
|
50
52
|
const terminals = new Map();
|
|
53
|
+
const browserService = new AgentBrowserService();
|
|
51
54
|
loadPersistedState(db, workspaces, workspacePathIndex, decks);
|
|
52
55
|
// Create Hono app
|
|
53
56
|
const app = new Hono();
|
|
@@ -58,17 +61,27 @@ export async function createServer() {
|
|
|
58
61
|
maxSize: MAX_REQUEST_BODY_SIZE,
|
|
59
62
|
onError: (c) => c.json({ error: 'Request body too large' }, 413),
|
|
60
63
|
}));
|
|
64
|
+
app.use('/mcp', bodyLimit({
|
|
65
|
+
maxSize: MAX_REQUEST_BODY_SIZE,
|
|
66
|
+
onError: (c) => c.json({ error: 'Request body too large' }, 413),
|
|
67
|
+
}));
|
|
68
|
+
app.use('/oauth/*', bodyLimit({
|
|
69
|
+
maxSize: MAX_REQUEST_BODY_SIZE,
|
|
70
|
+
onError: (c) => c.json({ error: 'Request body too large' }, 413),
|
|
71
|
+
}));
|
|
61
72
|
if (basicAuthMiddleware) {
|
|
62
73
|
app.use('/api/*', basicAuthMiddleware);
|
|
74
|
+
app.use('/oauth/authorize', basicAuthMiddleware);
|
|
63
75
|
}
|
|
64
76
|
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString(), uptime: process.uptime() }));
|
|
65
77
|
// Mount routers
|
|
66
78
|
app.route('/api/settings', createSettingsRouter());
|
|
67
79
|
app.route('/api/workspaces', createWorkspaceRouter(db, workspaces, workspacePathIndex));
|
|
68
80
|
app.route('/api/decks', createDeckRouter(db, workspaces, decks, terminals));
|
|
69
|
-
const terminalRouter = createTerminalRouter(
|
|
81
|
+
const terminalRouter = createTerminalRouter(decks, terminals);
|
|
70
82
|
app.route('/api/terminals', terminalRouter);
|
|
71
83
|
app.route('/api/git', createGitRouter(workspaces));
|
|
84
|
+
app.route('/api/browser', createBrowserRouter(browserService));
|
|
72
85
|
app.get('/api/config', getConfigHandler());
|
|
73
86
|
app.get('/api/ws-token', (c) => c.json({ token: generateWsToken(), authEnabled: isBasicAuthEnabled() }));
|
|
74
87
|
app.get('/api/ws/stats', (c) => c.json({ limit: getConnectionLimit(), connections: getConnectionStats() }));
|
|
@@ -86,6 +99,10 @@ export async function createServer() {
|
|
|
86
99
|
});
|
|
87
100
|
const fileRouter = createFileRouter(workspaces);
|
|
88
101
|
app.route('/api', fileRouter);
|
|
102
|
+
app.route('/', createMcpRouter({
|
|
103
|
+
apiFetch: (request) => Promise.resolve(app.fetch(request)),
|
|
104
|
+
terminals,
|
|
105
|
+
}));
|
|
89
106
|
if (hasStatic) {
|
|
90
107
|
const serveAssets = serveStatic({ root: distDir });
|
|
91
108
|
const serveIndex = serveStatic({ root: distDir, path: 'index.html' });
|
|
@@ -97,18 +114,20 @@ export async function createServer() {
|
|
|
97
114
|
});
|
|
98
115
|
}
|
|
99
116
|
const server = serve({ fetch: app.fetch, port: PORT, hostname: HOST });
|
|
100
|
-
setupWebSocketServer(server, terminals);
|
|
117
|
+
setupWebSocketServer(server, terminals, browserService);
|
|
101
118
|
server.on('listening', () => {
|
|
102
119
|
const baseUrl = `http://localhost:${PORT}`;
|
|
103
120
|
console.log(`Deck IDE server listening on ${baseUrl}`);
|
|
104
121
|
console.log(`UI: ${baseUrl}`);
|
|
105
122
|
console.log(`API: ${baseUrl}/api`);
|
|
123
|
+
console.log(`MCP: ${baseUrl}/mcp`);
|
|
106
124
|
console.log(`Health: ${baseUrl}/health`);
|
|
107
125
|
console.log('');
|
|
108
126
|
console.log('Security Status:');
|
|
109
127
|
console.log(` - Basic Auth: ${BASIC_AUTH_USER && BASIC_AUTH_PASSWORD ? 'enabled' : 'DISABLED'}`);
|
|
110
128
|
console.log(` - Max File Size: ${Math.round(MAX_FILE_SIZE / 1024 / 1024)}MB`);
|
|
111
129
|
console.log(` - Max Request Body: ${Math.round(MAX_REQUEST_BODY_SIZE / 1024)}KB`);
|
|
130
|
+
console.log(' - MCP OAuth: enabled');
|
|
112
131
|
console.log(` - Trust Proxy: ${TRUST_PROXY ? 'enabled' : 'disabled'}`);
|
|
113
132
|
console.log(` - CORS Origin: ${CORS_ORIGIN || (NODE_ENV === 'development' ? '*' : 'NOT SET')}`);
|
|
114
133
|
console.log(` - Environment: ${NODE_ENV}`);
|
|
@@ -130,6 +149,7 @@ export async function createServer() {
|
|
|
130
149
|
session.kill();
|
|
131
150
|
});
|
|
132
151
|
terminals.clear();
|
|
152
|
+
await browserService.stop();
|
|
133
153
|
try {
|
|
134
154
|
db.close();
|
|
135
155
|
}
|