deckide 3.5.42 → 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/routes/codeserver.js +8 -0
- package/dist/utils/agent-browser.js +22 -0
- package/dist/utils/browser-webrtc.js +28 -6
- package/dist/utils/code-server.js +73 -4
- package/package.json +1 -1
- 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-C1OzsVds.css +0 -32
- package/web/dist/assets/index-DFNjEZYB.js +0 -178
package/README.md
CHANGED
|
@@ -64,6 +64,11 @@ deckide config 全設定表示
|
|
|
64
64
|
deckide config set <key> <val> 設定値変更
|
|
65
65
|
deckide config get <key> 設定値取得
|
|
66
66
|
deckide config reset 設定リセット
|
|
67
|
+
|
|
68
|
+
deckide codeserver install code-server(VS Code エディタ)を取得/インストール
|
|
69
|
+
deckide codeserver status code-server のインストール状態
|
|
70
|
+
deckide codeserver path code-server バイナリのパスを表示
|
|
71
|
+
deckide codeserver uninstall 取得済み code-server を削除
|
|
67
72
|
```
|
|
68
73
|
|
|
69
74
|
### 起動オプション
|
|
@@ -167,6 +172,29 @@ ide/
|
|
|
167
172
|
| `AGENT_BROWSER_FFMPEG` | ffmpeg 実行ファイル | PATH から自動検出 |
|
|
168
173
|
| `AGENT_BROWSER_PACTL` | pactl 実行ファイル | PATH から自動検出 |
|
|
169
174
|
| `AGENT_BROWSER_AUDIO_SOURCE` | 音声キャプチャ元 | 既定 sink の monitor source |
|
|
175
|
+
| `AGENT_BROWSER_WEBRTC` | エージェントブラウザ映像を WebRTC 配信(`true`/`auto`/`false`、現状はオプトイン) | false |
|
|
176
|
+
| `AGENT_BROWSER_WEBRTC_BITRATE` | WebRTC ビットレート(ffmpeg `-b:v` 形式) | 3M |
|
|
177
|
+
| `AGENT_BROWSER_WEBRTC_FPS` | WebRTC フレームレート | 30 |
|
|
178
|
+
| `AGENT_BROWSER_WEBRTC_ICE_SERVERS` | ICE サーバ(JSON配列)。空ならホスト候補のみ(同一ホスト/LAN) | — |
|
|
179
|
+
| `AGENT_BROWSER_DISPLAY` | キャプチャする X ディスプレイ(未設定なら Chrome から自動検出) | — |
|
|
180
|
+
| `AGENT_BROWSER_CDP_PORT` | 共有ブラウザの CDP ポート | 9222 |
|
|
181
|
+
| `AGENT_BROWSER_REAP_MCP` | リークした chrome-devtools-mcp スタックを自動回収 | true |
|
|
182
|
+
| `AGENT_BROWSER_MAX_MCP_PER_AGENT` | 1エージェントが保持できる MCP スタック上限(超過分の古いものを回収) | 3 |
|
|
183
|
+
| `AGENT_BROWSER_MCP_IDLE_MS` | 回収対象とみなすアイドル閾値 (ms) | 600000 |
|
|
184
|
+
| `DECKIDE_CODE_SERVER_BIN` | code-server バイナリ(未設定なら ~/.deckide → PATH → 初回自動DL) | — |
|
|
185
|
+
| `DECKIDE_CODE_SERVER_VERSION` | 取得する code-server バージョン | 4.125.0 |
|
|
186
|
+
|
|
187
|
+
## Deck Drive シェルと code-server
|
|
188
|
+
|
|
189
|
+
既定の UI は **Deck Drive**(Google Drive 風のファイル/ワークスペース・ブラウザ)です。ファイルを開くと **code-server(ブラウザ版 VS Code)** が同一オリジンのリバースプロキシ(`/codeserver/:wsId/*`)越しに起動します。ターミナルデッキ・エージェントブラウザもタブとして同じシェルに統合されています。
|
|
190
|
+
|
|
191
|
+
- **code-server の取得**: 初回利用時に固定版(`DECKIDE_CODE_SERVER_VERSION`、既定 4.125.0)を `~/.deckide/code-server` へ自動ダウンロードします。手動なら `deckide codeserver install`。
|
|
192
|
+
- **拡張機能**: Microsoft マーケットプレイスは利用不可(Open VSX のみ)。Pylance/Copilot/Remote-* は使えません(Pyright 等で代替)。
|
|
193
|
+
- **リソース**: code-server はワークスペース毎に1プロセス(目安 +1GB RAM / +2 コア)。アイドル20分で自動停止します。
|
|
194
|
+
- **認証**: code-server は `--auth none` で起動し、Deck IDE の Basic 認証配下の同一オリジンプロキシで保護します。
|
|
195
|
+
- **旧 UI**: 従来の Monaco/分割ターミナル・シェルは `?shell=legacy` で引き続き利用できます。
|
|
196
|
+
|
|
197
|
+
> エージェントブラウザの WebRTC 配信(`AGENT_BROWSER_WEBRTC` / `?webrtc=1`)は実験的なオプトインです。既定は従来の JPEG ストリーミングです。音声トラックは PulseAudio がある環境でのみ動作します。
|
|
170
198
|
|
|
171
199
|
## MCP / OAuth
|
|
172
200
|
|
package/bin/deckide.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -172,6 +172,11 @@ Usage:
|
|
|
172
172
|
deckide config get <key> Get a config value
|
|
173
173
|
deckide config reset Reset all settings
|
|
174
174
|
|
|
175
|
+
deckide codeserver install Download/install code-server (the VS Code editor)
|
|
176
|
+
deckide codeserver status Show code-server install status
|
|
177
|
+
deckide codeserver path Print the code-server binary path
|
|
178
|
+
deckide codeserver uninstall Remove the downloaded code-server
|
|
179
|
+
|
|
175
180
|
Options (for start):
|
|
176
181
|
-p, --port <port> Port (default: 8787)
|
|
177
182
|
--host <host> Host (default: 0.0.0.0)
|
|
@@ -338,6 +343,50 @@ if (command === 'auth') {
|
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
// ── deckide status ──
|
|
346
|
+
// ── deckide codeserver ──
|
|
347
|
+
if (command === 'codeserver') {
|
|
348
|
+
const sub = args[1] || 'status';
|
|
349
|
+
process.env.DECKIDE_DATA_DIR = dataDir;
|
|
350
|
+
const modPath = path.join(__dirname, '..', 'dist', 'utils', 'code-server.js');
|
|
351
|
+
let CodeServerService;
|
|
352
|
+
try {
|
|
353
|
+
({ CodeServerService } = await import(modPath));
|
|
354
|
+
} catch (e) {
|
|
355
|
+
console.error(`code-server module not found — is Deck IDE built? (${e.message})`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const cs = new CodeServerService(new Map());
|
|
359
|
+
if (sub === 'install') {
|
|
360
|
+
try {
|
|
361
|
+
const bin = await cs.download((m) => console.log(` ${m}`));
|
|
362
|
+
console.log(`\x1b[32mInstalled\x1b[0m → ${bin}`);
|
|
363
|
+
process.exit(0);
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error(`\x1b[31mInstall failed:\x1b[0m ${e.message}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
} else if (sub === 'status') {
|
|
369
|
+
const s = await cs.getStatus();
|
|
370
|
+
console.log('code-server');
|
|
371
|
+
console.log(` acquired: ${s.acquired}`);
|
|
372
|
+
console.log(` binPath: ${s.binPath || '(none)'}`);
|
|
373
|
+
if (s.reason) console.log(` note: ${s.reason}`);
|
|
374
|
+
process.exit(0);
|
|
375
|
+
} else if (sub === 'path') {
|
|
376
|
+
const bin = await cs.resolveBin();
|
|
377
|
+
if (bin) { console.log(bin); process.exit(0); }
|
|
378
|
+
console.error('code-server is not installed (run: deckide codeserver install)');
|
|
379
|
+
process.exit(1);
|
|
380
|
+
} else if (sub === 'uninstall') {
|
|
381
|
+
await cs.uninstall();
|
|
382
|
+
console.log('Uninstalled code-server.');
|
|
383
|
+
process.exit(0);
|
|
384
|
+
} else {
|
|
385
|
+
console.error('usage: deckide codeserver install|status|path|uninstall');
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
341
390
|
if (command === 'status') {
|
|
342
391
|
const settings = loadSettings();
|
|
343
392
|
const port = settings.port || 8787;
|
|
@@ -71,6 +71,14 @@ export function createCodeServerRouter(codeServer) {
|
|
|
71
71
|
}
|
|
72
72
|
return c.json(await codeServer.getStatus(wsId || undefined));
|
|
73
73
|
});
|
|
74
|
+
// Kick off the (single-flight) code-server download without blocking — the
|
|
75
|
+
// client polls /status until acquired. No-op if already installed.
|
|
76
|
+
router.post('/install', async (c) => {
|
|
77
|
+
if (!(await codeServer.isAcquired())) {
|
|
78
|
+
void codeServer.download().catch(() => undefined);
|
|
79
|
+
}
|
|
80
|
+
return c.json(await codeServer.getStatus());
|
|
81
|
+
});
|
|
74
82
|
// Reverse-proxy everything else under /codeserver/:wsId/* to that workspace's
|
|
75
83
|
// code-server instance (prefix stripped).
|
|
76
84
|
router.all('/:wsId/*', (c) => proxy(c, codeServer));
|
|
@@ -480,6 +480,25 @@ export class AgentBrowserService {
|
|
|
480
480
|
})
|
|
481
481
|
.catch(() => undefined);
|
|
482
482
|
}
|
|
483
|
+
// Pin the active tab's render viewport to a fixed size (the WebRTC framebuffer)
|
|
484
|
+
// so captured pixels equal page coordinates. Does not mutate this.viewport, so
|
|
485
|
+
// the JPEG path's client-driven resize is unaffected when WebRTC isn't active.
|
|
486
|
+
async pinViewport(width, height) {
|
|
487
|
+
await this.ensureActiveCdp();
|
|
488
|
+
if (!this.activeCdp) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
await this.activeCdp
|
|
492
|
+
.send('Emulation.setDeviceMetricsOverride', {
|
|
493
|
+
width,
|
|
494
|
+
height,
|
|
495
|
+
deviceScaleFactor: 1,
|
|
496
|
+
mobile: false,
|
|
497
|
+
screenWidth: width,
|
|
498
|
+
screenHeight: height,
|
|
499
|
+
})
|
|
500
|
+
.catch(() => undefined);
|
|
501
|
+
}
|
|
483
502
|
async navigate(input) {
|
|
484
503
|
const url = normalizeBrowserUrl(input);
|
|
485
504
|
await this.start();
|
|
@@ -623,6 +642,9 @@ export class AgentBrowserService {
|
|
|
623
642
|
this.port = null;
|
|
624
643
|
this.disconnectActiveCdp();
|
|
625
644
|
this.closeBrowserCdp();
|
|
645
|
+
// Tear down the WebRTC relay too, so a Chrome crash never leaves an
|
|
646
|
+
// orphaned x11grab/ffmpeg encoder pointed at a dead display.
|
|
647
|
+
void this.webrtcRelay.stop();
|
|
626
648
|
this.tabs.clear();
|
|
627
649
|
this.activeTargetId = null;
|
|
628
650
|
this.ready = false;
|
|
@@ -4,7 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { WebSocket } from 'ws';
|
|
7
|
-
import { AGENT_BROWSER_WEBRTC, AGENT_BROWSER_WEBRTC_CODEC, AGENT_BROWSER_WEBRTC_BITRATE, AGENT_BROWSER_WEBRTC_FPS, AGENT_BROWSER_DISPLAY, AGENT_BROWSER_WEBRTC_ICE_SERVERS, } from '../config.js';
|
|
7
|
+
import { AGENT_BROWSER_WEBRTC, AGENT_BROWSER_WEBRTC_CODEC, AGENT_BROWSER_WEBRTC_BITRATE, AGENT_BROWSER_WEBRTC_FPS, AGENT_BROWSER_DISPLAY, AGENT_BROWSER_WEBRTC_ICE_SERVERS, AGENT_BROWSER_CDP_PORT, } from '../config.js';
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
let weriftPromise = null;
|
|
10
10
|
function loadWerift() {
|
|
@@ -51,7 +51,7 @@ async function resolveExecutable(envName, names) {
|
|
|
51
51
|
// picks the display number for the Chrome child and exports DISPLAY/XAUTHORITY
|
|
52
52
|
// only into that child's environment, so the server process can't read them from
|
|
53
53
|
// its own env — we recover them from /proc/<pid>/environ instead.
|
|
54
|
-
async function resolveXEnv() {
|
|
54
|
+
async function resolveXEnv(cdpPort) {
|
|
55
55
|
if (AGENT_BROWSER_DISPLAY) {
|
|
56
56
|
return { display: AGENT_BROWSER_DISPLAY, xauthority: process.env.XAUTHORITY ?? null };
|
|
57
57
|
}
|
|
@@ -63,6 +63,8 @@ async function resolveXEnv() {
|
|
|
63
63
|
// Not Linux / no procfs.
|
|
64
64
|
return process.env.DISPLAY ? { display: process.env.DISPLAY, xauthority: process.env.XAUTHORITY ?? null } : null;
|
|
65
65
|
}
|
|
66
|
+
const portToken = cdpPort ? `--remote-debugging-port=${cdpPort}` : null;
|
|
67
|
+
let fallback = null;
|
|
66
68
|
for (const pid of pids) {
|
|
67
69
|
let comm = '';
|
|
68
70
|
try {
|
|
@@ -87,9 +89,27 @@ async function resolveXEnv() {
|
|
|
87
89
|
continue;
|
|
88
90
|
}
|
|
89
91
|
const xauthority = vars.find((v) => v.startsWith('XAUTHORITY='))?.slice('XAUTHORITY='.length) ?? null;
|
|
90
|
-
|
|
92
|
+
const candidate = { display, xauthority };
|
|
93
|
+
if (!portToken) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
// With multiple Chrome instances, prefer the one bound to OUR CDP port so we
|
|
97
|
+
// capture (and control) the same browser. Fall back to the first found.
|
|
98
|
+
let cmdline = '';
|
|
99
|
+
try {
|
|
100
|
+
cmdline = (await fs.readFile(`/proc/${pid}/cmdline`, 'utf8')).replace(/\0/g, ' ');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
if (cmdline.includes(portToken)) {
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
if (!fallback) {
|
|
109
|
+
fallback = candidate;
|
|
110
|
+
}
|
|
91
111
|
}
|
|
92
|
-
return process.env.DISPLAY ? { display: process.env.DISPLAY, xauthority: process.env.XAUTHORITY ?? null } : null;
|
|
112
|
+
return fallback ?? (process.env.DISPLAY ? { display: process.env.DISPLAY, xauthority: process.env.XAUTHORITY ?? null } : null);
|
|
93
113
|
}
|
|
94
114
|
// Query the real screen geometry of the display so x11grab captures the exact
|
|
95
115
|
// region (a mismatch makes ffmpeg error or grab garbage).
|
|
@@ -145,7 +165,7 @@ export class BrowserWebRtcRelay {
|
|
|
145
165
|
else {
|
|
146
166
|
const werift = await loadWerift();
|
|
147
167
|
const ffmpeg = await resolveExecutable('AGENT_BROWSER_FFMPEG', ['ffmpeg']);
|
|
148
|
-
const xenv = await resolveXEnv();
|
|
168
|
+
const xenv = await resolveXEnv(AGENT_BROWSER_CDP_PORT);
|
|
149
169
|
if (!werift) {
|
|
150
170
|
available = false;
|
|
151
171
|
reason = 'werift is not installed (npm install werift)';
|
|
@@ -298,7 +318,7 @@ export class BrowserWebRtcRelay {
|
|
|
298
318
|
if (!ffmpeg) {
|
|
299
319
|
throw new Error('ffmpeg is required for WebRTC streaming');
|
|
300
320
|
}
|
|
301
|
-
const xenv = await resolveXEnv();
|
|
321
|
+
const xenv = await resolveXEnv(AGENT_BROWSER_CDP_PORT);
|
|
302
322
|
if (!xenv) {
|
|
303
323
|
throw new Error('No X display found to capture');
|
|
304
324
|
}
|
|
@@ -311,6 +331,8 @@ export class BrowserWebRtcRelay {
|
|
|
311
331
|
// Size the page's OS window to fill the captured screen so the framebuffer
|
|
312
332
|
// region maps 1:1 to page coordinates. Best-effort; ignore failures.
|
|
313
333
|
await this.host.setActiveWindowBounds(0, 0, this.size.width, this.size.height).catch(() => undefined);
|
|
334
|
+
// Pin the page render size to the framebuffer so input coords map 1:1.
|
|
335
|
+
await this.host.pinViewport(this.size.width, this.size.height).catch(() => undefined);
|
|
314
336
|
// Bind an ephemeral UDP port; ffmpeg sends RTP here and we fan each packet
|
|
315
337
|
// out to every viewer's track (no re-encode in werift).
|
|
316
338
|
const udp = dgram.createSocket('udp4');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
2
3
|
import net from 'node:net';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -6,6 +7,9 @@ import { dataDir } from '../config.js';
|
|
|
6
7
|
const IDLE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
7
8
|
const START_TIMEOUT_MS = 30_000;
|
|
8
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);
|
|
9
13
|
function sleep(ms) {
|
|
10
14
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
15
|
}
|
|
@@ -49,6 +53,9 @@ export class CodeServerService {
|
|
|
49
53
|
workspaces;
|
|
50
54
|
instances = new Map();
|
|
51
55
|
reaper = null;
|
|
56
|
+
// Single-flight auto-download of the code-server binary.
|
|
57
|
+
downloadPromise = null;
|
|
58
|
+
downloading = false;
|
|
52
59
|
// Directory layout under ~/.deckide/code-server (set by config.dataDir).
|
|
53
60
|
root = path.join(dataDir, 'code-server');
|
|
54
61
|
constructor(workspaces) {
|
|
@@ -70,6 +77,62 @@ export class CodeServerService {
|
|
|
70
77
|
async isAcquired() {
|
|
71
78
|
return (await this.resolveBin()) !== null;
|
|
72
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
|
+
}
|
|
73
136
|
async getStatus(wsId) {
|
|
74
137
|
const bin = await this.resolveBin();
|
|
75
138
|
const instance = wsId ? this.instances.get(wsId) : undefined;
|
|
@@ -79,8 +142,13 @@ export class CodeServerService {
|
|
|
79
142
|
binPath: bin,
|
|
80
143
|
running: instance ? instance.proc.exitCode == null : false,
|
|
81
144
|
instances: this.instances.size,
|
|
145
|
+
downloading: this.downloading,
|
|
82
146
|
wsId: wsId ?? null,
|
|
83
|
-
reason: bin
|
|
147
|
+
reason: bin
|
|
148
|
+
? undefined
|
|
149
|
+
: this.downloading
|
|
150
|
+
? 'code-server is downloading…'
|
|
151
|
+
: 'code-server will be downloaded automatically on first use',
|
|
84
152
|
error: instance?.error,
|
|
85
153
|
};
|
|
86
154
|
}
|
|
@@ -99,9 +167,10 @@ export class CodeServerService {
|
|
|
99
167
|
if (!workspace) {
|
|
100
168
|
throw new Error(`Unknown workspace: ${wsId}`);
|
|
101
169
|
}
|
|
102
|
-
|
|
170
|
+
let bin = await this.resolveBin();
|
|
103
171
|
if (!bin) {
|
|
104
|
-
|
|
172
|
+
// Auto-acquire on first use (single-flight ~100MB download).
|
|
173
|
+
bin = await this.download();
|
|
105
174
|
}
|
|
106
175
|
const port = await getFreePort();
|
|
107
176
|
const userDataDir = path.join(this.root, 'user', wsId);
|