@ironmussa/funny 0.1.7 → 0.1.9
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 +5 -6
- package/bin/funny.js +88 -26
- package/package.json +3 -1
- package/packages/client/dist/assets/{AcceptInvitePage-BjMGmkD4.js → AcceptInvitePage-D13hs9Zr.js} +1 -1
- package/packages/client/dist/assets/{ActivityPane-Cl-bUP3W.js → ActivityPane-CtxWrqSm.js} +1 -1
- package/packages/client/dist/assets/{AddProjectView-DZuks8BQ.js → AddProjectView-BrSbq9Zq.js} +1 -1
- package/packages/client/dist/assets/{AllThreadsView-DdT3QdFb.js → AllThreadsView-DVZkf_3a.js} +1 -1
- package/packages/client/dist/assets/{AnalyticsView-b_BZhrh1.js → AnalyticsView-DudiSHyr.js} +1 -1
- package/packages/client/dist/assets/{App-CY9kuarV.js → App-BkudlgY9.js} +3 -3
- package/packages/client/dist/assets/{AutomationInboxView-BQfGidRk.js → AutomationInboxView-B20N9Rzu.js} +1 -1
- package/packages/client/dist/assets/{BrowserPanel-CY_wB1AB.js → BrowserPanel-Dz4qr3zh.js} +1 -1
- package/packages/client/dist/assets/{BrowserPreview-2gdJqMpN.js → BrowserPreview-CKJfq_nE.js} +1 -1
- package/packages/client/dist/assets/{CircuitBreakerDialog-B6kP1aCS.js → CircuitBreakerDialog-CIYWhLBH.js} +1 -1
- package/packages/client/dist/assets/{CommandPalette-DeBQCdqo.js → CommandPalette-BWdGw1dT.js} +1 -1
- package/packages/client/dist/assets/{CommentsPane-JY0CN5R2.js → CommentsPane-Cq3KSPeq.js} +1 -1
- package/packages/client/dist/assets/{CsvTable-CaJ4BQ4d.js → CsvTable-Y1HjLh1s.js} +1 -1
- package/packages/client/dist/assets/{FileSearchDialog-C2RhpM7Z.js → FileSearchDialog-3mjmh0eF.js} +1 -1
- package/packages/client/dist/assets/{FileTree-Bl25zGAE.js → FileTree-mnY0OIfR.js} +1 -1
- package/packages/client/dist/assets/{FolderPicker-CpXi_ZrF.js → FolderPicker-DlzSNNoW.js} +1 -1
- package/packages/client/dist/assets/{GeneralSettingsView-f8pBncJY.js → GeneralSettingsView-CxS_XI8Z.js} +2 -2
- package/packages/client/dist/assets/{GitProgressModal-CJG5MsSI.js → GitProgressModal-D-h3ocu1.js} +1 -1
- package/packages/client/dist/assets/{LiveColumnsView-DwUkXA8m.js → LiveColumnsView-BGRek-gh.js} +1 -1
- package/packages/client/dist/assets/{LoginPage-CkgRw9Ji.js → LoginPage-DATO3dWs.js} +1 -1
- package/packages/client/dist/assets/{MediaPreview-DmMe4hGN.js → MediaPreview-Dnzd03yo.js} +1 -1
- package/packages/client/dist/assets/{MediaPreviewDialog-DHIv3gai.js → MediaPreviewDialog-Dm1nhBR9.js} +1 -1
- package/packages/client/dist/assets/{MermaidBlock-BZLj2TmL.js → MermaidBlock-DeQ02Wb_.js} +1 -1
- package/packages/client/dist/assets/{MobilePage-BRWC2SVo.js → MobilePage-B_-4EwLh.js} +2 -2
- package/packages/client/dist/assets/{MonacoEditorDialog-CTUQ5NFr.js → MonacoEditorDialog-DtBfzOtQ.js} +1 -1
- package/packages/client/dist/assets/{OrchestratorView-wTF6Ae0A.js → OrchestratorView-CIthX7Z9.js} +1 -1
- package/packages/client/dist/assets/{PreferencesPanel-CpIL485D.js → PreferencesPanel-UKXVNtTZ.js} +1 -1
- package/packages/client/dist/assets/{PreviewBrowser-CEXO1tHq.js → PreviewBrowser-Dm_Aqtps.js} +1 -1
- package/packages/client/dist/assets/{ProjectFilesPane-02_p9zry.js → ProjectFilesPane-B59Vq3_s.js} +1 -1
- package/packages/client/dist/assets/{ProjectHeader-TWkvwAxm.js → ProjectHeader-Qf_M_sBm.js} +1 -1
- package/packages/client/dist/assets/{ProjectHooksSettings-CIaY32WM.js → ProjectHooksSettings-D23PXt7X.js} +1 -1
- package/packages/client/dist/assets/{ReviewPane-DHTvq8t1.js → ReviewPane-DEY-F3Zq.js} +1 -1
- package/packages/client/dist/assets/{SearchablePicker-BJnuJVEi.js → SearchablePicker-BQEWhSu4.js} +1 -1
- package/packages/client/dist/assets/{SettingsDetailView-eBDvn5cS.js → SettingsDetailView-CyaMNfi4.js} +1 -1
- package/packages/client/dist/assets/{SettingsPageContent-CiHRUvG3.js → SettingsPageContent-BMPAc4uQ.js} +3 -3
- package/packages/client/dist/assets/{SetupWizard-B8rFRPma.js → SetupWizard-8eO2SYvf.js} +1 -1
- package/packages/client/dist/assets/{Sidebar-CPU0yoaQ.js → Sidebar-CDKV9zDp.js} +1 -1
- package/packages/client/dist/assets/{StashTab-BswTfOlb.js → StashTab-KG5Zzy5-.js} +1 -1
- package/packages/client/dist/assets/{TestRunnerPane-DZXDbHCu.js → TestRunnerPane-C_gSYI4_.js} +1 -1
- package/packages/client/dist/assets/{TextSearchDialog-BUMBp448.js → TextSearchDialog-CX5B9hyW.js} +1 -1
- package/packages/client/dist/assets/{ThreadPowerline-CSegubLW.js → ThreadPowerline-DGdjong-.js} +1 -1
- package/packages/client/dist/assets/{ThreadStatusPin-DhrEC0n-.js → ThreadStatusPin-Bmauv5AI.js} +1 -1
- package/packages/client/dist/assets/{ThreadView-hYXeM23q.js → ThreadView-BvizmCnl.js} +1 -1
- package/packages/client/dist/assets/{app-store-DKW9sVHv.js → app-store-CSYr64U4.js} +1 -1
- package/packages/client/dist/assets/{automation-store-C8v9UlHX.js → automation-store-er1E9xPe.js} +1 -1
- package/packages/client/dist/assets/{browser-panel-store-CSx5jc7O.js → browser-panel-store-BriXQezL.js} +2 -2
- package/packages/client/dist/assets/{button-B9Ypx1uQ.js → button-Bd970BE2.js} +1 -1
- package/packages/client/dist/assets/{combine-CIrCcf3o.js → combine-Um6l0bxC.js} +1 -1
- package/packages/client/dist/assets/{comment-store-BfRTY3FY.js → comment-store-B4-PYjwU.js} +1 -1
- package/packages/client/dist/assets/{diff-parse-DgLol0S6.js → diff-parse-dnfUXHzR.js} +1 -1
- package/packages/client/dist/assets/{element-yfBHMYal.js → element-DW-xqM5t.js} +1 -1
- package/packages/client/dist/assets/{go-to-thread-DD0G5vFp.js → go-to-thread-F_voqQgr.js} +1 -1
- package/packages/client/dist/assets/{index-DNxUSLe_.js → index-ea63jMuV.js} +2 -2
- package/packages/client/dist/assets/{input-CJ3TsT2L.js → input-CcrOgEwd.js} +1 -1
- package/packages/client/dist/assets/{items-7qqySF-X.js → items-iRGH_xTR.js} +1 -1
- package/packages/client/dist/assets/{job-store-0PeOgBpX.js → job-store-CaOITASA.js} +1 -1
- package/packages/client/dist/assets/{kbd-d4Wzagpn.js → kbd-HqsH-rnU.js} +1 -1
- package/packages/client/dist/assets/{loading-state-yLj5fYb6.js → loading-state-BSdX_QZB.js} +1 -1
- package/packages/client/dist/assets/{markdown-components-DxMKBjnh.js → markdown-components-DXE4xITV.js} +1 -1
- package/packages/client/dist/assets/{native-git-store-CCenGgLt.js → native-git-store-B4G1m25Q.js} +1 -1
- package/packages/client/dist/assets/{orchestrator-store-DP8UohlO.js → orchestrator-store-CRIf-E8M.js} +1 -1
- package/packages/client/dist/assets/{pipeline-approval-store-BiH1mYlB.js → pipeline-approval-store-Sm4xCBYI.js} +1 -1
- package/packages/client/dist/assets/{pr-detail-store-DHF0exs_.js → pr-detail-store-DIz8x882.js} +1 -1
- package/packages/client/dist/assets/{providers-Dd9yhyUY.js → providers-B6JAFGOv.js} +1 -1
- package/packages/client/dist/assets/{sidebar-Dzb_F1kj.js → sidebar-D5gUIY0G.js} +1 -1
- package/packages/client/dist/assets/{tabs-Cu3cWMHb.js → tabs-Bx4u8bcz.js} +1 -1
- package/packages/client/dist/assets/{test-store-D72ZqWH2.js → test-store-D4S9R398.js} +1 -1
- package/packages/client/dist/assets/{tooltip-icon-button-8C1wJ-l9.js → tooltip-icon-button-C1xTZR5-.js} +1 -1
- package/packages/client/dist/assets/use-active-thread-id-1WUch4uf.js +1 -0
- package/packages/client/dist/assets/{use-branch-switch-C45m2mx7.js → use-branch-switch-CFzzWMNs.js} +1 -1
- package/packages/client/dist/assets/{use-image-lightbox-CsMDwY1v.js → use-image-lightbox-DgkrBc3N.js} +1 -1
- package/packages/client/dist/assets/{use-stable-navigate-Cz_jWBHP.js → use-stable-navigate-U6VQ7K0f.js} +1 -1
- package/packages/client/dist/assets/{use-terminal-scope-BX8zAMzc.js → use-terminal-scope-BvFrkm0r.js} +1 -1
- package/packages/client/dist/assets/{use-thread-creation-DSLV7F8I.js → use-thread-creation-CdjjkhNZ.js} +1 -1
- package/packages/client/dist/assets/{use-thread-search-CXsiNqRR.js → use-thread-search-CXgC1XEY.js} +1 -1
- package/packages/client/dist/assets/{use-todo-panel-DjPuehjq.js → use-todo-panel-BAbS_SpL.js} +1 -1
- package/packages/client/dist/assets/{use-unified-prompt-model-groups-wdA26Vnd.js → use-unified-prompt-model-groups-DH94Yhir.js} +1 -1
- package/packages/client/dist/assets/{use-ws-AWIhcFYo.js → use-ws-DfaWLbd3.js} +3 -3
- package/packages/client/dist/assets/{visualizer-loader-oxC04P38.js → visualizer-loader-BnEPAAGw.js} +1 -1
- package/packages/client/dist/assets/{watcher-store-gym76Ag_.js → watcher-store-DKHnSlTd.js} +1 -1
- package/packages/client/dist/index.html +1 -1
- package/packages/runtime/dist/index.js +2 -2
- package/packages/runtime/dist/index.js.map +1 -1
- package/packages/runtime/dist/pty-daemon.ts +508 -0
- package/packages/client/dist/assets/use-active-thread-id-ZlJtsqq4.js +0 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @domain subdomain: Process Management
|
|
3
|
+
* @domain subdomain-type: supporting
|
|
4
|
+
* @domain type: adapter
|
|
5
|
+
* @domain layer: infrastructure
|
|
6
|
+
*
|
|
7
|
+
* Persistent PTY daemon — runs as a standalone Bun process that owns all PTY
|
|
8
|
+
* shell processes. Survives server restarts so terminals stay alive.
|
|
9
|
+
*
|
|
10
|
+
* Communicates with the funny server via Unix domain socket using NDJSON.
|
|
11
|
+
* Each PTY session has a headless xterm.js instance for state capture.
|
|
12
|
+
*
|
|
13
|
+
* Usage: bun run packages/runtime/src/services/pty-daemon.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'fs';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import { resolve } from 'path';
|
|
19
|
+
|
|
20
|
+
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
21
|
+
import { Terminal as HeadlessTerminal } from '@xterm/headless';
|
|
22
|
+
import type { Subprocess } from 'bun';
|
|
23
|
+
|
|
24
|
+
// ── Configuration ────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const DATA_DIR = process.env.FUNNY_DATA_DIR
|
|
27
|
+
? resolve(process.env.FUNNY_DATA_DIR)
|
|
28
|
+
: resolve(homedir(), '.funny');
|
|
29
|
+
|
|
30
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
31
|
+
|
|
32
|
+
const SOCKET_PATH = resolve(DATA_DIR, 'pty.sock');
|
|
33
|
+
const PID_FILE = resolve(DATA_DIR, 'pty-daemon.pid');
|
|
34
|
+
const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
35
|
+
|
|
36
|
+
// Standalone daemon process — use stderr for logging (no access to project logger)
|
|
37
|
+
const daemonLog = (...args: unknown[]) => process.stderr.write(`${args.join(' ')}\n`);
|
|
38
|
+
|
|
39
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
interface DaemonSession {
|
|
42
|
+
id: string;
|
|
43
|
+
proc: Subprocess;
|
|
44
|
+
headless: InstanceType<typeof HeadlessTerminal>;
|
|
45
|
+
serialize: SerializeAddon;
|
|
46
|
+
cwd: string;
|
|
47
|
+
shell: string;
|
|
48
|
+
cols: number;
|
|
49
|
+
rows: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SpawnCmd {
|
|
53
|
+
cmd: 'spawn';
|
|
54
|
+
id: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
cols: number;
|
|
57
|
+
rows: number;
|
|
58
|
+
shell?: string;
|
|
59
|
+
env?: Record<string, string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface WriteCmd {
|
|
63
|
+
cmd: 'write';
|
|
64
|
+
id: string;
|
|
65
|
+
data: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ResizeCmd {
|
|
69
|
+
cmd: 'resize';
|
|
70
|
+
id: string;
|
|
71
|
+
cols: number;
|
|
72
|
+
rows: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface KillCmd {
|
|
76
|
+
cmd: 'kill';
|
|
77
|
+
id: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ListCmd {
|
|
81
|
+
cmd: 'list';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface CaptureCmd {
|
|
85
|
+
cmd: 'capture';
|
|
86
|
+
id: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ShutdownCmd {
|
|
90
|
+
cmd: 'shutdown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface PingCmd {
|
|
94
|
+
cmd: 'ping';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface SignalCmd {
|
|
98
|
+
cmd: 'signal';
|
|
99
|
+
id: string;
|
|
100
|
+
signal: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type DaemonCommand =
|
|
104
|
+
| SpawnCmd
|
|
105
|
+
| WriteCmd
|
|
106
|
+
| ResizeCmd
|
|
107
|
+
| KillCmd
|
|
108
|
+
| ListCmd
|
|
109
|
+
| CaptureCmd
|
|
110
|
+
| ShutdownCmd
|
|
111
|
+
| PingCmd
|
|
112
|
+
| SignalCmd;
|
|
113
|
+
|
|
114
|
+
// ── Session Management ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const sessions = new Map<string, DaemonSession>();
|
|
117
|
+
const clients = new Set<import('bun').Socket>();
|
|
118
|
+
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
119
|
+
|
|
120
|
+
function broadcast(msg: object): void {
|
|
121
|
+
const line = JSON.stringify(msg) + '\n';
|
|
122
|
+
for (const client of clients) {
|
|
123
|
+
try {
|
|
124
|
+
client.write(line);
|
|
125
|
+
} catch {
|
|
126
|
+
// Client disconnected, will be cleaned up
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sendTo(socket: import('bun').Socket, msg: object): void {
|
|
132
|
+
try {
|
|
133
|
+
socket.write(JSON.stringify(msg) + '\n');
|
|
134
|
+
} catch {
|
|
135
|
+
// Client gone
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Defense-in-depth allowlist. The pty-manager (server) validates shellId
|
|
140
|
+
// against its detected-shells list before forwarding to the daemon, but
|
|
141
|
+
// the daemon listens on a Unix socket and should not trust arbitrary
|
|
142
|
+
// shell ids from anyone that can reach it. Only bare names matching this
|
|
143
|
+
// allowlist are resolved via `command -v`; anything else falls back to
|
|
144
|
+
// the system default shell.
|
|
145
|
+
const ALLOWED_SHELL_IDS = new Set([
|
|
146
|
+
'bash',
|
|
147
|
+
'zsh',
|
|
148
|
+
'fish',
|
|
149
|
+
'sh',
|
|
150
|
+
'dash',
|
|
151
|
+
'ksh',
|
|
152
|
+
'tcsh',
|
|
153
|
+
'csh',
|
|
154
|
+
'nushell',
|
|
155
|
+
'nu',
|
|
156
|
+
'elvish',
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
function resolveShell(shellId?: string): string {
|
|
160
|
+
const defaultShell = process.env.SHELL || 'bash';
|
|
161
|
+
if (!shellId || shellId === 'default') return defaultShell;
|
|
162
|
+
if (!ALLOWED_SHELL_IDS.has(shellId)) {
|
|
163
|
+
daemonLog(`[pty-daemon] Rejected unknown shell id '${shellId}' — using default`);
|
|
164
|
+
return defaultShell;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const r = Bun.spawnSync(['sh', '-c', `command -v "${shellId}"`], {
|
|
168
|
+
stdout: 'pipe',
|
|
169
|
+
stderr: 'pipe',
|
|
170
|
+
});
|
|
171
|
+
if (r.exitCode === 0) {
|
|
172
|
+
const path = r.stdout.toString().trim();
|
|
173
|
+
if (path) return path;
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// fall through
|
|
177
|
+
}
|
|
178
|
+
daemonLog(`[pty-daemon] Shell '${shellId}' not on PATH — using default`);
|
|
179
|
+
return defaultShell;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function spawnSession(cmd: SpawnCmd): void {
|
|
183
|
+
const existing = sessions.get(cmd.id);
|
|
184
|
+
if (existing) {
|
|
185
|
+
// Adopt the existing session instead of erroring. This handles clients
|
|
186
|
+
// that send spawn with an ID whose session is still alive in the daemon
|
|
187
|
+
// (e.g. after a runtime restart that lost activeSessions, a strict-mode
|
|
188
|
+
// double-mount, or a reconnect). Resize to the new client's dimensions
|
|
189
|
+
// and replay the serialized terminal state.
|
|
190
|
+
const cols = cmd.cols || existing.cols;
|
|
191
|
+
const rows = cmd.rows || existing.rows;
|
|
192
|
+
if (cols !== existing.cols || rows !== existing.rows) {
|
|
193
|
+
try {
|
|
194
|
+
(existing.proc as any).terminal?.resize(cols, rows);
|
|
195
|
+
existing.headless.resize(cols, rows);
|
|
196
|
+
existing.cols = cols;
|
|
197
|
+
existing.rows = rows;
|
|
198
|
+
} catch (err: any) {
|
|
199
|
+
daemonLog(`[pty-daemon] Adopt resize failed: ${cmd.id}`, err?.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
broadcast({ evt: 'spawned', id: cmd.id });
|
|
204
|
+
try {
|
|
205
|
+
const state = existing.serialize.serialize();
|
|
206
|
+
if (state) broadcast({ evt: 'data', id: cmd.id, data: state });
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
daemonLog(`[pty-daemon] Adopt serialize failed: ${cmd.id}`, err?.message);
|
|
209
|
+
}
|
|
210
|
+
daemonLog(`[pty-daemon] Session adopted on duplicate spawn: ${cmd.id}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const shell = resolveShell(cmd.shell);
|
|
215
|
+
const cols = cmd.cols || 80;
|
|
216
|
+
const rows = cmd.rows || 24;
|
|
217
|
+
const cwd = cmd.cwd || process.cwd();
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const headless = new HeadlessTerminal({
|
|
221
|
+
cols,
|
|
222
|
+
rows,
|
|
223
|
+
scrollback: 5000,
|
|
224
|
+
allowProposedApi: true,
|
|
225
|
+
});
|
|
226
|
+
const serialize = new SerializeAddon();
|
|
227
|
+
headless.loadAddon(serialize);
|
|
228
|
+
|
|
229
|
+
// Build a clean environment for user shell sessions.
|
|
230
|
+
// Remove node_modules/.bin entries from PATH — these leak from the Bun
|
|
231
|
+
// server/daemon process and can shadow user tools (e.g. npx → bunx shim).
|
|
232
|
+
const mergedEnv = { ...process.env, ...(cmd.env || {}) } as Record<string, string>;
|
|
233
|
+
if (mergedEnv.PATH) {
|
|
234
|
+
mergedEnv.PATH = mergedEnv.PATH.split(':')
|
|
235
|
+
.filter((p) => !p.includes('node_modules/.bin'))
|
|
236
|
+
.join(':');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const id = cmd.id;
|
|
240
|
+
const proc = Bun.spawn([shell, '-l'], {
|
|
241
|
+
cwd,
|
|
242
|
+
env: mergedEnv,
|
|
243
|
+
terminal: {
|
|
244
|
+
cols,
|
|
245
|
+
rows,
|
|
246
|
+
data(_terminal, data) {
|
|
247
|
+
const str = data.toString();
|
|
248
|
+
// Track state in headless terminal
|
|
249
|
+
headless.write(str);
|
|
250
|
+
// Broadcast to all connected servers
|
|
251
|
+
broadcast({ evt: 'data', id, data: str });
|
|
252
|
+
},
|
|
253
|
+
exit(_terminal, exitCode) {
|
|
254
|
+
broadcast({ evt: 'exit', id, exitCode });
|
|
255
|
+
const session = sessions.get(id);
|
|
256
|
+
if (session) {
|
|
257
|
+
session.headless.dispose();
|
|
258
|
+
sessions.delete(id);
|
|
259
|
+
}
|
|
260
|
+
resetIdleTimer();
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
sessions.set(id, { id, proc, headless, serialize, cwd, shell, cols, rows });
|
|
266
|
+
broadcast({ evt: 'spawned', id });
|
|
267
|
+
resetIdleTimer();
|
|
268
|
+
|
|
269
|
+
daemonLog(`[pty-daemon] Session spawned: ${id} (shell=${shell}, cwd=${cwd})`);
|
|
270
|
+
} catch (err: any) {
|
|
271
|
+
broadcast({ evt: 'error', id: cmd.id, error: err?.message ?? 'Failed to spawn' });
|
|
272
|
+
daemonLog(`[pty-daemon] Spawn failed: ${cmd.id}`, err?.message);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function writeToSession(id: string, data: string): void {
|
|
277
|
+
const session = sessions.get(id);
|
|
278
|
+
if (session) {
|
|
279
|
+
try {
|
|
280
|
+
(session.proc as any).terminal?.write(data);
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
daemonLog(`[pty-daemon] Write failed: ${id}`, err?.message);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resizeSession(id: string, cols: number, rows: number): void {
|
|
288
|
+
const session = sessions.get(id);
|
|
289
|
+
if (session) {
|
|
290
|
+
try {
|
|
291
|
+
(session.proc as any).terminal?.resize(cols, rows);
|
|
292
|
+
session.headless.resize(cols, rows);
|
|
293
|
+
session.cols = cols;
|
|
294
|
+
session.rows = rows;
|
|
295
|
+
} catch (err: any) {
|
|
296
|
+
daemonLog(`[pty-daemon] Resize failed: ${id}`, err?.message);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function signalSession(id: string, sig: number): void {
|
|
302
|
+
const session = sessions.get(id);
|
|
303
|
+
if (session) {
|
|
304
|
+
try {
|
|
305
|
+
session.proc.kill(sig);
|
|
306
|
+
daemonLog(`[pty-daemon] Signal ${sig} sent to session: ${id}`);
|
|
307
|
+
} catch (err: any) {
|
|
308
|
+
daemonLog(`[pty-daemon] Signal failed: ${id}`, err?.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function killSession(id: string): void {
|
|
314
|
+
const session = sessions.get(id);
|
|
315
|
+
if (session) {
|
|
316
|
+
sessions.delete(id);
|
|
317
|
+
try {
|
|
318
|
+
session.headless.dispose();
|
|
319
|
+
session.proc.kill();
|
|
320
|
+
} catch {
|
|
321
|
+
// Process may already be gone
|
|
322
|
+
}
|
|
323
|
+
daemonLog(`[pty-daemon] Session killed: ${id}`);
|
|
324
|
+
resetIdleTimer();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function listSessions(): Array<{
|
|
329
|
+
id: string;
|
|
330
|
+
cwd: string;
|
|
331
|
+
shell: string;
|
|
332
|
+
cols: number;
|
|
333
|
+
rows: number;
|
|
334
|
+
}> {
|
|
335
|
+
return Array.from(sessions.values()).map((s) => ({
|
|
336
|
+
id: s.id,
|
|
337
|
+
cwd: s.cwd,
|
|
338
|
+
shell: s.shell,
|
|
339
|
+
cols: s.cols,
|
|
340
|
+
rows: s.rows,
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function captureSession(id: string): string | null {
|
|
345
|
+
const session = sessions.get(id);
|
|
346
|
+
if (!session) return null;
|
|
347
|
+
try {
|
|
348
|
+
return session.serialize.serialize();
|
|
349
|
+
} catch (err: any) {
|
|
350
|
+
daemonLog(`[pty-daemon] Capture failed: ${id}`, err?.message);
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Idle Timer ───────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
function resetIdleTimer(): void {
|
|
358
|
+
if (idleTimer) {
|
|
359
|
+
clearTimeout(idleTimer);
|
|
360
|
+
idleTimer = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (sessions.size === 0 && clients.size === 0) {
|
|
364
|
+
idleTimer = setTimeout(() => {
|
|
365
|
+
daemonLog('[pty-daemon] Idle timeout — shutting down');
|
|
366
|
+
gracefulShutdown();
|
|
367
|
+
}, IDLE_TIMEOUT_MS);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Command Handler ──────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
function handleCommand(socket: import('bun').Socket, msg: DaemonCommand): void {
|
|
374
|
+
switch (msg.cmd) {
|
|
375
|
+
case 'spawn':
|
|
376
|
+
spawnSession(msg);
|
|
377
|
+
break;
|
|
378
|
+
case 'write':
|
|
379
|
+
writeToSession(msg.id, msg.data);
|
|
380
|
+
break;
|
|
381
|
+
case 'resize':
|
|
382
|
+
resizeSession(msg.id, msg.cols, msg.rows);
|
|
383
|
+
break;
|
|
384
|
+
case 'signal':
|
|
385
|
+
signalSession(msg.id, msg.signal);
|
|
386
|
+
break;
|
|
387
|
+
case 'kill':
|
|
388
|
+
killSession(msg.id);
|
|
389
|
+
break;
|
|
390
|
+
case 'list':
|
|
391
|
+
sendTo(socket, { evt: 'sessions', sessions: listSessions() });
|
|
392
|
+
break;
|
|
393
|
+
case 'capture':
|
|
394
|
+
sendTo(socket, { evt: 'captured', id: msg.id, state: captureSession(msg.id) });
|
|
395
|
+
break;
|
|
396
|
+
case 'ping':
|
|
397
|
+
sendTo(socket, { evt: 'pong' });
|
|
398
|
+
break;
|
|
399
|
+
case 'shutdown':
|
|
400
|
+
daemonLog('[pty-daemon] Shutdown requested');
|
|
401
|
+
gracefulShutdown();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Socket Server ────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
// Remove stale socket file
|
|
409
|
+
if (existsSync(SOCKET_PATH)) {
|
|
410
|
+
try {
|
|
411
|
+
unlinkSync(SOCKET_PATH);
|
|
412
|
+
} catch {
|
|
413
|
+
// May fail if another daemon is running — will error on listen
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Per-client line buffer for NDJSON parsing
|
|
418
|
+
const lineBuffers = new WeakMap<import('bun').Socket, string>();
|
|
419
|
+
|
|
420
|
+
const server = Bun.listen({
|
|
421
|
+
unix: SOCKET_PATH,
|
|
422
|
+
socket: {
|
|
423
|
+
open(socket) {
|
|
424
|
+
clients.add(socket);
|
|
425
|
+
lineBuffers.set(socket, '');
|
|
426
|
+
resetIdleTimer();
|
|
427
|
+
daemonLog(`[pty-daemon] Client connected (total: ${clients.size})`);
|
|
428
|
+
},
|
|
429
|
+
|
|
430
|
+
data(socket, data) {
|
|
431
|
+
let buffer = (lineBuffers.get(socket) ?? '') + data.toString();
|
|
432
|
+
|
|
433
|
+
// Process complete NDJSON lines
|
|
434
|
+
let newlineIdx: number;
|
|
435
|
+
while ((newlineIdx = buffer.indexOf('\n')) !== -1) {
|
|
436
|
+
const line = buffer.slice(0, newlineIdx).trim();
|
|
437
|
+
buffer = buffer.slice(newlineIdx + 1);
|
|
438
|
+
|
|
439
|
+
if (line.length === 0) continue;
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const msg = JSON.parse(line) as DaemonCommand;
|
|
443
|
+
handleCommand(socket, msg);
|
|
444
|
+
} catch {
|
|
445
|
+
daemonLog(`[pty-daemon] Invalid message: ${line.slice(0, 200)}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
lineBuffers.set(socket, buffer);
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
close(socket) {
|
|
453
|
+
clients.delete(socket);
|
|
454
|
+
lineBuffers.delete(socket);
|
|
455
|
+
resetIdleTimer();
|
|
456
|
+
daemonLog(`[pty-daemon] Client disconnected (total: ${clients.size})`);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
error(_socket, error) {
|
|
460
|
+
daemonLog('[pty-daemon] Socket error:', error.message);
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// ── PID File ─────────────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
468
|
+
|
|
469
|
+
// ── Graceful Shutdown ────────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
function gracefulShutdown(): void {
|
|
472
|
+
daemonLog('[pty-daemon] Shutting down...');
|
|
473
|
+
|
|
474
|
+
// Kill all PTY sessions
|
|
475
|
+
for (const [, session] of sessions) {
|
|
476
|
+
try {
|
|
477
|
+
session.headless.dispose();
|
|
478
|
+
session.proc.kill();
|
|
479
|
+
} catch {}
|
|
480
|
+
}
|
|
481
|
+
sessions.clear();
|
|
482
|
+
|
|
483
|
+
// Close socket server
|
|
484
|
+
try {
|
|
485
|
+
server.stop();
|
|
486
|
+
} catch {}
|
|
487
|
+
|
|
488
|
+
// Clean up files
|
|
489
|
+
try {
|
|
490
|
+
unlinkSync(SOCKET_PATH);
|
|
491
|
+
} catch {}
|
|
492
|
+
try {
|
|
493
|
+
unlinkSync(PID_FILE);
|
|
494
|
+
} catch {}
|
|
495
|
+
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
500
|
+
process.on('SIGINT', gracefulShutdown);
|
|
501
|
+
|
|
502
|
+
// ── Startup ──────────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
daemonLog(`[pty-daemon] Started (pid=${process.pid})`);
|
|
505
|
+
daemonLog(`[pty-daemon] Socket: ${SOCKET_PATH}`);
|
|
506
|
+
daemonLog(`[pty-daemon] PID file: ${PID_FILE}`);
|
|
507
|
+
|
|
508
|
+
resetIdleTimer();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e}from"./rolldown-runtime-Cyuzqnbw.js";import{zr as t}from"./icons-sqwAEN28.js";import{gt as n}from"./index-DNxUSLe_.js";import{M as r}from"./items-7qqySF-X.js";var i=e(t(),1);function a(){let{pathname:e}=n();return(0,i.useMemo)(()=>r(e).threadId??null,[e])}export{a as t};
|