deckide 3.5.41 → 3.5.43
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 +28 -0
- package/bin/deckide.js +50 -1
- package/dist/config.js +28 -0
- package/dist/routes/codeserver.js +87 -0
- package/dist/routes/files.js +72 -0
- package/dist/server.js +22 -2
- package/dist/utils/agent-browser.js +60 -0
- package/dist/utils/browser-maintenance.js +145 -0
- package/dist/utils/browser-webrtc.js +493 -0
- package/dist/utils/code-server.js +360 -0
- package/dist/websocket.js +54 -2
- package/package.json +5 -2
- package/web/dist/assets/index-CSzJh3Kn.js +178 -0
- package/web/dist/assets/index-DCAuOxsd.css +32 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CdzOg4rb.css +0 -32
- package/web/dist/assets/index-D2wX1H1J.js +0 -178
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { dataDir } from '../config.js';
|
|
7
|
+
const IDLE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
8
|
+
const START_TIMEOUT_MS = 30_000;
|
|
9
|
+
const REAP_INTERVAL_MS = 60_000;
|
|
10
|
+
// Pinned code-server version for reproducible auto-download.
|
|
11
|
+
const CODE_SERVER_VERSION = process.env.DECKIDE_CODE_SERVER_VERSION || '4.125.0';
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
async function pathExists(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
await fs.access(filePath);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function findInPath(name) {
|
|
26
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
27
|
+
if (!dir)
|
|
28
|
+
continue;
|
|
29
|
+
const candidate = path.join(dir, name);
|
|
30
|
+
if (await pathExists(candidate)) {
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function getFreePort() {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const srv = net.createServer();
|
|
39
|
+
srv.once('error', reject);
|
|
40
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
41
|
+
const addr = srv.address();
|
|
42
|
+
const port = typeof addr === 'object' && addr ? addr.port : 0;
|
|
43
|
+
srv.close(() => (port ? resolve(port) : reject(new Error('Could not get a free port'))));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Manages per-workspace code-server child processes — the "power editor" surface.
|
|
49
|
+
* Mirrors AgentBrowserService: spawn on demand, supervise, poll readiness, and
|
|
50
|
+
* idle-reap (auto-stop, not auto-restart). One instance per workspace id.
|
|
51
|
+
*/
|
|
52
|
+
export class CodeServerService {
|
|
53
|
+
workspaces;
|
|
54
|
+
instances = new Map();
|
|
55
|
+
reaper = null;
|
|
56
|
+
// Single-flight auto-download of the code-server binary.
|
|
57
|
+
downloadPromise = null;
|
|
58
|
+
downloading = false;
|
|
59
|
+
// Directory layout under ~/.deckide/code-server (set by config.dataDir).
|
|
60
|
+
root = path.join(dataDir, 'code-server');
|
|
61
|
+
constructor(workspaces) {
|
|
62
|
+
this.workspaces = workspaces;
|
|
63
|
+
}
|
|
64
|
+
// Resolve the code-server executable: explicit env → standalone install under
|
|
65
|
+
// dataDir → PATH. Returns null when not acquired.
|
|
66
|
+
async resolveBin() {
|
|
67
|
+
const configured = process.env.DECKIDE_CODE_SERVER_BIN;
|
|
68
|
+
if (configured) {
|
|
69
|
+
return (await pathExists(configured)) ? configured : null;
|
|
70
|
+
}
|
|
71
|
+
const standalone = path.join(this.root, 'bin', 'code-server');
|
|
72
|
+
if (await pathExists(standalone)) {
|
|
73
|
+
return standalone;
|
|
74
|
+
}
|
|
75
|
+
return findInPath('code-server');
|
|
76
|
+
}
|
|
77
|
+
async isAcquired() {
|
|
78
|
+
return (await this.resolveBin()) !== null;
|
|
79
|
+
}
|
|
80
|
+
// Download + install the pinned code-server build into dataDir (single-flight).
|
|
81
|
+
// Returns the resolved binary path. Auto-invoked by ensure() when not present.
|
|
82
|
+
download(onLog) {
|
|
83
|
+
if (this.downloadPromise) {
|
|
84
|
+
return this.downloadPromise;
|
|
85
|
+
}
|
|
86
|
+
this.downloading = true;
|
|
87
|
+
this.downloadPromise = this.doDownload(onLog).finally(() => {
|
|
88
|
+
this.downloading = false;
|
|
89
|
+
this.downloadPromise = null;
|
|
90
|
+
});
|
|
91
|
+
return this.downloadPromise;
|
|
92
|
+
}
|
|
93
|
+
async doDownload(onLog) {
|
|
94
|
+
const version = CODE_SERVER_VERSION;
|
|
95
|
+
const os = process.platform === 'darwin' ? 'macos' : process.platform === 'linux' ? 'linux' : null;
|
|
96
|
+
if (!os) {
|
|
97
|
+
throw new Error(`Automatic code-server download is unsupported on ${process.platform}. Install code-server manually and set DECKIDE_CODE_SERVER_BIN.`);
|
|
98
|
+
}
|
|
99
|
+
const arch = process.arch === 'arm64' ? 'arm64' : process.arch === 'x64' ? 'amd64' : null;
|
|
100
|
+
if (!arch) {
|
|
101
|
+
throw new Error(`Automatic code-server download is unsupported on architecture ${process.arch}.`);
|
|
102
|
+
}
|
|
103
|
+
const name = `code-server-${version}-${os}-${arch}`;
|
|
104
|
+
const url = `https://github.com/coder/code-server/releases/download/v${version}/${name}.tar.gz`;
|
|
105
|
+
const libDir = path.join(this.root, 'lib');
|
|
106
|
+
const tarPath = path.join(this.root, `${name}.tar.gz`);
|
|
107
|
+
await fs.mkdir(libDir, { recursive: true });
|
|
108
|
+
const log = (m) => { onLog?.(m); console.log(`[code-server] ${m}`); };
|
|
109
|
+
log(`downloading ${url}`);
|
|
110
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
throw new Error(`code-server download failed: HTTP ${res.status}`);
|
|
113
|
+
}
|
|
114
|
+
const bytes = Buffer.from(await res.arrayBuffer());
|
|
115
|
+
await fs.writeFile(tarPath, bytes);
|
|
116
|
+
log(`extracting ${(bytes.length / 1024 / 1024).toFixed(0)}MB`);
|
|
117
|
+
await execFileAsync('tar', ['-xzf', tarPath, '-C', libDir]);
|
|
118
|
+
await fs.rm(tarPath, { force: true });
|
|
119
|
+
const innerBin = path.join(libDir, name, 'bin', 'code-server');
|
|
120
|
+
if (!(await pathExists(innerBin))) {
|
|
121
|
+
throw new Error('code-server binary not found after extraction');
|
|
122
|
+
}
|
|
123
|
+
const binDir = path.join(this.root, 'bin');
|
|
124
|
+
await fs.mkdir(binDir, { recursive: true });
|
|
125
|
+
const link = path.join(binDir, 'code-server');
|
|
126
|
+
await fs.rm(link, { force: true });
|
|
127
|
+
await fs.symlink(innerBin, link);
|
|
128
|
+
log('installed');
|
|
129
|
+
return link;
|
|
130
|
+
}
|
|
131
|
+
// Remove the downloaded code-server install (and its per-workspace data).
|
|
132
|
+
async uninstall() {
|
|
133
|
+
await this.stopAll();
|
|
134
|
+
await fs.rm(this.root, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
async getStatus(wsId) {
|
|
137
|
+
const bin = await this.resolveBin();
|
|
138
|
+
const instance = wsId ? this.instances.get(wsId) : undefined;
|
|
139
|
+
return {
|
|
140
|
+
available: bin !== null,
|
|
141
|
+
acquired: bin !== null,
|
|
142
|
+
binPath: bin,
|
|
143
|
+
running: instance ? instance.proc.exitCode == null : false,
|
|
144
|
+
instances: this.instances.size,
|
|
145
|
+
downloading: this.downloading,
|
|
146
|
+
wsId: wsId ?? null,
|
|
147
|
+
reason: bin
|
|
148
|
+
? undefined
|
|
149
|
+
: this.downloading
|
|
150
|
+
? 'code-server is downloading…'
|
|
151
|
+
: 'code-server will be downloaded automatically on first use',
|
|
152
|
+
error: instance?.error,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Ensure an instance for this workspace is running and ready; return its port.
|
|
156
|
+
async ensure(wsId) {
|
|
157
|
+
const existing = this.instances.get(wsId);
|
|
158
|
+
if (existing && existing.proc.exitCode == null) {
|
|
159
|
+
existing.lastActive = Date.now();
|
|
160
|
+
await existing.ready;
|
|
161
|
+
return existing.port;
|
|
162
|
+
}
|
|
163
|
+
if (existing) {
|
|
164
|
+
this.instances.delete(wsId);
|
|
165
|
+
}
|
|
166
|
+
const workspace = this.workspaces.get(wsId);
|
|
167
|
+
if (!workspace) {
|
|
168
|
+
throw new Error(`Unknown workspace: ${wsId}`);
|
|
169
|
+
}
|
|
170
|
+
let bin = await this.resolveBin();
|
|
171
|
+
if (!bin) {
|
|
172
|
+
// Auto-acquire on first use (single-flight ~100MB download).
|
|
173
|
+
bin = await this.download();
|
|
174
|
+
}
|
|
175
|
+
const port = await getFreePort();
|
|
176
|
+
const userDataDir = path.join(this.root, 'user', wsId);
|
|
177
|
+
const extensionsDir = path.join(this.root, 'ext');
|
|
178
|
+
await fs.mkdir(userDataDir, { recursive: true });
|
|
179
|
+
await fs.mkdir(extensionsDir, { recursive: true });
|
|
180
|
+
// Per-instance config file so the user's global ~/.config/code-server/config.yaml
|
|
181
|
+
// (which may set auth/bind-addr) can never override our flags.
|
|
182
|
+
const configPath = path.join(userDataDir, 'config.yaml');
|
|
183
|
+
await fs.writeFile(configPath, `bind-addr: 127.0.0.1:${port}\nauth: none\ncert: false\n`, 'utf8');
|
|
184
|
+
// code-server honors the PORT env var ahead of --bind-addr, so strip it
|
|
185
|
+
// (Deck IDE's own PORT would otherwise collide).
|
|
186
|
+
const childEnv = { ...process.env };
|
|
187
|
+
delete childEnv.PORT;
|
|
188
|
+
const proc = spawn(bin, [
|
|
189
|
+
'--config', configPath,
|
|
190
|
+
'--auth', 'none', // Deck IDE owns auth at the same-origin proxy.
|
|
191
|
+
'--disable-telemetry',
|
|
192
|
+
'--disable-update-check',
|
|
193
|
+
'--bind-addr', `127.0.0.1:${port}`,
|
|
194
|
+
'--user-data-dir', userDataDir,
|
|
195
|
+
'--extensions-dir', extensionsDir,
|
|
196
|
+
workspace.path,
|
|
197
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], env: childEnv });
|
|
198
|
+
let stderrTail = '';
|
|
199
|
+
proc.stderr.setEncoding('utf8');
|
|
200
|
+
proc.stderr.on('data', (chunk) => {
|
|
201
|
+
stderrTail = (stderrTail + chunk).slice(-2000);
|
|
202
|
+
});
|
|
203
|
+
proc.stdout.resume();
|
|
204
|
+
const instance = {
|
|
205
|
+
wsId,
|
|
206
|
+
proc,
|
|
207
|
+
port,
|
|
208
|
+
lastActive: Date.now(),
|
|
209
|
+
startedAt: Date.now(),
|
|
210
|
+
ready: this.waitForReady(port, proc, () => stderrTail),
|
|
211
|
+
};
|
|
212
|
+
this.instances.set(wsId, instance);
|
|
213
|
+
proc.once('exit', (code, signal) => {
|
|
214
|
+
const current = this.instances.get(wsId);
|
|
215
|
+
if (current === instance) {
|
|
216
|
+
this.instances.delete(wsId);
|
|
217
|
+
}
|
|
218
|
+
if (code !== 0 && code != null) {
|
|
219
|
+
console.error(`[code-server] ${wsId} exited (${signal ?? code}): ${stderrTail.trim()}`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
proc.once('error', (error) => {
|
|
223
|
+
instance.error = error.message;
|
|
224
|
+
});
|
|
225
|
+
this.ensureReaper();
|
|
226
|
+
try {
|
|
227
|
+
await instance.ready;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
await this.stop(wsId);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
return port;
|
|
234
|
+
}
|
|
235
|
+
async waitForReady(port, proc, tail) {
|
|
236
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
237
|
+
while (Date.now() < deadline) {
|
|
238
|
+
if (proc.exitCode != null) {
|
|
239
|
+
throw new Error(`code-server exited before becoming ready: ${tail().trim()}`);
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch(`http://127.0.0.1:${port}/healthz`, {
|
|
243
|
+
signal: AbortSignal.timeout(2000),
|
|
244
|
+
});
|
|
245
|
+
if (res.ok) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// not up yet
|
|
251
|
+
}
|
|
252
|
+
await sleep(250);
|
|
253
|
+
}
|
|
254
|
+
throw new Error('Timed out waiting for code-server to become ready');
|
|
255
|
+
}
|
|
256
|
+
// Returns the running port for a ready instance (for the HTTP proxy), bumping
|
|
257
|
+
// its idle timer. Starts the instance if needed.
|
|
258
|
+
async portFor(wsId) {
|
|
259
|
+
return this.ensure(wsId);
|
|
260
|
+
}
|
|
261
|
+
touch(wsId) {
|
|
262
|
+
const instance = this.instances.get(wsId);
|
|
263
|
+
if (instance) {
|
|
264
|
+
instance.lastActive = Date.now();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Proxy a WebSocket upgrade for a ready instance to its loopback port. The
|
|
268
|
+
// prefix /codeserver/:wsId has already been stripped from req.url by the caller.
|
|
269
|
+
proxyUpgrade(wsId, req, clientSocket, head) {
|
|
270
|
+
const instance = this.instances.get(wsId);
|
|
271
|
+
if (!instance || instance.proc.exitCode != null) {
|
|
272
|
+
clientSocket.destroy();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
instance.lastActive = Date.now();
|
|
276
|
+
const upstream = net.connect(instance.port, '127.0.0.1', () => {
|
|
277
|
+
const headerLines = [`${req.method} ${req.url} HTTP/1.1`];
|
|
278
|
+
const headers = req.headers;
|
|
279
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
280
|
+
if (value === undefined)
|
|
281
|
+
continue;
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
for (const v of value)
|
|
284
|
+
headerLines.push(`${key}: ${v}`);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
headerLines.push(`${key}: ${value}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
upstream.write(headerLines.join('\r\n') + '\r\n\r\n');
|
|
291
|
+
if (head && head.length) {
|
|
292
|
+
upstream.write(head);
|
|
293
|
+
}
|
|
294
|
+
upstream.pipe(clientSocket);
|
|
295
|
+
clientSocket.pipe(upstream);
|
|
296
|
+
});
|
|
297
|
+
const teardown = () => {
|
|
298
|
+
try {
|
|
299
|
+
upstream.destroy();
|
|
300
|
+
}
|
|
301
|
+
catch { /* ignore */ }
|
|
302
|
+
try {
|
|
303
|
+
clientSocket.destroy();
|
|
304
|
+
}
|
|
305
|
+
catch { /* ignore */ }
|
|
306
|
+
};
|
|
307
|
+
upstream.on('error', teardown);
|
|
308
|
+
clientSocket.on('error', teardown);
|
|
309
|
+
}
|
|
310
|
+
async stop(wsId) {
|
|
311
|
+
const instance = this.instances.get(wsId);
|
|
312
|
+
if (!instance)
|
|
313
|
+
return;
|
|
314
|
+
this.instances.delete(wsId);
|
|
315
|
+
const proc = instance.proc;
|
|
316
|
+
if (proc.exitCode == null && proc.signalCode == null) {
|
|
317
|
+
await new Promise((resolve) => {
|
|
318
|
+
const killTimer = setTimeout(() => {
|
|
319
|
+
try {
|
|
320
|
+
proc.kill('SIGKILL');
|
|
321
|
+
}
|
|
322
|
+
catch { /* ignore */ }
|
|
323
|
+
}, 3000);
|
|
324
|
+
killTimer.unref?.();
|
|
325
|
+
proc.once('exit', () => { clearTimeout(killTimer); resolve(); });
|
|
326
|
+
try {
|
|
327
|
+
proc.kill('SIGTERM');
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
clearTimeout(killTimer);
|
|
331
|
+
resolve();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async stopAll() {
|
|
337
|
+
if (this.reaper) {
|
|
338
|
+
clearInterval(this.reaper);
|
|
339
|
+
this.reaper = null;
|
|
340
|
+
}
|
|
341
|
+
await Promise.all([...this.instances.keys()].map((wsId) => this.stop(wsId)));
|
|
342
|
+
}
|
|
343
|
+
ensureReaper() {
|
|
344
|
+
if (this.reaper)
|
|
345
|
+
return;
|
|
346
|
+
this.reaper = setInterval(() => {
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
for (const [wsId, instance] of this.instances) {
|
|
349
|
+
if (now - instance.lastActive > IDLE_TIMEOUT_MS) {
|
|
350
|
+
void this.stop(wsId);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (this.instances.size === 0 && this.reaper) {
|
|
354
|
+
clearInterval(this.reaper);
|
|
355
|
+
this.reaper = null;
|
|
356
|
+
}
|
|
357
|
+
}, REAP_INTERVAL_MS);
|
|
358
|
+
this.reaper.unref?.();
|
|
359
|
+
}
|
|
360
|
+
}
|
package/dist/websocket.js
CHANGED
|
@@ -196,8 +196,47 @@ function readBufferedRange(session, startOffset, endOffset) {
|
|
|
196
196
|
}
|
|
197
197
|
return Buffer.from(raw.subarray(alignedStart, alignedEnd));
|
|
198
198
|
}
|
|
199
|
-
export function setupWebSocketServer(server, terminals, browserService) {
|
|
200
|
-
|
|
199
|
+
export function setupWebSocketServer(server, terminals, browserService, codeServerService) {
|
|
200
|
+
// noServer mode: we own the HTTP 'upgrade' event so code-server's WebSocket can
|
|
201
|
+
// be reverse-proxied alongside the app's own sockets. Everything that isn't a
|
|
202
|
+
// /codeserver/ upgrade is handed to wss exactly as before.
|
|
203
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE_SIZE });
|
|
204
|
+
server.on('upgrade', (req, socket, head) => {
|
|
205
|
+
let pathname;
|
|
206
|
+
try {
|
|
207
|
+
pathname = new URL(req.url || '', `http://localhost:${PORT}`).pathname;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
socket.destroy();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (codeServerService && pathname.startsWith('/codeserver/')) {
|
|
214
|
+
if (!verifyWebSocketAuth(req)) {
|
|
215
|
+
socket.destroy();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const rest = pathname.slice('/codeserver/'.length);
|
|
219
|
+
const slash = rest.indexOf('/');
|
|
220
|
+
const wsId = slash === -1 ? rest : rest.slice(0, slash);
|
|
221
|
+
const prefix = `/codeserver/${wsId}`;
|
|
222
|
+
const original = req.url || '';
|
|
223
|
+
req.url = original.slice(prefix.length) || '/';
|
|
224
|
+
if (!req.url.startsWith('/')) {
|
|
225
|
+
req.url = `/${req.url}`;
|
|
226
|
+
}
|
|
227
|
+
codeServerService
|
|
228
|
+
.ensure(wsId)
|
|
229
|
+
.then(() => codeServerService.proxyUpgrade(wsId, req, socket, head))
|
|
230
|
+
.catch(() => {
|
|
231
|
+
try {
|
|
232
|
+
socket.destroy();
|
|
233
|
+
}
|
|
234
|
+
catch { /* ignore */ }
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
|
|
239
|
+
});
|
|
201
240
|
const heartbeatState = new WeakMap();
|
|
202
241
|
const heartbeatInterval = setInterval(() => {
|
|
203
242
|
for (const socket of wss.clients) {
|
|
@@ -298,6 +337,19 @@ export function setupWebSocketServer(server, terminals, browserService) {
|
|
|
298
337
|
browserService.attachAudioWebSocket(socket);
|
|
299
338
|
return;
|
|
300
339
|
}
|
|
340
|
+
if (url.pathname === '/api/browser/webrtc/ws') {
|
|
341
|
+
if (!browserService) {
|
|
342
|
+
untrackConnection(clientIP, socket);
|
|
343
|
+
socket.close(1011, 'Browser service unavailable');
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
socket.on('close', () => {
|
|
347
|
+
heartbeatState.delete(socket);
|
|
348
|
+
untrackConnection(clientIP, socket);
|
|
349
|
+
});
|
|
350
|
+
browserService.attachWebRtcWebSocket(socket);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
301
353
|
const match = url.pathname.match(/\/api\/terminals\/(.+)/);
|
|
302
354
|
if (!match) {
|
|
303
355
|
untrackConnection(clientIP, socket);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deckide",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.43",
|
|
4
4
|
"description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -66,5 +66,8 @@
|
|
|
66
66
|
"developer-tools"
|
|
67
67
|
],
|
|
68
68
|
"author": "tako0614",
|
|
69
|
-
"license": "MIT"
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"werift": "^0.23.0"
|
|
72
|
+
}
|
|
70
73
|
}
|