deckide 3.0.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/apps/server/dist/config.js +77 -0
  4. package/apps/server/dist/index.js +5 -0
  5. package/apps/server/dist/middleware/auth.js +78 -0
  6. package/apps/server/dist/middleware/cors.js +26 -0
  7. package/apps/server/dist/middleware/security.js +16 -0
  8. package/apps/server/dist/pty-client.js +177 -0
  9. package/apps/server/dist/pty-daemon.js +246 -0
  10. package/apps/server/dist/routes/decks.js +95 -0
  11. package/apps/server/dist/routes/files.js +221 -0
  12. package/apps/server/dist/routes/git.js +775 -0
  13. package/apps/server/dist/routes/settings.js +95 -0
  14. package/apps/server/dist/routes/terminals.js +239 -0
  15. package/apps/server/dist/routes/workspaces.js +83 -0
  16. package/apps/server/dist/server.js +257 -0
  17. package/apps/server/dist/types.js +1 -0
  18. package/apps/server/dist/utils/database.js +136 -0
  19. package/apps/server/dist/utils/error.js +28 -0
  20. package/apps/server/dist/utils/path.js +98 -0
  21. package/apps/server/dist/utils/shell.js +4 -0
  22. package/apps/server/dist/websocket.js +207 -0
  23. package/apps/server/package.json +26 -0
  24. package/apps/web/dist/assets/index-C-usl0y0.css +32 -0
  25. package/apps/web/dist/assets/index-CibzlLP5.js +129 -0
  26. package/apps/web/dist/index.html +13 -0
  27. package/bin/deckide.js +79 -0
  28. package/package.json +77 -0
  29. package/packages/shared/dist/types.d.ts +124 -0
  30. package/packages/shared/dist/types.d.ts.map +1 -0
  31. package/packages/shared/dist/types.js +3 -0
  32. package/packages/shared/dist/types.js.map +1 -0
  33. package/packages/shared/dist/utils-node.d.ts +22 -0
  34. package/packages/shared/dist/utils-node.d.ts.map +1 -0
  35. package/packages/shared/dist/utils-node.js +35 -0
  36. package/packages/shared/dist/utils-node.js.map +1 -0
  37. package/packages/shared/dist/utils.d.ts +90 -0
  38. package/packages/shared/dist/utils.d.ts.map +1 -0
  39. package/packages/shared/dist/utils.js +186 -0
  40. package/packages/shared/dist/utils.js.map +1 -0
  41. package/packages/shared/package.json +16 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 tako0614
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Deck IDE
2
+
3
+ 複数のターミナルを並列で管理できる軽量なWeb IDE。AIエージェント(Claude Code、Codex CLI等)を永続的に動かすことに最適化されています。
4
+
5
+ ## 特徴
6
+
7
+ - **マルチターミナル** - 複数のターミナルを同時に起動・管理
8
+ - **Monaco Editor** - VS Codeと同じエディタエンジンによるコード編集
9
+ - **Git統合** - ステージング、コミット、プッシュ、プル、ブランチ管理
10
+ - **マルチリポジトリ対応** - ワークスペース内の複数Gitリポジトリを自動検出
11
+ - **ファイルエクスプローラー** - ファイル・フォルダの作成、削除、名前変更
12
+ - **Diffビューア** - 変更ファイルの差分表示
13
+ - **Windowsデスクトップアプリ** - Electronベースのネイティブアプリ(自動アップデート対応)
14
+
15
+ ## スクリーンショット
16
+
17
+ ```
18
+ ┌─────────────────────────────────────────────────────────────┐
19
+ │ [エクスプローラー] [Git] [設定] Deck IDE │
20
+ ├─────────┬───────────────────────────────────────────────────┤
21
+ │ ファイル │ Monaco Editor │
22
+ │ ツリー │ │
23
+ │ │ │
24
+ │ ├───────────────────────────────────────────────────┤
25
+ │ │ Terminal 1 │ Terminal 2 │
26
+ │ │ $ claude │ $ codex │
27
+ │ │ ... │ ... │
28
+ └─────────┴───────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ## インストール
32
+
33
+ ### Windowsデスクトップアプリ(推奨)
34
+
35
+ [Releases](https://github.com/tako0614/ide/releases)から最新の`Deck-IDE-Setup-x.x.x.exe`をダウンロードしてインストール。
36
+
37
+ ### ソースから実行
38
+
39
+ ```bash
40
+ # リポジトリをクローン
41
+ git clone https://github.com/tako0614/ide.git
42
+ cd ide
43
+
44
+ # 依存関係をインストール
45
+ npm install
46
+
47
+ # 開発モードで起動(Web + Server)
48
+ npm run dev:server # ターミナル1
49
+ npm run dev:web # ターミナル2
50
+
51
+ # またはビルドして起動
52
+ npm run serve
53
+ ```
54
+
55
+ ブラウザで http://localhost:3210 を開く。
56
+
57
+ ## プロジェクト構成
58
+
59
+ ```
60
+ ide/
61
+ ├── apps/
62
+ │ ├── web/ # フロントエンド (React + Vite)
63
+ │ ├── server/ # バックエンド (Hono + node-pty)
64
+ │ └── desktop/ # Electronアプリ
65
+ ├── packages/
66
+ │ └── shared/ # 共有型定義
67
+ └── docs/ # ドキュメント
68
+ ```
69
+
70
+ ## 技術スタック
71
+
72
+ ### フロントエンド
73
+ - React 19
74
+ - Vite
75
+ - Monaco Editor
76
+ - xterm.js (WebGL対応)
77
+ - TypeScript
78
+
79
+ ### バックエンド
80
+ - Hono (高速HTTPフレームワーク)
81
+ - node-pty (疑似ターミナル)
82
+ - simple-git (Git操作)
83
+ - WebSocket (ターミナル通信)
84
+ - SQLite (データ永続化)
85
+
86
+ ### デスクトップ
87
+ - Electron 40
88
+ - electron-builder
89
+ - electron-updater (自動アップデート)
90
+
91
+ ## 使い方
92
+
93
+ ### ワークスペースの追加
94
+
95
+ 1. 左サイドバーの「+」ボタンをクリック
96
+ 2. ディレクトリパスを入力(例: `C:\Projects\myapp`)
97
+ 3. ワークスペースが追加される
98
+
99
+ ### デッキの作成
100
+
101
+ 1. ワークスペースを選択
102
+ 2. 「デッキ作成」ボタンをクリック
103
+ 3. デッキ名を入力
104
+
105
+ ### ターミナルの使用
106
+
107
+ - 「ターミナル追加」: 新しいターミナルを開く
108
+ - 「Claude」: `claude` コマンドを実行するターミナルを開く
109
+ - 「Codex」: `codex` コマンドを実行するターミナルを開く
110
+
111
+ ### Git操作
112
+
113
+ 1. サイドナビでGitアイコンをクリック
114
+ 2. 変更ファイルを確認
115
+ 3. 「+」でステージング、「−」でアンステージ
116
+ 4. コミットメッセージを入力してコミット
117
+ 5. プッシュ/プル/フェッチボタンで同期
118
+
119
+ ## 開発
120
+
121
+ ```bash
122
+ # 依存関係のインストール
123
+ npm install
124
+
125
+ # 開発サーバー起動
126
+ npm run dev:server # サーバー (port 3210)
127
+ npm run dev:web # Web (port 5173)
128
+
129
+ # ビルド
130
+ npm run build:web # Webのみ
131
+ npm run build:server # サーバーのみ
132
+ npm run build:desktop # デスクトップアプリ
133
+
134
+ # デスクトップアプリの開発
135
+ npm run dev:desktop
136
+ ```
137
+
138
+ ## 環境変数
139
+
140
+ サーバーは以下の環境変数をサポートします:
141
+
142
+ | 変数名 | 説明 | デフォルト |
143
+ |--------|------|-----------|
144
+ | `PORT` | サーバーポート | 3210 |
145
+ | `HOST` | バインドアドレス | localhost |
146
+ | `BASIC_AUTH_USER` | Basic認証ユーザー名 | (なし) |
147
+ | `BASIC_AUTH_PASSWORD` | Basic認証パスワード | (なし) |
148
+ | `CORS_ORIGIN` | CORS許可オリジン | * (開発時) |
149
+ | `DEFAULT_ROOT` | デフォルトルートパス | (なし) |
150
+
151
+ ## API
152
+
153
+ ### ワークスペース
154
+ - `GET /api/workspaces` - ワークスペース一覧
155
+ - `POST /api/workspaces` - ワークスペース作成
156
+
157
+ ### デッキ
158
+ - `GET /api/decks` - デッキ一覧
159
+ - `POST /api/decks` - デッキ作成
160
+
161
+ ### ファイル
162
+ - `GET /api/files` - ファイル一覧
163
+ - `GET /api/file` - ファイル読み込み
164
+ - `PUT /api/file` - ファイル保存
165
+ - `POST /api/file` - ファイル作成
166
+ - `DELETE /api/file` - ファイル削除
167
+
168
+ ### ターミナル
169
+ - `GET /api/terminals` - ターミナル一覧
170
+ - `POST /api/terminals` - ターミナル作成
171
+ - `DELETE /api/terminals/:id` - ターミナル削除
172
+ - `WS /api/terminals/:id` - ターミナル接続
173
+
174
+ ### Git
175
+ - `GET /api/git/status` - Gitステータス
176
+ - `GET /api/git/repos` - リポジトリ一覧
177
+ - `POST /api/git/stage` - ステージング
178
+ - `POST /api/git/unstage` - アンステージ
179
+ - `POST /api/git/commit` - コミット
180
+ - `POST /api/git/push` - プッシュ
181
+ - `POST /api/git/pull` - プル
182
+ - `GET /api/git/diff` - 差分取得
183
+ - `GET /api/git/branches` - ブランチ一覧
184
+ - `POST /api/git/checkout` - ブランチ切り替え
185
+
186
+ ## ライセンス
187
+
188
+ MIT
189
+
190
+ ## 作者
191
+
192
+ [tako0614](https://github.com/tako0614)
@@ -0,0 +1,77 @@
1
+ import os from 'node:os';
2
+ import fsSync from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ // When running as a global CLI (`deckide`), DECKIDE_DATA_DIR is set to ~/.deckide/
7
+ const globalDataDir = process.env.DECKIDE_DATA_DIR;
8
+ export const SETTINGS_FILE = globalDataDir
9
+ ? path.join(globalDataDir, 'settings.json')
10
+ : path.join(__dirname, '..', '..', 'settings.json');
11
+ let fileSettings = {};
12
+ try {
13
+ const settingsData = fsSync.readFileSync(SETTINGS_FILE, 'utf-8');
14
+ fileSettings = JSON.parse(settingsData);
15
+ console.log('[CONFIG] Loaded settings from file');
16
+ }
17
+ catch {
18
+ // No settings file, use environment variables
19
+ }
20
+ function parseIntEnv(val, defaultVal) {
21
+ if (typeof val === 'number')
22
+ return Number.isFinite(val) ? Math.floor(val) : defaultVal;
23
+ const n = parseInt(val ?? '', 10);
24
+ return Number.isFinite(n) ? n : defaultVal;
25
+ }
26
+ export const DEFAULT_ROOT = process.env.DEFAULT_ROOT || os.homedir();
27
+ export const PORT = parseIntEnv(process.env.PORT ?? fileSettings.port, 8787);
28
+ export const HOST = process.env.HOST || '0.0.0.0';
29
+ export const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || (fileSettings.basicAuthEnabled ? fileSettings.basicAuthUser : undefined);
30
+ export const BASIC_AUTH_PASSWORD = process.env.BASIC_AUTH_PASSWORD || (fileSettings.basicAuthEnabled ? fileSettings.basicAuthPassword : undefined);
31
+ export const CORS_ORIGIN = process.env.CORS_ORIGIN;
32
+ export const NODE_ENV = process.env.NODE_ENV || 'development';
33
+ export const MAX_FILE_SIZE = parseIntEnv(process.env.MAX_FILE_SIZE, 10 * 1024 * 1024);
34
+ export const TERMINAL_BUFFER_LIMIT = parseIntEnv(process.env.TERMINAL_BUFFER_LIMIT, 500_000);
35
+ export const MAX_REQUEST_BODY_SIZE = parseIntEnv(process.env.MAX_REQUEST_BODY_SIZE, 1024 * 1024); // 1MB default
36
+ export const TRUST_PROXY = process.env.TRUST_PROXY === 'true'; // Only trust proxy headers if explicitly enabled
37
+ // In packaged app: server is at app.asar.unpacked/server/, web is at app.asar.unpacked/web/dist/
38
+ // In development: server is at apps/server/dist/, web is at apps/web/dist/
39
+ const packagedDistDir = path.resolve(__dirname, '..', 'web', 'dist');
40
+ const devDistDir = path.resolve(__dirname, '..', '..', 'web', 'dist');
41
+ export const distDir = fsSync.existsSync(packagedDistDir) ? packagedDistDir : devDistDir;
42
+ export const hasStatic = fsSync.existsSync(distDir);
43
+ const packagedDataDir = path.resolve(__dirname, '..', 'data');
44
+ const devDataDir = path.resolve(__dirname, '..', '..', 'data');
45
+ export const dataDir = globalDataDir
46
+ ? globalDataDir
47
+ : fsSync.existsSync(path.dirname(packagedDataDir)) && !fsSync.existsSync(devDataDir) ? packagedDataDir : devDataDir;
48
+ export const dbPath = process.env.DB_PATH || path.join(dataDir, 'deck-ide.db');
49
+ // Validate critical configuration
50
+ if (NODE_ENV === 'production') {
51
+ if (!CORS_ORIGIN) {
52
+ console.error('CRITICAL: CORS_ORIGIN must be set in production!');
53
+ process.exit(1);
54
+ }
55
+ // Validate password strength in production
56
+ if (BASIC_AUTH_PASSWORD && BASIC_AUTH_PASSWORD.length < 12) {
57
+ console.error('CRITICAL: BASIC_AUTH_PASSWORD must be at least 12 characters in production!');
58
+ process.exit(1);
59
+ }
60
+ // Warn if no authentication is configured
61
+ if (!BASIC_AUTH_USER || !BASIC_AUTH_PASSWORD) {
62
+ console.warn('WARNING: No authentication configured! API is publicly accessible.');
63
+ }
64
+ }
65
+ // Validate numeric configuration values
66
+ if (!Number.isFinite(PORT) || PORT < 1 || PORT > 65535) {
67
+ console.error('CRITICAL: Invalid PORT value');
68
+ process.exit(1);
69
+ }
70
+ if (!Number.isFinite(MAX_FILE_SIZE) || MAX_FILE_SIZE < 1024) {
71
+ console.error('CRITICAL: Invalid MAX_FILE_SIZE value');
72
+ process.exit(1);
73
+ }
74
+ // Ensure data directory exists
75
+ fsSync.mkdirSync(path.dirname(dbPath), { recursive: true });
76
+ // PTY daemon info file - written by daemon on startup so server can find its port
77
+ export const daemonInfoPath = path.join(path.dirname(dbPath), 'pty-daemon.json');
@@ -0,0 +1,5 @@
1
+ import { createServer } from './server.js';
2
+ createServer().catch((err) => {
3
+ console.error('Failed to start server:', err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,78 @@
1
+ import { basicAuth } from 'hono/basic-auth';
2
+ import crypto from 'node:crypto';
3
+ import { BASIC_AUTH_USER, BASIC_AUTH_PASSWORD } from '../config.js';
4
+ export const basicAuthMiddleware = BASIC_AUTH_USER && BASIC_AUTH_PASSWORD
5
+ ? basicAuth({ username: BASIC_AUTH_USER, password: BASIC_AUTH_PASSWORD })
6
+ : undefined;
7
+ // WebSocket token management
8
+ const WS_TOKEN_TTL_MS = 30 * 1000; // 30 seconds
9
+ const wsTokens = new Map(); // token -> expiry timestamp
10
+ // Cleanup expired tokens periodically
11
+ setInterval(() => {
12
+ const now = Date.now();
13
+ for (const [token, expiry] of wsTokens.entries()) {
14
+ if (now > expiry) {
15
+ wsTokens.delete(token);
16
+ }
17
+ }
18
+ }, 10000).unref();
19
+ /**
20
+ * Generate a one-time WebSocket token
21
+ */
22
+ export function generateWsToken() {
23
+ const token = crypto.randomBytes(32).toString('hex');
24
+ wsTokens.set(token, Date.now() + WS_TOKEN_TTL_MS);
25
+ return token;
26
+ }
27
+ /**
28
+ * Validate and consume a WebSocket token
29
+ */
30
+ export function validateWsToken(token) {
31
+ const expiry = wsTokens.get(token);
32
+ if (!expiry) {
33
+ return false;
34
+ }
35
+ wsTokens.delete(token); // One-time use
36
+ return Date.now() <= expiry;
37
+ }
38
+ /**
39
+ * Check if Basic Auth is enabled
40
+ */
41
+ export function isBasicAuthEnabled() {
42
+ return Boolean(BASIC_AUTH_USER && BASIC_AUTH_PASSWORD);
43
+ }
44
+ function timingSafeEqual(a, b) {
45
+ const aBuf = Buffer.from(a);
46
+ const bBuf = Buffer.from(b);
47
+ if (aBuf.length !== bBuf.length) {
48
+ // Dummy comparison to prevent length-based timing leak
49
+ crypto.timingSafeEqual(aBuf, aBuf);
50
+ return false;
51
+ }
52
+ return crypto.timingSafeEqual(aBuf, bBuf);
53
+ }
54
+ export function verifyWebSocketAuth(req) {
55
+ if (!BASIC_AUTH_USER || !BASIC_AUTH_PASSWORD) {
56
+ return true;
57
+ }
58
+ // Check for token in query string first
59
+ const url = new URL(req.url || '', 'http://localhost');
60
+ const token = url.searchParams.get('token');
61
+ if (token && validateWsToken(token)) {
62
+ return true;
63
+ }
64
+ // Fall back to Basic Auth header
65
+ const authHeader = req.headers.authorization;
66
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
67
+ return false;
68
+ }
69
+ const base64Credentials = authHeader.slice('Basic '.length);
70
+ const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
71
+ const colonIndex = credentials.indexOf(':');
72
+ if (colonIndex === -1) {
73
+ return false;
74
+ }
75
+ const username = credentials.substring(0, colonIndex);
76
+ const password = credentials.substring(colonIndex + 1);
77
+ return timingSafeEqual(username, BASIC_AUTH_USER) && timingSafeEqual(password, BASIC_AUTH_PASSWORD);
78
+ }
@@ -0,0 +1,26 @@
1
+ import { CORS_ORIGIN, NODE_ENV, PORT } from '../config.js';
2
+ const DEV_ALLOWED_ORIGINS = [
3
+ 'http://localhost:5173',
4
+ 'http://localhost:3000',
5
+ `http://localhost:${PORT}`,
6
+ ];
7
+ export const corsMiddleware = async (c, next) => {
8
+ if (CORS_ORIGIN) {
9
+ c.header('Access-Control-Allow-Origin', CORS_ORIGIN);
10
+ }
11
+ else if (NODE_ENV === 'development') {
12
+ const origin = c.req.header('origin');
13
+ if (origin && DEV_ALLOWED_ORIGINS.includes(origin)) {
14
+ c.header('Access-Control-Allow-Origin', origin);
15
+ }
16
+ else {
17
+ c.header('Access-Control-Allow-Origin', `http://localhost:${PORT}`);
18
+ }
19
+ }
20
+ c.header('Access-Control-Allow-Methods', 'GET,POST,PUT,OPTIONS');
21
+ c.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
22
+ if (c.req.method === 'OPTIONS') {
23
+ return c.body(null, 204);
24
+ }
25
+ await next();
26
+ };
@@ -0,0 +1,16 @@
1
+ // Security event logging
2
+ export function logSecurityEvent(event, details) {
3
+ const timestamp = new Date().toISOString();
4
+ console.warn(`[SECURITY] ${timestamp} ${event}:`, JSON.stringify(details));
5
+ }
6
+ export const securityHeaders = async (c, next) => {
7
+ c.header('X-Frame-Options', 'DENY');
8
+ c.header('X-Content-Type-Options', 'nosniff');
9
+ c.header('X-XSS-Protection', '1; mode=block');
10
+ c.header('Referrer-Policy', 'strict-origin-when-cross-origin');
11
+ c.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
12
+ c.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
13
+ // Allow Monaco Editor from CDN, Google Fonts, and blob: for workers
14
+ c.header('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net blob:; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com; font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com data:; img-src 'self' data: blob:; connect-src 'self' ws: wss:; worker-src 'self' blob:;");
15
+ await next();
16
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * PtyClient - IPC client for the main server to communicate with the PTY daemon.
3
+ * Uses newline-delimited JSON over a local TCP connection.
4
+ */
5
+ import net from 'node:net';
6
+ import { EventEmitter } from 'node:events';
7
+ export class PtyClient extends EventEmitter {
8
+ socket = null;
9
+ lineBuf = '';
10
+ _connected = false;
11
+ listCallback = null;
12
+ shutdownCallback = null;
13
+ createCallbacks = new Map();
14
+ get connected() {
15
+ return this._connected;
16
+ }
17
+ connect(port) {
18
+ return new Promise((resolve, reject) => {
19
+ const socket = net.createConnection(port, '127.0.0.1');
20
+ socket.once('connect', () => {
21
+ this.socket = socket;
22
+ this._connected = true;
23
+ resolve();
24
+ });
25
+ socket.once('error', (err) => {
26
+ if (!this._connected) {
27
+ reject(err);
28
+ }
29
+ else {
30
+ this.emit('error', err);
31
+ }
32
+ });
33
+ socket.on('data', (chunk) => {
34
+ this.lineBuf += chunk.toString('utf8');
35
+ const lines = this.lineBuf.split('\n');
36
+ this.lineBuf = lines.pop();
37
+ for (const line of lines) {
38
+ if (!line.trim())
39
+ continue;
40
+ try {
41
+ this.handleMessage(JSON.parse(line));
42
+ }
43
+ catch { /* malformed message, ignore */ }
44
+ }
45
+ });
46
+ socket.on('close', () => {
47
+ this._connected = false;
48
+ this.socket = null;
49
+ if (this.shutdownCallback) {
50
+ this.shutdownCallback(false);
51
+ this.shutdownCallback = null;
52
+ }
53
+ this.emit('disconnect');
54
+ });
55
+ });
56
+ }
57
+ handleMessage(msg) {
58
+ switch (msg.type) {
59
+ case 'data':
60
+ this.emit('data', msg.id, msg.data);
61
+ break;
62
+ case 'exit':
63
+ this.emit('exit', msg.id, msg.code);
64
+ break;
65
+ case 'list_result': {
66
+ const cb = this.listCallback;
67
+ this.listCallback = null;
68
+ cb?.(msg.terminals);
69
+ break;
70
+ }
71
+ case 'shutdown_ack': {
72
+ const cb = this.shutdownCallback;
73
+ this.shutdownCallback = null;
74
+ cb?.(true);
75
+ break;
76
+ }
77
+ case 'created': {
78
+ const cb = this.createCallbacks.get(msg.id);
79
+ if (cb) {
80
+ this.createCallbacks.delete(msg.id);
81
+ cb();
82
+ }
83
+ break;
84
+ }
85
+ case 'error': {
86
+ if (msg.id) {
87
+ const cb = this.createCallbacks.get(msg.id);
88
+ if (cb) {
89
+ this.createCallbacks.delete(msg.id);
90
+ cb(msg.message);
91
+ }
92
+ }
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ send(msg) {
98
+ if (this.socket && !this.socket.destroyed) {
99
+ try {
100
+ this.socket.write(JSON.stringify(msg) + '\n');
101
+ }
102
+ catch { /* socket may be closing */ }
103
+ }
104
+ }
105
+ /** Spawn a new PTY in the daemon. Resolves when daemon confirms creation. */
106
+ create(params) {
107
+ return new Promise((resolve, reject) => {
108
+ const timer = setTimeout(() => {
109
+ this.createCallbacks.delete(params.id);
110
+ reject(new Error(`Timeout creating terminal ${params.id}`));
111
+ }, 10_000);
112
+ this.createCallbacks.set(params.id, (err) => {
113
+ clearTimeout(timer);
114
+ if (err)
115
+ reject(new Error(err));
116
+ else
117
+ resolve();
118
+ });
119
+ this.send({ type: 'create', ...params });
120
+ });
121
+ }
122
+ /** Send keyboard input to a terminal. */
123
+ input(id, data) {
124
+ this.send({ type: 'input', id, data });
125
+ }
126
+ /** Resize a terminal. */
127
+ resize(id, cols, rows) {
128
+ this.send({ type: 'resize', id, cols, rows });
129
+ }
130
+ /** Kill a terminal and remove it from the daemon. */
131
+ kill(id) {
132
+ this.send({ type: 'kill', id });
133
+ }
134
+ /**
135
+ * Subscribe to live data from a terminal.
136
+ * The daemon will first send any buffered output since `bufferOffset`,
137
+ * then stream all subsequent output.
138
+ */
139
+ attach(id, bufferOffset) {
140
+ this.send({ type: 'attach', id, bufferOffset });
141
+ }
142
+ /** Get a list of all terminals currently alive in the daemon. */
143
+ list() {
144
+ return new Promise((resolve) => {
145
+ const timer = setTimeout(() => {
146
+ this.listCallback = null;
147
+ resolve([]);
148
+ }, 5_000);
149
+ this.listCallback = (terminals) => {
150
+ clearTimeout(timer);
151
+ resolve(terminals);
152
+ };
153
+ this.send({ type: 'list' });
154
+ });
155
+ }
156
+ /** Ask the daemon to terminate itself and all child PTYs. */
157
+ shutdown(timeoutMs = 2_000) {
158
+ if (!this.connected) {
159
+ return Promise.resolve(false);
160
+ }
161
+ return new Promise((resolve) => {
162
+ const timer = setTimeout(() => {
163
+ this.shutdownCallback = null;
164
+ resolve(false);
165
+ }, timeoutMs);
166
+ this.shutdownCallback = (ok) => {
167
+ clearTimeout(timer);
168
+ this.shutdownCallback = null;
169
+ resolve(ok);
170
+ };
171
+ this.send({ type: 'shutdown' });
172
+ });
173
+ }
174
+ destroy() {
175
+ this.socket?.destroy();
176
+ }
177
+ }