tmux-weblink 0.3.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 (75) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/addon-fit-D5N335P2.js +16 -0
  3. package/dist/assets/terminal-client.js +4 -0
  4. package/dist/assets/xterm-NI46XXQY.js +51 -0
  5. package/dist/assets/xterm.css +285 -0
  6. package/dist/browser/terminal-client.js +4 -0
  7. package/dist/frontend.js +1 -0
  8. package/dist/index.js +12 -0
  9. package/dist/lib/agent-detect.js +3 -0
  10. package/dist/lib/agents-watch.js +2 -0
  11. package/dist/lib/atomicWrite.js +1 -0
  12. package/dist/lib/auditLog.js +1 -0
  13. package/dist/lib/auth.js +2 -0
  14. package/dist/lib/cli.js +32 -0
  15. package/dist/lib/commandbar.js +555 -0
  16. package/dist/lib/db.js +1 -0
  17. package/dist/lib/drawer-resize.js +166 -0
  18. package/dist/lib/drawer-script.js +8 -0
  19. package/dist/lib/ext-loader.js +1 -0
  20. package/dist/lib/html.js +1 -0
  21. package/dist/lib/icons.js +1 -0
  22. package/dist/lib/image-upload.js +1 -0
  23. package/dist/lib/load-env.js +6 -0
  24. package/dist/lib/mobile-toolbar.js +313 -0
  25. package/dist/lib/notes-db.js +18 -0
  26. package/dist/lib/notes-drawer.js +210 -0
  27. package/dist/lib/notes-utils.js +15 -0
  28. package/dist/lib/pages/agents-index.js +105 -0
  29. package/dist/lib/pages/history-index.js +85 -0
  30. package/dist/lib/pages/landing.js +78 -0
  31. package/dist/lib/pages/login.js +191 -0
  32. package/dist/lib/pages/notes-index.js +65 -0
  33. package/dist/lib/pages/notes-page.js +158 -0
  34. package/dist/lib/pages/quick-commands.js +361 -0
  35. package/dist/lib/pages/schedule-index.js +392 -0
  36. package/dist/lib/pages/settings.js +206 -0
  37. package/dist/lib/pages/terminal.js +364 -0
  38. package/dist/lib/pinned-views.js +1 -0
  39. package/dist/lib/plugins.js +3 -0
  40. package/dist/lib/quick-commands.js +1 -0
  41. package/dist/lib/rateLimiter.js +2 -0
  42. package/dist/lib/schedule-delay.js +9 -0
  43. package/dist/lib/scheduler-drawer.js +379 -0
  44. package/dist/lib/scheduler.js +1 -0
  45. package/dist/lib/security-config.js +2 -0
  46. package/dist/lib/session-access.js +1 -0
  47. package/dist/lib/session-windows.js +1 -0
  48. package/dist/lib/sessions-drawer.js +634 -0
  49. package/dist/lib/sessions-sidebar.js +1 -0
  50. package/dist/lib/settings.js +2 -0
  51. package/dist/lib/setup-features.js +1 -0
  52. package/dist/lib/setup-prompts.js +3 -0
  53. package/dist/lib/shared-layout.js +412 -0
  54. package/dist/lib/state-paths.js +1 -0
  55. package/dist/lib/terminal-config.js +1 -0
  56. package/dist/lib/theme-store.js +2 -0
  57. package/dist/lib/theme.js +28 -0
  58. package/dist/lib/themes/dark-cove.js +1 -0
  59. package/dist/lib/themes/ghostty.js +1 -0
  60. package/dist/lib/themes/index.js +1 -0
  61. package/dist/lib/themes/types.js +0 -0
  62. package/dist/lib/themes/vscode.js +1 -0
  63. package/dist/lib/themes/warm-clay.js +1 -0
  64. package/dist/lib/tmux-capture.js +5 -0
  65. package/dist/lib/tmux-control.js +2 -0
  66. package/dist/lib/tmux-panes.js +2 -0
  67. package/dist/lib/tmux-windows.js +4 -0
  68. package/dist/lib/watched-panes.js +1 -0
  69. package/dist/lib/window-history.js +1 -0
  70. package/dist/lib/window-labels.js +1 -0
  71. package/dist/lib/windows-drawer.js +364 -0
  72. package/dist/lib/ws-message.js +1 -0
  73. package/dist/sessions.js +2 -0
  74. package/package.json +62 -0
  75. package/scripts/fix-pty-perms.mjs +16 -0
@@ -0,0 +1,364 @@
1
+ import{closeOtherDrawersExcept as n,wrapDrawerScript as t}from"./drawer-script.js";import{drawerResizeCSS as o,drawerResizeHandleHTML as r,drawerResizeScript as i}from"./drawer-resize.js";import{escapeHtml as s}from"./html.js";function c(){return`
2
+ #windows-backdrop {
3
+ position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 999;
4
+ opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
5
+ }
6
+ #windows-backdrop.open { opacity: 1; pointer-events: auto; }
7
+ #windows-drawer {
8
+ position: fixed; right: 0; top: 0; height: 100%; width: 360px; z-index: 1000;
9
+ background: var(--panel-bg); border-left: 1px solid var(--panel-border);
10
+ display: flex; flex-direction: column;
11
+ transform: translateX(100%); transition: transform 0.25s ease;
12
+ }
13
+ #windows-drawer.open { transform: translateX(0); }
14
+ ${o()}
15
+ #windows-error {
16
+ padding: 8px 16px; font-size: var(--text-xs); color: #cc6666;
17
+ font-family: var(--font-mono); flex-shrink: 0;
18
+ border-bottom: 1px solid var(--panel-border);
19
+ display: none;
20
+ }
21
+ #windows-error.show { display: block; }
22
+ #windows-list {
23
+ flex: 1; overflow-y: auto; padding: 8px 0;
24
+ scrollbar-width: thin;
25
+ scrollbar-color: var(--panel-border) transparent;
26
+ }
27
+ #windows-list::-webkit-scrollbar { width: 4px; }
28
+ #windows-list::-webkit-scrollbar-track { background: transparent; }
29
+ #windows-list::-webkit-scrollbar-thumb {
30
+ background: var(--panel-border); border-radius: 2px;
31
+ }
32
+ #windows-list::-webkit-scrollbar-thumb:hover { background: var(--panel-muted); }
33
+ .windows-row {
34
+ display: flex; align-items: center; gap: 10px;
35
+ width: 100%; min-height: 48px; padding: 12px 16px;
36
+ background: none; border: none; border-bottom: 1px solid var(--panel-border);
37
+ color: var(--page-fg); cursor: pointer; text-align: left;
38
+ font-family: var(--font-mono); transition: background 0.15s;
39
+ }
40
+ .windows-row:last-child { border-bottom: none; }
41
+ .windows-row:not(.is-active):hover { background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
42
+ .windows-row.is-active { cursor: default; opacity: 0.85; }
43
+ .windows-row-index {
44
+ font-size: var(--text-xs); color: var(--panel-muted); flex-shrink: 0; min-width: 24px;
45
+ }
46
+ .windows-row-name {
47
+ flex: 1; min-width: 0; font-size: var(--text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
48
+ }
49
+ .windows-row-edit {
50
+ flex-shrink: 0; display: flex; align-items: center; justify-content: center;
51
+ background: none; border: none; min-width: 44px; min-height: 44px; padding: 4px; border-radius: 6px;
52
+ color: var(--panel-muted); cursor: pointer; opacity: 0;
53
+ transition: opacity 0.15s, color 0.15s, background 0.15s;
54
+ }
55
+ .windows-row:hover .windows-row-edit, .windows-row-edit:focus-visible { opacity: 1; }
56
+ .windows-row-edit:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
57
+ .windows-row-edit:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
58
+ .windows-row-edit svg { width: 16px; height: 16px; fill: currentColor; display: block; }
59
+ .windows-row-input {
60
+ flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm);
61
+ background: var(--page-bg); color: var(--page-fg);
62
+ border: 1px solid var(--panel-accent); border-radius: 4px;
63
+ padding: 4px 8px; outline: none;
64
+ }
65
+ .windows-row-badge {
66
+ font-size: var(--text-xs); color: var(--panel-success); text-transform: uppercase;
67
+ letter-spacing: 0.08em; flex-shrink: 0;
68
+ }
69
+ .windows-empty {
70
+ padding: 24px 16px; text-align: center; color: var(--panel-muted);
71
+ font-size: var(--text-xs); font-family: var(--font-mono);
72
+ }
73
+ #windows-new-btn {
74
+ display: flex; align-items: center; justify-content: center; gap: 4px;
75
+ background: none; border: none; color: var(--panel-muted); cursor: pointer;
76
+ min-width: 44px; min-height: 44px; padding: 8px; border-radius: 8px; font-size: var(--text-lg); line-height: 1;
77
+ transition: color 0.15s, background 0.15s;
78
+ }
79
+ #windows-new-btn:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
80
+ #windows-new-btn:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
81
+ #windows-drawer .drawer-header button {
82
+ background: none; border: none; color: var(--panel-muted); cursor: pointer;
83
+ font-size: var(--text-lg); line-height: 1; min-width: 44px; min-height: 44px; padding: 8px; border-radius: 8px; transition: color 0.15s, background 0.15s;
84
+ }
85
+ #windows-drawer .drawer-header button:hover { color: var(--panel-accent); background: color-mix(in srgb, var(--panel-accent) 8%, transparent); }
86
+ #windows-drawer .drawer-header button:focus-visible { box-shadow: 0 0 0 2px var(--panel-accent); outline: none; }
87
+ @media (max-width: 560px) {
88
+ #windows-drawer { width: min(100vw - 16px, 400px); }
89
+ .windows-row { min-height: 52px; padding: 14px 16px; }
90
+ .windows-row-name { font-size: var(--text-base); }
91
+ .windows-row-edit { opacity: 0.6; }
92
+ }`}function l(e){return`
93
+ <div id="windows-backdrop"></div>
94
+ <div id="windows-drawer" class="resizable-drawer">
95
+ ${r()}
96
+ <div class="drawer-header">
97
+ <span>${s(e)}</span>
98
+ <button id="windows-new-btn" title="New window">+</button>
99
+ <button id="windows-close">&times;</button>
100
+ </div>
101
+ <div id="windows-error"></div>
102
+ <div id="windows-list"></div>
103
+ </div>`}function p(e){return t("windows",`
104
+ ${i("windows-drawer","tmux-web:drawer-width:windows",360)}
105
+ const WIN_SESSION = ${JSON.stringify(e)};
106
+ const windowsDrawer = document.getElementById('windows-drawer');
107
+ const windowsBackdrop = document.getElementById('windows-backdrop');
108
+ const windowsList = document.getElementById('windows-list');
109
+ const windowsError = document.getElementById('windows-error');
110
+ let windowsRefreshInterval = null;
111
+ let editingWindow = false;
112
+
113
+ const PENCIL_PATH = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z';
114
+
115
+ function makeEditIcon() {
116
+ const btn = document.createElement('button');
117
+ btn.type = 'button';
118
+ btn.className = 'windows-row-edit';
119
+ btn.title = 'Rename window';
120
+ btn.setAttribute('aria-label', 'Rename window');
121
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
122
+ svg.setAttribute('viewBox', '0 0 24 24');
123
+ svg.setAttribute('aria-hidden', 'true');
124
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
125
+ path.setAttribute('d', PENCIL_PATH);
126
+ svg.appendChild(path);
127
+ btn.appendChild(svg);
128
+ return btn;
129
+ }
130
+
131
+ function showWindowsError(msg) {
132
+ windowsError.textContent = msg;
133
+ windowsError.classList.add('show');
134
+ }
135
+
136
+ function clearWindowsError() {
137
+ windowsError.textContent = '';
138
+ windowsError.classList.remove('show');
139
+ }
140
+
141
+ function clearWinList(el) {
142
+ while (el.firstChild) el.removeChild(el.firstChild);
143
+ }
144
+
145
+ async function fetchWinList() {
146
+ try {
147
+ const res = await fetch('/api/session/' + encodeURIComponent(WIN_SESSION) + '/windows');
148
+ if (!res.ok) return [];
149
+ return await res.json();
150
+ } catch {
151
+ return [];
152
+ }
153
+ }
154
+
155
+ function renderWindowsList(windows) {
156
+ clearWinList(windowsList);
157
+ if (!windows.length) {
158
+ windowsList.appendChild(Object.assign(document.createElement('div'), {
159
+ className: 'windows-empty',
160
+ textContent: 'No windows in this session',
161
+ }));
162
+ return;
163
+ }
164
+ for (const w of windows) {
165
+ const row = document.createElement('div');
166
+ row.className = 'windows-row' + (w.active ? ' is-active' : '');
167
+
168
+ const idx = document.createElement('span');
169
+ idx.className = 'windows-row-index';
170
+ idx.textContent = String(w.index);
171
+
172
+ const name = document.createElement('span');
173
+ name.className = 'windows-row-name';
174
+ name.textContent = w.label || w.name;
175
+
176
+ row.appendChild(idx);
177
+ row.appendChild(name);
178
+
179
+ const edit = makeEditIcon();
180
+ edit.addEventListener('click', (e) => {
181
+ e.stopPropagation();
182
+ startWindowRename(w, name);
183
+ });
184
+ row.appendChild(edit);
185
+
186
+ if (w.active) {
187
+ const badge = document.createElement('span');
188
+ badge.className = 'windows-row-badge';
189
+ badge.textContent = 'current';
190
+ row.appendChild(badge);
191
+ } else {
192
+ row.setAttribute('role', 'button');
193
+ row.tabIndex = 0;
194
+ row.addEventListener('click', () => selectWindow(w.index));
195
+ row.addEventListener('keydown', (e) => {
196
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectWindow(w.index); }
197
+ });
198
+ }
199
+
200
+ windowsList.appendChild(row);
201
+ }
202
+ }
203
+
204
+ function startWindowRename(win, nameEl) {
205
+ const row = nameEl.parentNode;
206
+ if (!row || row.querySelector('.windows-row-input')) return;
207
+ editingWindow = true;
208
+
209
+ const input = document.createElement('input');
210
+ input.type = 'text';
211
+ input.className = 'windows-row-input';
212
+ input.value = win.name || '';
213
+ input.placeholder = win.name || 'window name';
214
+
215
+ let done = false;
216
+ const finish = async (save) => {
217
+ if (done) return;
218
+ done = true;
219
+ editingWindow = false;
220
+ const newName = input.value.trim();
221
+ if (save && newName && newName !== win.name) {
222
+ try {
223
+ const res = await fetch(
224
+ '/api/session/' + encodeURIComponent(WIN_SESSION) + '/rename-window',
225
+ {
226
+ method: 'POST',
227
+ headers: { 'Content-Type': 'application/json' },
228
+ body: JSON.stringify({ windowIndex: win.index, name: newName }),
229
+ },
230
+ );
231
+ if (res.ok) {
232
+ win.name = newName;
233
+ clearWindowsError();
234
+ } else {
235
+ const data = await res.json().catch(() => ({}));
236
+ showWindowsError(data.error || 'Failed to rename window');
237
+ }
238
+ } catch {
239
+ showWindowsError('Failed to rename window');
240
+ }
241
+ }
242
+ nameEl.textContent = win.label || win.name;
243
+ input.replaceWith(nameEl);
244
+ };
245
+
246
+ // Keep clicks/keys local so the row's switch handler and the drawer's global
247
+ // Escape-to-close don't fire while editing.
248
+ input.addEventListener('click', (e) => e.stopPropagation());
249
+ input.addEventListener('keydown', (e) => {
250
+ e.stopPropagation();
251
+ if (e.key === 'Enter') { e.preventDefault(); void finish(true); }
252
+ else if (e.key === 'Escape') { e.preventDefault(); void finish(false); }
253
+ });
254
+ input.addEventListener('blur', () => { void finish(true); });
255
+
256
+ nameEl.replaceWith(input);
257
+ input.focus();
258
+ input.select();
259
+ }
260
+
261
+ async function selectWindow(windowIndex) {
262
+ clearWindowsError();
263
+ try {
264
+ const res = await fetch(
265
+ '/api/session/' + encodeURIComponent(WIN_SESSION) + '/select-window',
266
+ {
267
+ method: 'POST',
268
+ headers: { 'Content-Type': 'application/json' },
269
+ body: JSON.stringify({ windowIndex }),
270
+ },
271
+ );
272
+ if (!res.ok) {
273
+ const data = await res.json().catch(() => ({}));
274
+ showWindowsError(data.error || 'Failed to switch window');
275
+ return;
276
+ }
277
+ closeWindowsDrawer();
278
+ } catch {
279
+ showWindowsError('Failed to switch window');
280
+ }
281
+ }
282
+
283
+ function startWindowsRefresh() {
284
+ stopWindowsRefresh();
285
+ windowsRefreshInterval = setInterval(async () => {
286
+ if (!windowsDrawer.classList.contains('open') || editingWindow) return;
287
+ renderWindowsList(await fetchWinList());
288
+ }, 2500);
289
+ }
290
+
291
+ function stopWindowsRefresh() {
292
+ if (windowsRefreshInterval) {
293
+ clearInterval(windowsRefreshInterval);
294
+ windowsRefreshInterval = null;
295
+ }
296
+ }
297
+
298
+ async function openWindowsDrawer() {
299
+ ${n("windows")}
300
+ clearWindowsError();
301
+ windowsDrawer.classList.add('open');
302
+ windowsBackdrop.classList.add('open');
303
+ renderWindowsList(await fetchWinList());
304
+ startWindowsRefresh();
305
+ const url = new URL(location.href);
306
+ if (url.searchParams.get('tab') !== 'windows') {
307
+ url.searchParams.set('tab', 'windows');
308
+ history.pushState({ windowsOpen: true }, '', url);
309
+ }
310
+ }
311
+
312
+ function closeWindowsDrawer() {
313
+ windowsDrawer.classList.remove('open');
314
+ windowsBackdrop.classList.remove('open');
315
+ stopWindowsRefresh();
316
+ clearWindowsError();
317
+ const url = new URL(location.href);
318
+ if (url.searchParams.get('tab') === 'windows') {
319
+ url.searchParams.delete('tab');
320
+ history.pushState({}, '', url);
321
+ }
322
+ }
323
+
324
+ document.getElementById('windows-new-btn').addEventListener('click', async () => {
325
+ clearWindowsError();
326
+ try {
327
+ const res = await fetch(
328
+ '/api/session/' + encodeURIComponent(WIN_SESSION) + '/new-window',
329
+ { method: 'POST' },
330
+ );
331
+ if (!res.ok) {
332
+ const data = await res.json().catch(() => ({}));
333
+ showWindowsError(data.error || 'Failed to create window');
334
+ return;
335
+ }
336
+ renderWindowsList(await fetchWinList());
337
+ } catch {
338
+ showWindowsError('Failed to create window');
339
+ }
340
+ });
341
+
342
+ document.getElementById('windows-toggle').addEventListener('click', () => {
343
+ if (windowsDrawer.classList.contains('open')) closeWindowsDrawer();
344
+ else openWindowsDrawer();
345
+ });
346
+ document.getElementById('windows-close').addEventListener('click', closeWindowsDrawer);
347
+ windowsBackdrop.addEventListener('click', closeWindowsDrawer);
348
+
349
+ // A tmux-side window switch (pushed over the terminal WebSocket) re-renders the
350
+ // list so the 'current' highlight tracks tmux. Re-fetch rather than trust the
351
+ // pushed payload so custom labels stay merged. Only when the drawer is open.
352
+ window.addEventListener('tmux:windows', async () => {
353
+ if (windowsDrawer.classList.contains('open') && !editingWindow) {
354
+ renderWindowsList(await fetchWinList());
355
+ }
356
+ });
357
+
358
+ window.addEventListener('popstate', () => {
359
+ const tab = new URLSearchParams(location.search).get('tab');
360
+ if (tab === 'windows') openWindowsDrawer();
361
+ else closeWindowsDrawer();
362
+ });
363
+
364
+ if (new URLSearchParams(location.search).get('tab') === 'windows') openWindowsDrawer();`,"closeWindowsDrawer")}export{c as windowsDrawerCSS,l as windowsDrawerHTML,p as windowsDrawerScript};
@@ -0,0 +1 @@
1
+ function a(e,r){const n=typeof e=="string"?e:e.toString("utf-8");let t;try{t=JSON.parse(n)}catch{return!1}return t.type==="input"&&typeof t.data=="string"?(r.write(t.data),!0):t.type==="resize"&&typeof t.cols=="number"&&typeof t.rows=="number"?(r.resize(Math.max(10,t.cols),Math.max(5,t.rows)),!0):!1}export{a as handleClientMessage};
@@ -0,0 +1,2 @@
1
+ import{execFileSync as o}from"node:child_process";function a(){try{return o("tmux",["list-sessions","-F","#{session_name} #{session_windows} #{session_attached}"],{encoding:"utf-8",timeout:3e3}).trim().split(`
2
+ `).filter(Boolean).map(t=>{const[s,n,e]=t.split(" ");return{name:s,windows:parseInt(n,10),attached:e!=="0"}})}catch{return[]}}export{a as listSessions};
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "tmux-weblink",
3
+ "version": "0.3.0",
4
+ "description": "Access your tmux sessions from the browser. A continuation of tmux-web and persalink.",
5
+ "type": "module",
6
+ "bin": {
7
+ "tmux-weblink": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "scripts/fix-pty-perms.mjs"
12
+ ],
13
+ "scripts": {
14
+ "postinstall": "node scripts/fix-pty-perms.mjs",
15
+ "prepublishOnly": "tsc && npm run build:client && npm run build:exts && npm run minify:dist",
16
+ "build": "tsc && npm run check:drawer-scripts && npm run build:client && npm run build:exts && npm run minify:dist",
17
+ "check:drawer-scripts": "tsx scripts/check-drawer-scripts.ts",
18
+ "build:client": "node scripts/build-client.mjs",
19
+ "typecheck": "tsc --noEmit",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "coverage": "vitest run --coverage",
23
+ "build:exts": "node scripts/build-exts.mjs --force",
24
+ "minify:dist": "node scripts/minify-dist.mjs",
25
+ "start": "node dist/index.js",
26
+ "predev": "node scripts/build-client.mjs && node scripts/build-exts.mjs",
27
+ "dev": "TMUX_WEB_DEV=1 npx tsx src/index.ts"
28
+ },
29
+ "keywords": [
30
+ "tmux",
31
+ "terminal",
32
+ "web",
33
+ "pty",
34
+ "websocket"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/Willlee11/tmux-weblink"
40
+ },
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
+ "dependencies": {
45
+ "@hono/node-server": "^1.19.12",
46
+ "@xterm/addon-fit": "^0.11.0",
47
+ "@xterm/xterm": "^6.0.0",
48
+ "hono": "^4.7.0",
49
+ "lowdb": "^7.0.1",
50
+ "node-pty": "^1.0.0",
51
+ "ws": "^8.18.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^25.8.0",
55
+ "@types/ws": "^8.5.0",
56
+ "@vitest/coverage-v8": "^2.1.9",
57
+ "esbuild": "^0.25.0",
58
+ "tsx": "^4.21.0",
59
+ "typescript": "^5.7.0",
60
+ "vitest": "^2.1.9"
61
+ }
62
+ }
@@ -0,0 +1,16 @@
1
+ import { createRequire } from "node:module";
2
+ import { chmodSync, existsSync, readdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+
5
+ try {
6
+ const require = createRequire(import.meta.url);
7
+ const ptyDir = dirname(require.resolve("node-pty/package.json"));
8
+ const prebuilds = join(ptyDir, "prebuilds");
9
+ if (!existsSync(prebuilds)) process.exit(0);
10
+ for (const arch of readdirSync(prebuilds)) {
11
+ const helper = join(prebuilds, arch, "spawn-helper");
12
+ if (existsSync(helper)) chmodSync(helper, 0o755);
13
+ }
14
+ } catch {
15
+ // best-effort; never block install
16
+ }