pi-interactive-shell 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.
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "pi-interactive-shell",
3
+ "version": "0.3.0",
4
+ "description": "Run AI coding agents as foreground subagents in pi TUI overlays with hands-free monitoring",
5
+ "type": "module",
6
+ "bin": {
7
+ "pi-interactive-shell-install": "./scripts/install.js"
8
+ },
9
+ "files": [
10
+ "index.ts",
11
+ "config.ts",
12
+ "overlay-component.ts",
13
+ "pty-session.ts",
14
+ "session-manager.ts",
15
+ "scripts/",
16
+ "README.md",
17
+ "SKILL.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "pi": {
21
+ "extensions": [
22
+ "./index.ts"
23
+ ]
24
+ },
25
+ "dependencies": {
26
+ "node-pty": "^1.0.0",
27
+ "@xterm/headless": "^5.5.0",
28
+ "@xterm/addon-serialize": "^0.13.0"
29
+ },
30
+ "scripts": {
31
+ "postinstall": "node ./scripts/fix-spawn-helper.cjs"
32
+ },
33
+ "keywords": [
34
+ "pi",
35
+ "pi-coding-agent",
36
+ "extension",
37
+ "interactive",
38
+ "shell",
39
+ "terminal",
40
+ "tui",
41
+ "subagent",
42
+ "claude",
43
+ "gemini",
44
+ "codex"
45
+ ],
46
+ "author": "Nico Bailon",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/nicobailon/pi-interactive-shell.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/nicobailon/pi-interactive-shell/issues"
54
+ },
55
+ "homepage": "https://github.com/nicobailon/pi-interactive-shell#readme"
56
+ }
package/pty-session.ts ADDED
@@ -0,0 +1,561 @@
1
+ import { chmodSync, statSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { stripVTControlCharacters } from "node:util";
5
+ import * as pty from "node-pty";
6
+ import type { IBufferCell, Terminal as XtermTerminal } from "@xterm/headless";
7
+ import xterm from "@xterm/headless";
8
+ import { SerializeAddon } from "@xterm/addon-serialize";
9
+
10
+ const Terminal = xterm.Terminal;
11
+ const require = createRequire(import.meta.url);
12
+ let spawnHelperChecked = false;
13
+
14
+ // Regex patterns for sanitizing terminal output (used by sanitizeLine for viewport rendering)
15
+ const OSC_REGEX = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
16
+ const APC_REGEX = /\x1b_[^\x07\x1b]*(?:\x07|\x1b\\)/g;
17
+ const DCS_REGEX = /\x1bP[^\x07\x1b]*(?:\x07|\x1b\\)/g;
18
+ const CSI_REGEX = /\x1b\[[0-9;?]*[A-Za-z]/g;
19
+ const ESC_SINGLE_REGEX = /\x1b[@-_]/g;
20
+ const CONTROL_REGEX = /[\x00-\x08\x0B\x0C\x0E-\x1A\x1C-\x1F\x7F]/g;
21
+
22
+ // DSR (Device Status Report) - cursor position query: ESC[6n or ESC[?6n
23
+ const DSR_PATTERN = /\x1b\[\??6n/g;
24
+
25
+ function stripDsrRequests(input: string): { cleaned: string; requests: number } {
26
+ let requests = 0;
27
+ const cleaned = input.replace(DSR_PATTERN, () => {
28
+ requests += 1;
29
+ return "";
30
+ });
31
+ return { cleaned, requests };
32
+ }
33
+
34
+ function buildCursorPositionResponse(row = 1, col = 1): string {
35
+ return `\x1b[${row};${col}R`;
36
+ }
37
+
38
+ function ensureSpawnHelperExec(): void {
39
+ if (spawnHelperChecked) return;
40
+ spawnHelperChecked = true;
41
+ if (process.platform !== "darwin") return;
42
+
43
+ let pkgPath: string;
44
+ try {
45
+ pkgPath = require.resolve("node-pty/package.json");
46
+ } catch {
47
+ return;
48
+ }
49
+
50
+ const base = dirname(pkgPath);
51
+ const targets = [
52
+ join(base, "prebuilds", "darwin-arm64", "spawn-helper"),
53
+ join(base, "prebuilds", "darwin-x64", "spawn-helper"),
54
+ ];
55
+
56
+ for (const target of targets) {
57
+ try {
58
+ const stats = statSync(target);
59
+ const mode = stats.mode | 0o111;
60
+ if ((stats.mode & 0o111) !== 0o111) {
61
+ chmodSync(target, mode);
62
+ }
63
+ } catch {
64
+ continue;
65
+ }
66
+ }
67
+ }
68
+
69
+ function sanitizeLine(line: string): string {
70
+ let out = line;
71
+ if (out.includes("\u001b")) {
72
+ out = out.replace(OSC_REGEX, "");
73
+ out = out.replace(APC_REGEX, "");
74
+ out = out.replace(DCS_REGEX, "");
75
+ out = out.replace(CSI_REGEX, (match) => (match.endsWith("m") ? match : ""));
76
+ out = out.replace(ESC_SINGLE_REGEX, "");
77
+ }
78
+ if (out.includes("\t")) {
79
+ out = out.replace(/\t/g, " ");
80
+ }
81
+ if (out.includes("\r")) {
82
+ out = out.replace(/\r/g, "");
83
+ }
84
+ out = out.replace(CONTROL_REGEX, "");
85
+ return out;
86
+ }
87
+
88
+ type CellStyle = {
89
+ bold: boolean;
90
+ dim: boolean;
91
+ italic: boolean;
92
+ underline: boolean;
93
+ inverse: boolean;
94
+ invisible: boolean;
95
+ strikethrough: boolean;
96
+ fgMode: "default" | "palette" | "rgb";
97
+ fg: number;
98
+ bgMode: "default" | "palette" | "rgb";
99
+ bg: number;
100
+ };
101
+
102
+ function styleKey(style: CellStyle): string {
103
+ return [
104
+ style.bold ? "b" : "-",
105
+ style.dim ? "d" : "-",
106
+ style.italic ? "i" : "-",
107
+ style.underline ? "u" : "-",
108
+ style.inverse ? "v" : "-",
109
+ style.invisible ? "x" : "-",
110
+ style.strikethrough ? "s" : "-",
111
+ `fg:${style.fgMode}:${style.fg}`,
112
+ `bg:${style.bgMode}:${style.bg}`,
113
+ ].join("");
114
+ }
115
+
116
+ function rgbToSgr(isFg: boolean, hex: number): string {
117
+ const r = (hex >> 16) & 0xff;
118
+ const g = (hex >> 8) & 0xff;
119
+ const b = hex & 0xff;
120
+ return isFg ? `38;2;${r};${g};${b}` : `48;2;${r};${g};${b}`;
121
+ }
122
+
123
+ function paletteToSgr(isFg: boolean, idx: number): string {
124
+ return isFg ? `38;5;${idx}` : `48;5;${idx}`;
125
+ }
126
+
127
+ function sgrForStyle(style: CellStyle): string {
128
+ const parts: string[] = ["0"];
129
+ if (style.bold) parts.push("1");
130
+ if (style.dim) parts.push("2");
131
+ if (style.italic) parts.push("3");
132
+ if (style.underline) parts.push("4");
133
+ if (style.inverse) parts.push("7");
134
+ if (style.invisible) parts.push("8");
135
+ if (style.strikethrough) parts.push("9");
136
+
137
+ if (style.fgMode === "rgb") parts.push(rgbToSgr(true, style.fg));
138
+ else if (style.fgMode === "palette") parts.push(paletteToSgr(true, style.fg));
139
+
140
+ if (style.bgMode === "rgb") parts.push(rgbToSgr(false, style.bg));
141
+ else if (style.bgMode === "palette") parts.push(paletteToSgr(false, style.bg));
142
+
143
+ return `\u001b[${parts.join(";")}m`;
144
+ }
145
+
146
+ function normalizePaletteColor(mode: "default" | "palette" | "rgb", value: number): { mode: "default" | "palette" | "rgb"; value: number } {
147
+ if (mode !== "palette") return { mode, value };
148
+ // xterm uses special palette values (>= 256) to represent defaults/specials; do not emit invalid 38;5;N codes.
149
+ if (value < 0 || value > 255) {
150
+ return { mode: "default", value: 0 };
151
+ }
152
+ return { mode: "palette", value };
153
+ }
154
+
155
+ export interface PtySessionOptions {
156
+ command: string;
157
+ shell?: string;
158
+ cwd?: string;
159
+ env?: Record<string, string | undefined>;
160
+ cols?: number;
161
+ rows?: number;
162
+ scrollback?: number;
163
+ ansiReemit?: boolean;
164
+ }
165
+
166
+ export interface PtySessionEvents {
167
+ onData?: (data: string) => void;
168
+ onExit?: (exitCode: number, signal?: number) => void;
169
+ }
170
+
171
+ // Simple write queue to ensure ordered writes to terminal
172
+ class WriteQueue {
173
+ private queue = Promise.resolve();
174
+
175
+ enqueue(fn: () => Promise<void> | void): void {
176
+ this.queue = this.queue.then(() => fn()).catch((err) => {
177
+ console.error("WriteQueue error:", err);
178
+ });
179
+ }
180
+
181
+ async drain(): Promise<void> {
182
+ await this.queue;
183
+ }
184
+ }
185
+
186
+ export class PtyTerminalSession {
187
+ private ptyProcess: pty.IPty;
188
+ private xterm: XtermTerminal;
189
+ private serializer: SerializeAddon | null = null;
190
+ private _exited = false;
191
+ private _exitCode: number | null = null;
192
+ private _signal: number | undefined;
193
+ private scrollOffset = 0;
194
+
195
+ // Raw output buffer for incremental streaming
196
+ private rawOutput = "";
197
+ private lastStreamPosition = 0;
198
+
199
+ // Write queue for ordered terminal writes
200
+ private writeQueue = new WriteQueue();
201
+
202
+ private dataHandler: ((data: string) => void) | undefined;
203
+ private exitHandler: ((exitCode: number, signal?: number) => void) | undefined;
204
+
205
+ constructor(options: PtySessionOptions, events: PtySessionEvents = {}) {
206
+ const {
207
+ command,
208
+ cwd = process.cwd(),
209
+ env,
210
+ cols = 80,
211
+ rows = 24,
212
+ scrollback = 5000,
213
+ ansiReemit = true,
214
+ } = options;
215
+
216
+ this.dataHandler = events.onData;
217
+ this.exitHandler = events.onExit;
218
+
219
+ this.xterm = new Terminal({ cols, rows, scrollback, allowProposedApi: true, convertEol: true });
220
+ if (ansiReemit) {
221
+ this.serializer = new SerializeAddon();
222
+ this.xterm.loadAddon(this.serializer);
223
+ }
224
+
225
+ const shell =
226
+ options.shell ??
227
+ (process.platform === "win32"
228
+ ? process.env.COMSPEC || "cmd.exe"
229
+ : process.env.SHELL || "/bin/sh");
230
+ const shellArgs = process.platform === "win32" ? ["/c", command] : ["-c", command];
231
+
232
+ const mergedEnv = env ? { ...process.env, ...env } : { ...process.env };
233
+ if (!mergedEnv.TERM) mergedEnv.TERM = "xterm-256color";
234
+
235
+ ensureSpawnHelperExec();
236
+
237
+ this.ptyProcess = pty.spawn(shell, shellArgs, {
238
+ name: "xterm-256color",
239
+ cols,
240
+ rows,
241
+ cwd,
242
+ env: mergedEnv,
243
+ });
244
+
245
+ this.ptyProcess.onData((data) => {
246
+ // Handle DSR (Device Status Report) cursor position queries
247
+ // TUI apps send ESC[6n or ESC[?6n expecting ESC[row;colR response
248
+ const { cleaned, requests } = stripDsrRequests(data);
249
+ if (requests > 0) {
250
+ // Respond with cursor position (use xterm's actual cursor position)
251
+ const buffer = this.xterm.buffer.active;
252
+ const response = buildCursorPositionResponse(buffer.cursorY + 1, buffer.cursorX + 1);
253
+ for (let i = 0; i < requests; i++) {
254
+ this.ptyProcess.write(response);
255
+ }
256
+ }
257
+
258
+ // Write cleaned data to xterm (without DSR sequences)
259
+ const dataToProcess = requests > 0 ? cleaned : data;
260
+
261
+ // Use write queue for ordered writes
262
+ this.writeQueue.enqueue(async () => {
263
+ // Track raw output for incremental streaming
264
+ this.rawOutput += dataToProcess;
265
+
266
+ await new Promise<void>((resolve) => {
267
+ this.xterm.write(dataToProcess, () => resolve());
268
+ });
269
+ this.dataHandler?.(dataToProcess);
270
+ });
271
+ });
272
+
273
+ this.ptyProcess.onExit(({ exitCode, signal }) => {
274
+ this._exited = true;
275
+ this._exitCode = exitCode;
276
+ this._signal = signal;
277
+
278
+ // Append exit message to terminal buffer, then notify handler after queue drains
279
+ const exitMsg = `\n[Process exited with code ${exitCode}${signal ? ` (signal: ${signal})` : ""}]\n`;
280
+ this.writeQueue.enqueue(async () => {
281
+ this.rawOutput += exitMsg;
282
+ await new Promise<void>((resolve) => {
283
+ this.xterm.write(exitMsg, () => resolve());
284
+ });
285
+ });
286
+
287
+ // Wait for writeQueue to drain before calling exitHandler
288
+ // This ensures exit message is in rawOutput and xterm buffer
289
+ this.writeQueue.drain().then(() => {
290
+ this.exitHandler?.(exitCode, signal);
291
+ });
292
+ });
293
+ }
294
+
295
+ setEventHandlers(events: PtySessionEvents): void {
296
+ this.dataHandler = events.onData;
297
+ this.exitHandler = events.onExit;
298
+ }
299
+
300
+ get exited(): boolean {
301
+ return this._exited;
302
+ }
303
+ get exitCode(): number | null {
304
+ return this._exitCode;
305
+ }
306
+ get signal(): number | undefined {
307
+ return this._signal;
308
+ }
309
+ get pid(): number {
310
+ return this.ptyProcess.pid;
311
+ }
312
+ get cols(): number {
313
+ return this.xterm.cols;
314
+ }
315
+ get rows(): number {
316
+ return this.xterm.rows;
317
+ }
318
+
319
+ write(data: string): void {
320
+ if (!this._exited) {
321
+ this.ptyProcess.write(data);
322
+ }
323
+ }
324
+
325
+ resize(cols: number, rows: number): void {
326
+ if (cols === this.xterm.cols && rows === this.xterm.rows) return;
327
+ if (cols < 1 || rows < 1) return;
328
+ this.xterm.resize(cols, rows);
329
+ if (!this._exited) {
330
+ this.ptyProcess.resize(cols, rows);
331
+ }
332
+ }
333
+
334
+ private renderLineFromCells(lineIndex: number, cols: number): string {
335
+ const buffer = this.xterm.buffer.active;
336
+ const line = buffer.getLine(lineIndex);
337
+
338
+ let currentStyle: CellStyle = {
339
+ bold: false,
340
+ dim: false,
341
+ italic: false,
342
+ underline: false,
343
+ inverse: false,
344
+ invisible: false,
345
+ strikethrough: false,
346
+ fgMode: "default",
347
+ fg: 0,
348
+ bgMode: "default",
349
+ bg: 0,
350
+ };
351
+ let currentKey = styleKey(currentStyle);
352
+
353
+ let out = sgrForStyle(currentStyle);
354
+
355
+ for (let x = 0; x < cols; x++) {
356
+ const cell: IBufferCell | undefined = line?.getCell(x);
357
+ const width = cell?.getWidth() ?? 1;
358
+ if (width === 0) continue;
359
+
360
+ const chars = cell?.getChars() ?? " ";
361
+ const cellChars = chars.length === 0 ? " " : chars;
362
+
363
+ const rawFgMode: CellStyle["fgMode"] = cell?.isFgDefault()
364
+ ? "default"
365
+ : cell?.isFgRGB()
366
+ ? "rgb"
367
+ : cell?.isFgPalette()
368
+ ? "palette"
369
+ : "default";
370
+ const rawBgMode: CellStyle["bgMode"] = cell?.isBgDefault()
371
+ ? "default"
372
+ : cell?.isBgRGB()
373
+ ? "rgb"
374
+ : cell?.isBgPalette()
375
+ ? "palette"
376
+ : "default";
377
+
378
+ const fg = normalizePaletteColor(rawFgMode, cell?.getFgColor() ?? 0);
379
+ const bg = normalizePaletteColor(rawBgMode, cell?.getBgColor() ?? 0);
380
+
381
+ const nextStyle: CellStyle = {
382
+ bold: !!cell?.isBold(),
383
+ dim: !!cell?.isDim(),
384
+ italic: !!cell?.isItalic(),
385
+ underline: !!cell?.isUnderline(),
386
+ inverse: !!cell?.isInverse(),
387
+ invisible: !!cell?.isInvisible(),
388
+ strikethrough: !!cell?.isStrikethrough(),
389
+ fgMode: fg.mode,
390
+ fg: fg.value,
391
+ bgMode: bg.mode,
392
+ bg: bg.value,
393
+ };
394
+ const nextKey = styleKey(nextStyle);
395
+ if (nextKey !== currentKey) {
396
+ currentStyle = nextStyle;
397
+ currentKey = nextKey;
398
+ out += sgrForStyle(currentStyle);
399
+ }
400
+
401
+ out += cellChars;
402
+ }
403
+
404
+ return out + "\u001b[0m";
405
+ }
406
+
407
+ getViewportLines(options: { ansi?: boolean } = {}): string[] {
408
+ const buffer = this.xterm.buffer.active;
409
+ const lines: string[] = [];
410
+
411
+ const totalLines = buffer.length;
412
+ const viewportStart = Math.max(0, totalLines - this.xterm.rows - this.scrollOffset);
413
+
414
+ const useAnsi = !!options.ansi;
415
+ if (useAnsi) {
416
+ for (let i = 0; i < this.xterm.rows; i++) {
417
+ const lineIndex = viewportStart + i;
418
+ const rendered = this.renderLineFromCells(lineIndex, this.xterm.cols);
419
+
420
+ // Safety fallback: if our cell->SGR renderer produces no visible non-space content
421
+ // but the buffer line contains text, fall back to plain translation. This prevents
422
+ // “blank screen” regressions on terminals that use special color encodings.
423
+ const plain = buffer.getLine(lineIndex)?.translateToString(true) ?? "";
424
+ const renderedPlain = rendered
425
+ .replace(/\x1b\[[0-9;]*m/g, "")
426
+ .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "");
427
+ if (plain.trim().length > 0 && renderedPlain.trim().length === 0) {
428
+ lines.push(sanitizeLine(plain) + "\u001b[0m");
429
+ } else {
430
+ lines.push(rendered);
431
+ }
432
+ }
433
+ return lines;
434
+ }
435
+
436
+ for (let i = 0; i < this.xterm.rows; i++) {
437
+ const lineIndex = viewportStart + i;
438
+ if (lineIndex < totalLines) {
439
+ const line = buffer.getLine(lineIndex);
440
+ lines.push(sanitizeLine(line?.translateToString(true) ?? ""));
441
+ } else {
442
+ lines.push("");
443
+ }
444
+ }
445
+
446
+ return lines;
447
+ }
448
+
449
+ getTailLines(options: { lines: number; ansi?: boolean; maxChars?: number }): string[] {
450
+ const requested = Math.max(0, Math.trunc(options.lines));
451
+ const maxChars = options.maxChars !== undefined ? Math.max(0, Math.trunc(options.maxChars)) : undefined;
452
+ if (requested === 0) return [];
453
+
454
+ const buffer = this.xterm.buffer.active;
455
+ const totalLines = buffer.length;
456
+ const start = Math.max(0, totalLines - requested);
457
+
458
+ const out: string[] = [];
459
+ let remainingChars = maxChars;
460
+
461
+ const useAnsi = options.ansi && this.serializer;
462
+ if (useAnsi) {
463
+ const serialized = this.serializer!.serialize();
464
+ const serializedLines = serialized.split(/\r?\n/);
465
+ if (serializedLines.length >= totalLines) {
466
+ for (let i = start; i < totalLines; i++) {
467
+ const raw = serializedLines[i] ?? "";
468
+ const line = sanitizeLine(raw) + "\u001b[0m";
469
+ if (remainingChars !== undefined) {
470
+ if (remainingChars <= 0) break;
471
+ remainingChars -= line.length;
472
+ }
473
+ out.push(line);
474
+ }
475
+ return out;
476
+ }
477
+ }
478
+
479
+ for (let i = start; i < totalLines; i++) {
480
+ const lineObj = buffer.getLine(i);
481
+ const line = sanitizeLine(lineObj?.translateToString(true) ?? "");
482
+ if (remainingChars !== undefined) {
483
+ if (remainingChars <= 0) break;
484
+ remainingChars -= line.length;
485
+ }
486
+ out.push(line);
487
+ }
488
+
489
+ return out;
490
+ }
491
+
492
+ /**
493
+ * Get raw output stream with optional incremental reading.
494
+ * @param options.sinceLast - If true, only return output since last call
495
+ * @param options.stripAnsi - If true, strip ANSI escape codes (default: true)
496
+ */
497
+ getRawStream(options: { sinceLast?: boolean; stripAnsi?: boolean } = {}): string {
498
+ let output: string;
499
+
500
+ if (options.sinceLast) {
501
+ output = this.rawOutput.substring(this.lastStreamPosition);
502
+ this.lastStreamPosition = this.rawOutput.length;
503
+ } else {
504
+ output = this.rawOutput;
505
+ }
506
+
507
+ // Strip ANSI codes and control characters by default using Node.js built-in
508
+ if (options.stripAnsi !== false && output) {
509
+ output = stripVTControlCharacters(output);
510
+ }
511
+
512
+ return output;
513
+ }
514
+
515
+ scrollUp(lines: number): void {
516
+ const buffer = this.xterm.buffer.active;
517
+ const maxScroll = Math.max(0, buffer.length - this.xterm.rows);
518
+ this.scrollOffset = Math.min(this.scrollOffset + lines, maxScroll);
519
+ }
520
+
521
+ scrollDown(lines: number): void {
522
+ this.scrollOffset = Math.max(0, this.scrollOffset - lines);
523
+ }
524
+
525
+ scrollToBottom(): void {
526
+ this.scrollOffset = 0;
527
+ }
528
+
529
+ isScrolledUp(): boolean {
530
+ return this.scrollOffset > 0;
531
+ }
532
+
533
+ kill(signal: string = "SIGTERM"): void {
534
+ if (this._exited) return;
535
+
536
+ const pid = this.ptyProcess.pid;
537
+
538
+ // Try to kill the entire process tree (prevents orphan child processes)
539
+ if (process.platform !== "win32" && pid) {
540
+ try {
541
+ // Kill process group (negative PID)
542
+ process.kill(-pid, signal as NodeJS.Signals);
543
+ return;
544
+ } catch {
545
+ // Fall through to direct kill
546
+ }
547
+ }
548
+
549
+ // Direct kill as fallback
550
+ try {
551
+ this.ptyProcess.kill(signal);
552
+ } catch {
553
+ // Process may already be dead
554
+ }
555
+ }
556
+
557
+ dispose(): void {
558
+ this.kill();
559
+ this.xterm.dispose();
560
+ }
561
+ }
@@ -0,0 +1,37 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ function ensureExecutable(filePath) {
5
+ try {
6
+ const stats = fs.statSync(filePath);
7
+ const mode = stats.mode | 0o111;
8
+ if ((stats.mode & 0o111) !== 0o111) {
9
+ fs.chmodSync(filePath, mode);
10
+ process.stdout.write(`chmod +x ${filePath}\n`);
11
+ }
12
+ } catch (error) {
13
+ process.stdout.write(`skip ${filePath}: ${String(error)}\n`);
14
+ }
15
+ }
16
+
17
+ function main() {
18
+ let pkgPath;
19
+ try {
20
+ pkgPath = require.resolve("node-pty/package.json", { paths: [process.cwd()] });
21
+ } catch (error) {
22
+ process.stdout.write(`node-pty not found: ${String(error)}\n`);
23
+ return;
24
+ }
25
+
26
+ const base = path.dirname(pkgPath);
27
+ const targets = [
28
+ path.join(base, "prebuilds", "darwin-arm64", "spawn-helper"),
29
+ path.join(base, "prebuilds", "darwin-x64", "spawn-helper"),
30
+ ];
31
+
32
+ for (const target of targets) {
33
+ ensureExecutable(target);
34
+ }
35
+ }
36
+
37
+ main();