@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.
Files changed (88) hide show
  1. package/README.md +5 -6
  2. package/bin/funny.js +88 -26
  3. package/package.json +3 -1
  4. package/packages/client/dist/assets/{AcceptInvitePage-BjMGmkD4.js → AcceptInvitePage-D13hs9Zr.js} +1 -1
  5. package/packages/client/dist/assets/{ActivityPane-Cl-bUP3W.js → ActivityPane-CtxWrqSm.js} +1 -1
  6. package/packages/client/dist/assets/{AddProjectView-DZuks8BQ.js → AddProjectView-BrSbq9Zq.js} +1 -1
  7. package/packages/client/dist/assets/{AllThreadsView-DdT3QdFb.js → AllThreadsView-DVZkf_3a.js} +1 -1
  8. package/packages/client/dist/assets/{AnalyticsView-b_BZhrh1.js → AnalyticsView-DudiSHyr.js} +1 -1
  9. package/packages/client/dist/assets/{App-CY9kuarV.js → App-BkudlgY9.js} +3 -3
  10. package/packages/client/dist/assets/{AutomationInboxView-BQfGidRk.js → AutomationInboxView-B20N9Rzu.js} +1 -1
  11. package/packages/client/dist/assets/{BrowserPanel-CY_wB1AB.js → BrowserPanel-Dz4qr3zh.js} +1 -1
  12. package/packages/client/dist/assets/{BrowserPreview-2gdJqMpN.js → BrowserPreview-CKJfq_nE.js} +1 -1
  13. package/packages/client/dist/assets/{CircuitBreakerDialog-B6kP1aCS.js → CircuitBreakerDialog-CIYWhLBH.js} +1 -1
  14. package/packages/client/dist/assets/{CommandPalette-DeBQCdqo.js → CommandPalette-BWdGw1dT.js} +1 -1
  15. package/packages/client/dist/assets/{CommentsPane-JY0CN5R2.js → CommentsPane-Cq3KSPeq.js} +1 -1
  16. package/packages/client/dist/assets/{CsvTable-CaJ4BQ4d.js → CsvTable-Y1HjLh1s.js} +1 -1
  17. package/packages/client/dist/assets/{FileSearchDialog-C2RhpM7Z.js → FileSearchDialog-3mjmh0eF.js} +1 -1
  18. package/packages/client/dist/assets/{FileTree-Bl25zGAE.js → FileTree-mnY0OIfR.js} +1 -1
  19. package/packages/client/dist/assets/{FolderPicker-CpXi_ZrF.js → FolderPicker-DlzSNNoW.js} +1 -1
  20. package/packages/client/dist/assets/{GeneralSettingsView-f8pBncJY.js → GeneralSettingsView-CxS_XI8Z.js} +2 -2
  21. package/packages/client/dist/assets/{GitProgressModal-CJG5MsSI.js → GitProgressModal-D-h3ocu1.js} +1 -1
  22. package/packages/client/dist/assets/{LiveColumnsView-DwUkXA8m.js → LiveColumnsView-BGRek-gh.js} +1 -1
  23. package/packages/client/dist/assets/{LoginPage-CkgRw9Ji.js → LoginPage-DATO3dWs.js} +1 -1
  24. package/packages/client/dist/assets/{MediaPreview-DmMe4hGN.js → MediaPreview-Dnzd03yo.js} +1 -1
  25. package/packages/client/dist/assets/{MediaPreviewDialog-DHIv3gai.js → MediaPreviewDialog-Dm1nhBR9.js} +1 -1
  26. package/packages/client/dist/assets/{MermaidBlock-BZLj2TmL.js → MermaidBlock-DeQ02Wb_.js} +1 -1
  27. package/packages/client/dist/assets/{MobilePage-BRWC2SVo.js → MobilePage-B_-4EwLh.js} +2 -2
  28. package/packages/client/dist/assets/{MonacoEditorDialog-CTUQ5NFr.js → MonacoEditorDialog-DtBfzOtQ.js} +1 -1
  29. package/packages/client/dist/assets/{OrchestratorView-wTF6Ae0A.js → OrchestratorView-CIthX7Z9.js} +1 -1
  30. package/packages/client/dist/assets/{PreferencesPanel-CpIL485D.js → PreferencesPanel-UKXVNtTZ.js} +1 -1
  31. package/packages/client/dist/assets/{PreviewBrowser-CEXO1tHq.js → PreviewBrowser-Dm_Aqtps.js} +1 -1
  32. package/packages/client/dist/assets/{ProjectFilesPane-02_p9zry.js → ProjectFilesPane-B59Vq3_s.js} +1 -1
  33. package/packages/client/dist/assets/{ProjectHeader-TWkvwAxm.js → ProjectHeader-Qf_M_sBm.js} +1 -1
  34. package/packages/client/dist/assets/{ProjectHooksSettings-CIaY32WM.js → ProjectHooksSettings-D23PXt7X.js} +1 -1
  35. package/packages/client/dist/assets/{ReviewPane-DHTvq8t1.js → ReviewPane-DEY-F3Zq.js} +1 -1
  36. package/packages/client/dist/assets/{SearchablePicker-BJnuJVEi.js → SearchablePicker-BQEWhSu4.js} +1 -1
  37. package/packages/client/dist/assets/{SettingsDetailView-eBDvn5cS.js → SettingsDetailView-CyaMNfi4.js} +1 -1
  38. package/packages/client/dist/assets/{SettingsPageContent-CiHRUvG3.js → SettingsPageContent-BMPAc4uQ.js} +3 -3
  39. package/packages/client/dist/assets/{SetupWizard-B8rFRPma.js → SetupWizard-8eO2SYvf.js} +1 -1
  40. package/packages/client/dist/assets/{Sidebar-CPU0yoaQ.js → Sidebar-CDKV9zDp.js} +1 -1
  41. package/packages/client/dist/assets/{StashTab-BswTfOlb.js → StashTab-KG5Zzy5-.js} +1 -1
  42. package/packages/client/dist/assets/{TestRunnerPane-DZXDbHCu.js → TestRunnerPane-C_gSYI4_.js} +1 -1
  43. package/packages/client/dist/assets/{TextSearchDialog-BUMBp448.js → TextSearchDialog-CX5B9hyW.js} +1 -1
  44. package/packages/client/dist/assets/{ThreadPowerline-CSegubLW.js → ThreadPowerline-DGdjong-.js} +1 -1
  45. package/packages/client/dist/assets/{ThreadStatusPin-DhrEC0n-.js → ThreadStatusPin-Bmauv5AI.js} +1 -1
  46. package/packages/client/dist/assets/{ThreadView-hYXeM23q.js → ThreadView-BvizmCnl.js} +1 -1
  47. package/packages/client/dist/assets/{app-store-DKW9sVHv.js → app-store-CSYr64U4.js} +1 -1
  48. package/packages/client/dist/assets/{automation-store-C8v9UlHX.js → automation-store-er1E9xPe.js} +1 -1
  49. package/packages/client/dist/assets/{browser-panel-store-CSx5jc7O.js → browser-panel-store-BriXQezL.js} +2 -2
  50. package/packages/client/dist/assets/{button-B9Ypx1uQ.js → button-Bd970BE2.js} +1 -1
  51. package/packages/client/dist/assets/{combine-CIrCcf3o.js → combine-Um6l0bxC.js} +1 -1
  52. package/packages/client/dist/assets/{comment-store-BfRTY3FY.js → comment-store-B4-PYjwU.js} +1 -1
  53. package/packages/client/dist/assets/{diff-parse-DgLol0S6.js → diff-parse-dnfUXHzR.js} +1 -1
  54. package/packages/client/dist/assets/{element-yfBHMYal.js → element-DW-xqM5t.js} +1 -1
  55. package/packages/client/dist/assets/{go-to-thread-DD0G5vFp.js → go-to-thread-F_voqQgr.js} +1 -1
  56. package/packages/client/dist/assets/{index-DNxUSLe_.js → index-ea63jMuV.js} +2 -2
  57. package/packages/client/dist/assets/{input-CJ3TsT2L.js → input-CcrOgEwd.js} +1 -1
  58. package/packages/client/dist/assets/{items-7qqySF-X.js → items-iRGH_xTR.js} +1 -1
  59. package/packages/client/dist/assets/{job-store-0PeOgBpX.js → job-store-CaOITASA.js} +1 -1
  60. package/packages/client/dist/assets/{kbd-d4Wzagpn.js → kbd-HqsH-rnU.js} +1 -1
  61. package/packages/client/dist/assets/{loading-state-yLj5fYb6.js → loading-state-BSdX_QZB.js} +1 -1
  62. package/packages/client/dist/assets/{markdown-components-DxMKBjnh.js → markdown-components-DXE4xITV.js} +1 -1
  63. package/packages/client/dist/assets/{native-git-store-CCenGgLt.js → native-git-store-B4G1m25Q.js} +1 -1
  64. package/packages/client/dist/assets/{orchestrator-store-DP8UohlO.js → orchestrator-store-CRIf-E8M.js} +1 -1
  65. package/packages/client/dist/assets/{pipeline-approval-store-BiH1mYlB.js → pipeline-approval-store-Sm4xCBYI.js} +1 -1
  66. package/packages/client/dist/assets/{pr-detail-store-DHF0exs_.js → pr-detail-store-DIz8x882.js} +1 -1
  67. package/packages/client/dist/assets/{providers-Dd9yhyUY.js → providers-B6JAFGOv.js} +1 -1
  68. package/packages/client/dist/assets/{sidebar-Dzb_F1kj.js → sidebar-D5gUIY0G.js} +1 -1
  69. package/packages/client/dist/assets/{tabs-Cu3cWMHb.js → tabs-Bx4u8bcz.js} +1 -1
  70. package/packages/client/dist/assets/{test-store-D72ZqWH2.js → test-store-D4S9R398.js} +1 -1
  71. package/packages/client/dist/assets/{tooltip-icon-button-8C1wJ-l9.js → tooltip-icon-button-C1xTZR5-.js} +1 -1
  72. package/packages/client/dist/assets/use-active-thread-id-1WUch4uf.js +1 -0
  73. package/packages/client/dist/assets/{use-branch-switch-C45m2mx7.js → use-branch-switch-CFzzWMNs.js} +1 -1
  74. package/packages/client/dist/assets/{use-image-lightbox-CsMDwY1v.js → use-image-lightbox-DgkrBc3N.js} +1 -1
  75. package/packages/client/dist/assets/{use-stable-navigate-Cz_jWBHP.js → use-stable-navigate-U6VQ7K0f.js} +1 -1
  76. package/packages/client/dist/assets/{use-terminal-scope-BX8zAMzc.js → use-terminal-scope-BvFrkm0r.js} +1 -1
  77. package/packages/client/dist/assets/{use-thread-creation-DSLV7F8I.js → use-thread-creation-CdjjkhNZ.js} +1 -1
  78. package/packages/client/dist/assets/{use-thread-search-CXsiNqRR.js → use-thread-search-CXgC1XEY.js} +1 -1
  79. package/packages/client/dist/assets/{use-todo-panel-DjPuehjq.js → use-todo-panel-BAbS_SpL.js} +1 -1
  80. package/packages/client/dist/assets/{use-unified-prompt-model-groups-wdA26Vnd.js → use-unified-prompt-model-groups-DH94Yhir.js} +1 -1
  81. package/packages/client/dist/assets/{use-ws-AWIhcFYo.js → use-ws-DfaWLbd3.js} +3 -3
  82. package/packages/client/dist/assets/{visualizer-loader-oxC04P38.js → visualizer-loader-BnEPAAGw.js} +1 -1
  83. package/packages/client/dist/assets/{watcher-store-gym76Ag_.js → watcher-store-DKHnSlTd.js} +1 -1
  84. package/packages/client/dist/index.html +1 -1
  85. package/packages/runtime/dist/index.js +2 -2
  86. package/packages/runtime/dist/index.js.map +1 -1
  87. package/packages/runtime/dist/pty-daemon.ts +508 -0
  88. 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};