Skip to content

Commit 0cbfbfd

Browse files
committed
feat: PTY fallback via script command for compiled binaries
node-pty won't bundle in Bun compiled binaries, so the agent now falls back to the OS script command which allocates a real PTY without native deps. Bump agent + CLI to 0.6.25.
1 parent 76a3d74 commit 0cbfbfd

File tree

5 files changed

+115
-51
lines changed

5 files changed

+115
-51
lines changed

apps/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent",
3-
"version": "0.6.24",
3+
"version": "0.6.25",
44
"private": true,
55
"bin": {
66
"connect": "./dist/cli.js"

apps/agent/src/commands/expose.ts

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -380,30 +380,62 @@ export async function exposeCommand(target: string, options: ExposeOptions): Pro
380380

381381
if (data.pty) {
382382
try {
383-
const pty = await import('node-pty');
384383
const shell = process.env.SHELL || '/bin/bash';
385-
const ptyProcess = pty.spawn(shell, [], {
386-
name: 'xterm-256color',
387-
cols: 80,
388-
rows: 24,
389-
cwd: process.env.HOME || '/',
390-
env: { ...process.env } as Record<string, string>,
391-
});
392-
393-
connections.set(data.connectionId, { socket: ptyProcess, connected: true, pty: ptyProcess });
394-
socket.emit('dial_success', { connectionId: data.connectionId });
395-
396-
ptyProcess.onData((chunk: string) => {
397-
socket.emit('data', {
398-
connectionId: data.connectionId,
399-
data: Buffer.from(chunk, 'utf8').toString('base64'),
384+
let ptyHandle: any;
385+
386+
try {
387+
const pty = await import('node-pty');
388+
const p = pty.spawn(shell, [], {
389+
name: 'xterm-256color',
390+
cols: 80,
391+
rows: 24,
392+
cwd: process.env.HOME || '/',
393+
env: { ...process.env } as Record<string, string>,
400394
});
401-
});
395+
ptyHandle = {
396+
type: 'node-pty',
397+
process: p,
398+
write: (d: string) => p.write(d),
399+
resize: (c: number, r: number) => p.resize(c, r),
400+
kill: () => p.kill(),
401+
};
402+
p.onData((chunk: string) => {
403+
socket.emit('data', { connectionId: data.connectionId, data: Buffer.from(chunk, 'utf8').toString('base64') });
404+
});
405+
p.onExit(() => {
406+
socket.emit('close', { connectionId: data.connectionId });
407+
connections.delete(data.connectionId);
408+
});
409+
} catch {
410+
const { spawn: spawnChild } = await import('child_process');
411+
const isLinux = process.platform === 'linux';
412+
const args = isLinux ? ['-qc', shell, '/dev/null'] : ['-q', '/dev/null', shell];
413+
const child = spawnChild('script', args, {
414+
stdio: 'pipe',
415+
env: { ...process.env, TERM: 'xterm-256color' },
416+
cwd: process.env.HOME || '/',
417+
});
418+
ptyHandle = {
419+
type: 'script',
420+
process: child,
421+
write: (d: string) => child.stdin?.write(d),
422+
resize: () => {},
423+
kill: () => child.kill(),
424+
};
425+
child.stdout?.on('data', (chunk: Buffer) => {
426+
socket.emit('data', { connectionId: data.connectionId, data: chunk.toString('base64') });
427+
});
428+
child.stderr?.on('data', (chunk: Buffer) => {
429+
socket.emit('data', { connectionId: data.connectionId, data: chunk.toString('base64') });
430+
});
431+
child.on('exit', () => {
432+
socket.emit('close', { connectionId: data.connectionId });
433+
connections.delete(data.connectionId);
434+
});
435+
}
402436

403-
ptyProcess.onExit(() => {
404-
socket.emit('close', { connectionId: data.connectionId });
405-
connections.delete(data.connectionId);
406-
});
437+
connections.set(data.connectionId, { socket: ptyHandle, connected: true, pty: ptyHandle });
438+
socket.emit('dial_success', { connectionId: data.connectionId });
407439
} catch (err: any) {
408440
socket.emit('dial_error', { connectionId: data.connectionId, error: err.message || 'PTY spawn failed' });
409441
}
@@ -454,17 +486,17 @@ export async function exposeCommand(target: string, options: ExposeOptions): Pro
454486

455487
socket.on('resize', (data: { connectionId: string; cols: number; rows: number }) => {
456488
const conn = connections.get(data.connectionId);
457-
if (conn?.pty) {
489+
if (conn?.pty?.resize) {
458490
conn.pty.resize(data.cols, data.rows);
459491
}
460492
});
461493

462494
socket.on('close', (data: { connectionId: string }) => {
463495
const conn = connections.get(data.connectionId);
464496
if (conn) {
465-
if (conn.pty) {
497+
if (conn.pty?.kill) {
466498
conn.pty.kill();
467-
} else {
499+
} else if (conn.socket?.end) {
468500
conn.socket.end();
469501
}
470502
connections.delete(data.connectionId);

apps/agent/src/commands/up.ts

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -373,31 +373,63 @@ function connectToHub(config: AgentConfig): Socket {
373373

374374
if (data.pty) {
375375
try {
376-
const pty = await import('node-pty');
377376
const shell = process.env.SHELL || '/bin/bash';
378-
const ptyProcess = pty.spawn(shell, [], {
379-
name: 'xterm-256color',
380-
cols: 80,
381-
rows: 24,
382-
cwd: process.env.HOME || '/',
383-
env: { ...process.env } as Record<string, string>,
384-
});
385-
386-
connections.set(data.connectionId, { socket: ptyProcess, connected: true, pty: ptyProcess });
387-
console.log(chalk.green(` [ok] PTY shell spawned (${shell})`));
388-
socket.emit('dial_success', { connectionId: data.connectionId });
377+
let ptyHandle: any;
389378

390-
ptyProcess.onData((chunk: string) => {
391-
socket.emit('data', {
392-
connectionId: data.connectionId,
393-
data: Buffer.from(chunk, 'utf8').toString('base64'),
379+
try {
380+
const pty = await import('node-pty');
381+
const p = pty.spawn(shell, [], {
382+
name: 'xterm-256color',
383+
cols: 80,
384+
rows: 24,
385+
cwd: process.env.HOME || '/',
386+
env: { ...process.env } as Record<string, string>,
394387
});
395-
});
388+
ptyHandle = {
389+
type: 'node-pty',
390+
process: p,
391+
write: (d: string) => p.write(d),
392+
resize: (c: number, r: number) => p.resize(c, r),
393+
kill: () => p.kill(),
394+
};
395+
p.onData((chunk: string) => {
396+
socket.emit('data', { connectionId: data.connectionId, data: Buffer.from(chunk, 'utf8').toString('base64') });
397+
});
398+
p.onExit(() => {
399+
socket.emit('close', { connectionId: data.connectionId });
400+
connections.delete(data.connectionId);
401+
});
402+
} catch {
403+
const { spawn: spawnChild } = await import('child_process');
404+
const isLinux = process.platform === 'linux';
405+
const args = isLinux ? ['-qc', shell, '/dev/null'] : ['-q', '/dev/null', shell];
406+
const child = spawnChild('script', args, {
407+
stdio: 'pipe',
408+
env: { ...process.env, TERM: 'xterm-256color' },
409+
cwd: process.env.HOME || '/',
410+
});
411+
ptyHandle = {
412+
type: 'script',
413+
process: child,
414+
write: (d: string) => child.stdin?.write(d),
415+
resize: () => {},
416+
kill: () => child.kill(),
417+
};
418+
child.stdout?.on('data', (chunk: Buffer) => {
419+
socket.emit('data', { connectionId: data.connectionId, data: chunk.toString('base64') });
420+
});
421+
child.stderr?.on('data', (chunk: Buffer) => {
422+
socket.emit('data', { connectionId: data.connectionId, data: chunk.toString('base64') });
423+
});
424+
child.on('exit', () => {
425+
socket.emit('close', { connectionId: data.connectionId });
426+
connections.delete(data.connectionId);
427+
});
428+
}
396429

397-
ptyProcess.onExit(() => {
398-
socket.emit('close', { connectionId: data.connectionId });
399-
connections.delete(data.connectionId);
400-
});
430+
connections.set(data.connectionId, { socket: ptyHandle, connected: true, pty: ptyHandle });
431+
console.log(chalk.green(` [ok] PTY shell spawned (${shell}) via ${ptyHandle.type}`));
432+
socket.emit('dial_success', { connectionId: data.connectionId });
401433
} catch (error: unknown) {
402434
const err = error as Error;
403435
console.log(chalk.red(` [x] PTY spawn failed: ${err.message}`));
@@ -465,7 +497,7 @@ function connectToHub(config: AgentConfig): Socket {
465497
// Handle terminal resize from hub
466498
socket.on('resize', (data: { connectionId: string; cols: number; rows: number }) => {
467499
const conn = connections.get(data.connectionId);
468-
if (conn?.pty) {
500+
if (conn?.pty?.resize) {
469501
conn.pty.resize(data.cols, data.rows);
470502
}
471503
});
@@ -474,9 +506,9 @@ function connectToHub(config: AgentConfig): Socket {
474506
socket.on('close', (data: { connectionId: string }) => {
475507
const conn = connections.get(data.connectionId);
476508
if (conn) {
477-
if (conn.pty) {
509+
if (conn.pty?.kill) {
478510
conn.pty.kill();
479-
} else {
511+
} else if (conn.socket?.end) {
480512
conn.socket.end();
481513
}
482514
connections.delete(data.connectionId);

apps/web/pages/terminal.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
Disconnect
6363
</button>
6464
</div>
65-
<div ref="terminalEl" class="flex-1 min-h-0 p-2" />
65+
<div ref="terminalEl" class="flex-1 min-h-0 p-2 pt-0" />
6666
</div>
6767
</div>
6868
</template>

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "private-connect",
3-
"version": "0.6.24",
3+
"version": "0.6.25",
44
"description": "Access private services by name from anywhere. No VPN setup, no firewall rules. Open source alternative to ngrok and Tailscale for service-level connectivity.",
55
"bin": {
66
"private-connect": "dist/index.js"

0 commit comments

Comments
 (0)