pty-serv 0.1.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/README.md +73 -0
- package/bin/cli.js +3 -0
- package/dist/cli.js +88 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# pty-serve
|
|
2
|
+
|
|
3
|
+
Spawns a command in a [node-pty](https://github.com/microsoft/node-pty) pseudo-terminal and exposes an HTTP endpoint to interact with it at runtime.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g pty-serve
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Run a shell, then send a command from another process:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Terminal 1 – start the PTY server
|
|
17
|
+
pty-serve -s /tmp/pty-serve-0.sock -- bash
|
|
18
|
+
|
|
19
|
+
# Terminal 2 – interact with it
|
|
20
|
+
curl --unix-socket /tmp/pty-serve-0.sock 'http://[::]/write' --data $'echo hello\r'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### CLI Options
|
|
24
|
+
|
|
25
|
+
| Flag | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `-s, --socket <path>` | Unix socket path to listen on |
|
|
28
|
+
| `-p, --port <number>` | TCP port to listen on |
|
|
29
|
+
| `-h, --hostname <string>` | Hostname for TCP listener (default: all interfaces) |
|
|
30
|
+
| `-o, --options <json>` | JSON options forwarded to [node-pty](https://github.com/microsoft/node-pty) |
|
|
31
|
+
|
|
32
|
+
### HTTP Endpoint
|
|
33
|
+
|
|
34
|
+
#### `POST /write`
|
|
35
|
+
|
|
36
|
+
Write raw data to the PTY's stdin. The request body is streamed directly to the PTY.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl 'http://[::]/write' -d $'ls\r'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### `POST /resize`
|
|
43
|
+
|
|
44
|
+
Resize the PTY window.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl 'http://[::]/resize' -d '{"cols":220,"rows":50}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
#### `POST /kill`
|
|
51
|
+
|
|
52
|
+
Send a signal to the PTY process.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl 'http://[::]/kill' -d '{"signal":"SIGTERM"}'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### `POST /clear`
|
|
59
|
+
|
|
60
|
+
Clear the PTY screen buffer.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl 'http://[::]/clear' -X POST
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### `POST /pause` / `POST /resume`
|
|
67
|
+
|
|
68
|
+
Pause or resume data flow from the PTY.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
curl 'http://[::]/pause' -X POST
|
|
72
|
+
curl 'http://[::]/resume' -X POST
|
|
73
|
+
```
|
package/bin/cli.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import * as pty from 'node-pty';
|
|
4
|
+
program
|
|
5
|
+
.option('-o, --options <string>', 'JSON options forwarded to `node-pty`')
|
|
6
|
+
.option('-s, --socket <string>', 'Unix socket path to listen on')
|
|
7
|
+
.option('-h, --hostname <string>', 'Hostname for TCP listener')
|
|
8
|
+
.option('-p, --port <number>', 'TCP port to listen on')
|
|
9
|
+
.allowExcessArguments()
|
|
10
|
+
.parse();
|
|
11
|
+
const opts = program.opts();
|
|
12
|
+
const server = createServer();
|
|
13
|
+
const ptyProcess = pty.spawn(program.args[0], program.args.slice(1), opts.options && JSON.parse(opts.options));
|
|
14
|
+
console.log('PTY spawned with PID:', ptyProcess.pid);
|
|
15
|
+
ptyProcess.onData((chunk) => {
|
|
16
|
+
process.stdout.write(chunk);
|
|
17
|
+
});
|
|
18
|
+
let hasExit = false;
|
|
19
|
+
ptyProcess.onExit((info) => {
|
|
20
|
+
hasExit = true;
|
|
21
|
+
server.close();
|
|
22
|
+
process.exit(info.exitCode);
|
|
23
|
+
});
|
|
24
|
+
const EXIT_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
25
|
+
EXIT_SIGNALS.forEach((signal) => {
|
|
26
|
+
process.on(signal, () => {
|
|
27
|
+
if (!hasExit) {
|
|
28
|
+
ptyProcess.kill(process.platform === 'win32' ? undefined : signal);
|
|
29
|
+
}
|
|
30
|
+
server.close();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
server.on('request', async (req, res) => {
|
|
35
|
+
try {
|
|
36
|
+
await handleRequest(req);
|
|
37
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
38
|
+
res.end(JSON.stringify({ code: 0 }));
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
42
|
+
res.end(JSON.stringify({ code: -1, message: String(error) }));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (opts.socket) {
|
|
46
|
+
server.listen(opts.socket);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
server.listen(opts.port, opts.hostname);
|
|
50
|
+
}
|
|
51
|
+
async function handleRequest(req) {
|
|
52
|
+
switch (req.url) {
|
|
53
|
+
case '/resize': {
|
|
54
|
+
const data = await parseBody(req);
|
|
55
|
+
ptyProcess.resize(data.cols, data.rows);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
case '/clear':
|
|
59
|
+
ptyProcess.clear();
|
|
60
|
+
return;
|
|
61
|
+
case '/write':
|
|
62
|
+
for await (const chunk of req) {
|
|
63
|
+
ptyProcess.write(chunk);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
case '/kill': {
|
|
67
|
+
const data = await parseBody(req);
|
|
68
|
+
ptyProcess.kill(data.signal);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
case '/pause':
|
|
72
|
+
ptyProcess.pause();
|
|
73
|
+
return;
|
|
74
|
+
case '/resume':
|
|
75
|
+
ptyProcess.resume();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function parseBody(req) {
|
|
80
|
+
if (req.method !== 'POST') {
|
|
81
|
+
throw new Error(`Can't parse body for ${req.method} request`);
|
|
82
|
+
}
|
|
83
|
+
const chunks = [];
|
|
84
|
+
for await (const chunk of req) {
|
|
85
|
+
chunks.push(chunk);
|
|
86
|
+
}
|
|
87
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
88
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pty-serv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/gzzhanghao/node-pty-serve.git"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"pty-serve": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"test": "node --import tsx/esm --experimental-test-coverage --test src/cli.test.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commander": "^14.0.3",
|
|
23
|
+
"node-pty": "^1.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@changesets/cli": "^2.29.8",
|
|
27
|
+
"@eslint/js": "^10.0.1",
|
|
28
|
+
"@types/node": "^25.3.0",
|
|
29
|
+
"eslint": "^10.0.2",
|
|
30
|
+
"eslint-config-prettier": "^10.1.8",
|
|
31
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
32
|
+
"prettier": "^3.8.1",
|
|
33
|
+
"rolldown": "1.0.0-rc.5",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"typescript-eslint": "^8.56.1"
|
|
37
|
+
},
|
|
38
|
+
"packageManager": "pnpm@10.30.3"
|
|
39
|
+
}
|