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 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
- return { display, xauthority };
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 ? undefined : 'code-server is not installed (run: deckide codeserver install)',
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
- const bin = await this.resolveBin();
170
+ let bin = await this.resolveBin();
103
171
  if (!bin) {
104
- throw new Error('code-server is not installed (run: deckide codeserver install)');
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.5.42",
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": {