noninteractive 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/CLAUDE.md +144 -0
- package/README.md +15 -0
- package/index.ts +1 -0
- package/package.json +19 -0
- package/prompt.md +11 -0
- package/src/client.ts +37 -0
- package/src/daemon.ts +123 -0
- package/src/index.ts +146 -0
- package/src/paths.ts +12 -0
- package/src/ptybridge.py +47 -0
- package/tsconfig.json +29 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# noninteractive CLI
|
|
2
|
+
|
|
3
|
+
Session wrapper for interactive CLI commands. Lets claude code run interactive login flows non-interactively.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
- `src/index.ts` — CLI entry point, arg parsing, auto-start flow
|
|
8
|
+
- `src/daemon.ts` — detached daemon per session, spawns target command, unix socket server
|
|
9
|
+
- `src/client.ts` — connects to daemon via unix socket, sends JSON commands
|
|
10
|
+
- `src/paths.ts` — session dir/socket path helpers
|
|
11
|
+
- `src/ptybridge.py` — python3 PTY bridge, allocates real terminal for child process
|
|
12
|
+
|
|
13
|
+
Sessions stored at `~/.noninteractive/sessions/<name>.sock`. Protocol is JSON over unix socket.
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
noninteractive start <name> [args...] # start session (runs npx <name> in a PTY)
|
|
19
|
+
noninteractive read <name> # read terminal output
|
|
20
|
+
noninteractive send <name> <text> # send keystrokes to session
|
|
21
|
+
noninteractive stop <name> # stop a session
|
|
22
|
+
noninteractive list # show active sessions (alias: ls)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Build
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
bun run build # compiles to bin/noninteractive standalone binary
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Notes
|
|
32
|
+
|
|
33
|
+
- daemon uses node:child_process and node:net (not Bun-specific APIs) for subprocess/socket — needed for detached spawn and unix socket server
|
|
34
|
+
- PTY via python3 ptybridge.py — allocates a real pseudo-terminal so child processes see isTTY=true
|
|
35
|
+
- compiled binary is ~55MB (includes bun runtime)
|
|
36
|
+
- ptybridge.py must be co-located with the binary (or in src/ during dev)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Default to using Bun instead of Node.js.
|
|
41
|
+
|
|
42
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
43
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
44
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
45
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
46
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
47
|
+
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
48
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
49
|
+
|
|
50
|
+
## APIs
|
|
51
|
+
|
|
52
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
53
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
54
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
55
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
56
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
57
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
58
|
+
- Bun.$`ls` instead of execa.
|
|
59
|
+
|
|
60
|
+
## Testing
|
|
61
|
+
|
|
62
|
+
Use `bun test` to run tests.
|
|
63
|
+
|
|
64
|
+
```ts#index.test.ts
|
|
65
|
+
import { test, expect } from "bun:test";
|
|
66
|
+
|
|
67
|
+
test("hello world", () => {
|
|
68
|
+
expect(1).toBe(1);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Frontend
|
|
73
|
+
|
|
74
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
75
|
+
|
|
76
|
+
Server:
|
|
77
|
+
|
|
78
|
+
```ts#index.ts
|
|
79
|
+
import index from "./index.html"
|
|
80
|
+
|
|
81
|
+
Bun.serve({
|
|
82
|
+
routes: {
|
|
83
|
+
"/": index,
|
|
84
|
+
"/api/users/:id": {
|
|
85
|
+
GET: (req) => {
|
|
86
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
// optional websocket support
|
|
91
|
+
websocket: {
|
|
92
|
+
open: (ws) => {
|
|
93
|
+
ws.send("Hello, world!");
|
|
94
|
+
},
|
|
95
|
+
message: (ws, message) => {
|
|
96
|
+
ws.send(message);
|
|
97
|
+
},
|
|
98
|
+
close: (ws) => {
|
|
99
|
+
// handle close
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
development: {
|
|
103
|
+
hmr: true,
|
|
104
|
+
console: true,
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
110
|
+
|
|
111
|
+
```html#index.html
|
|
112
|
+
<html>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>Hello, world!</h1>
|
|
115
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
116
|
+
</body>
|
|
117
|
+
</html>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
With the following `frontend.tsx`:
|
|
121
|
+
|
|
122
|
+
```tsx#frontend.tsx
|
|
123
|
+
import React from "react";
|
|
124
|
+
import { createRoot } from "react-dom/client";
|
|
125
|
+
|
|
126
|
+
// import .css files directly and it works
|
|
127
|
+
import './index.css';
|
|
128
|
+
|
|
129
|
+
const root = createRoot(document.body);
|
|
130
|
+
|
|
131
|
+
export default function Frontend() {
|
|
132
|
+
return <h1>Hello, world!</h1>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
root.render(<Frontend />);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then, run index.ts
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
bun --hot ./index.ts
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
package/README.md
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
console.log("Hello via Bun!");
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "noninteractive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"noninteractive": "./src/index.ts"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "bun run src/index.ts",
|
|
10
|
+
"build": "bun build src/index.ts --compile --outfile bin/noninteractive",
|
|
11
|
+
"test": "bun test"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bun": "latest"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"typescript": "^5"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/prompt.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
You have a CLI tool called `2027` that wraps interactive CLI commands so you can control them non-interactively across multiple invocations.
|
|
2
|
+
|
|
3
|
+
It has these subcommands:
|
|
4
|
+
|
|
5
|
+
- Start a session (runs the command in background with a PTY)
|
|
6
|
+
- Read the current terminal output of a session
|
|
7
|
+
- Send keystrokes/text to a session
|
|
8
|
+
- Stop a session
|
|
9
|
+
- List active sessions
|
|
10
|
+
|
|
11
|
+
Given this tool, what would you name each subcommand? Just the verb, one word each. Think about what feels most natural and guessable.
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
|
|
3
|
+
export function sendMessage(sockPath: string, msg: Record<string, unknown>): Promise<any> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const socket = createConnection(sockPath);
|
|
6
|
+
let data = "";
|
|
7
|
+
|
|
8
|
+
socket.on("connect", () => {
|
|
9
|
+
socket.write(JSON.stringify(msg));
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
socket.on("data", (chunk) => {
|
|
13
|
+
data += chunk.toString();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
socket.on("end", () => {
|
|
17
|
+
try {
|
|
18
|
+
resolve(JSON.parse(data));
|
|
19
|
+
} catch {
|
|
20
|
+
reject(new Error("invalid response from daemon"));
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
socket.on("error", (err: NodeJS.ErrnoException) => {
|
|
25
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
26
|
+
reject(new Error("session not found"));
|
|
27
|
+
} else {
|
|
28
|
+
reject(err);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
socket.destroy();
|
|
34
|
+
reject(new Error("connection timeout"));
|
|
35
|
+
}, 5000);
|
|
36
|
+
});
|
|
37
|
+
}
|
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { unlinkSync } from "node:fs";
|
|
4
|
+
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { socketPath, ensureSessionsDir } from "./paths";
|
|
6
|
+
|
|
7
|
+
function getPtyBridge(): string {
|
|
8
|
+
// when compiled, pty.py is bundled next to the binary
|
|
9
|
+
// in dev, it's in the same directory as this file
|
|
10
|
+
const candidates = [
|
|
11
|
+
resolve(dirname(process.argv[1] || process.execPath), "ptybridge.py"),
|
|
12
|
+
resolve(dirname(import.meta.dirname), "ptybridge.py"),
|
|
13
|
+
resolve(import.meta.dirname, "ptybridge.py"),
|
|
14
|
+
];
|
|
15
|
+
for (const p of candidates) {
|
|
16
|
+
try {
|
|
17
|
+
const { statSync } = require("node:fs");
|
|
18
|
+
if (statSync(p).isFile()) return p;
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
return resolve(import.meta.dirname, "ptybridge.py");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runDaemon(sessionName: string, executable: string, args: string[]) {
|
|
25
|
+
ensureSessionsDir();
|
|
26
|
+
const sock = socketPath(sessionName);
|
|
27
|
+
|
|
28
|
+
try { unlinkSync(sock); } catch {}
|
|
29
|
+
|
|
30
|
+
let outputBuffer = "";
|
|
31
|
+
let processExited = false;
|
|
32
|
+
let exitCode: number | null = null;
|
|
33
|
+
|
|
34
|
+
const ptyBridge = getPtyBridge();
|
|
35
|
+
const proc = spawn("python3", [ptyBridge, executable, ...args], {
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
proc.stdout!.on("data", (chunk: Buffer) => {
|
|
41
|
+
outputBuffer += chunk.toString();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
proc.stderr!.on("data", (chunk: Buffer) => {
|
|
45
|
+
outputBuffer += chunk.toString();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
proc.on("exit", (code) => {
|
|
49
|
+
processExited = true;
|
|
50
|
+
exitCode = code;
|
|
51
|
+
outputBuffer += `\n[exited ${code}]`;
|
|
52
|
+
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
server.close();
|
|
55
|
+
try { unlinkSync(sock); } catch {}
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}, 60_000);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
proc.on("error", (err) => {
|
|
61
|
+
outputBuffer += `\n[error: ${err.message}]`;
|
|
62
|
+
processExited = true;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const server = createServer((socket) => {
|
|
66
|
+
let buf = "";
|
|
67
|
+
|
|
68
|
+
socket.on("data", (chunk) => {
|
|
69
|
+
buf += chunk.toString();
|
|
70
|
+
try {
|
|
71
|
+
const msg = JSON.parse(buf);
|
|
72
|
+
buf = "";
|
|
73
|
+
handle(msg, socket);
|
|
74
|
+
} catch {}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
function handle(msg: any, socket: any) {
|
|
79
|
+
switch (msg.action) {
|
|
80
|
+
case "read":
|
|
81
|
+
socket.end(JSON.stringify({
|
|
82
|
+
ok: true,
|
|
83
|
+
output: outputBuffer,
|
|
84
|
+
exited: processExited,
|
|
85
|
+
exitCode,
|
|
86
|
+
}));
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case "send":
|
|
90
|
+
if (processExited) {
|
|
91
|
+
socket.end(JSON.stringify({ ok: false, error: "process exited" }));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
proc.stdin!.write(msg.data + "\r");
|
|
95
|
+
socket.end(JSON.stringify({ ok: true }));
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "stop":
|
|
99
|
+
proc.kill("SIGTERM");
|
|
100
|
+
socket.end(JSON.stringify({ ok: true }));
|
|
101
|
+
setTimeout(() => {
|
|
102
|
+
server.close();
|
|
103
|
+
try { unlinkSync(sock); } catch {}
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}, 500);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case "status":
|
|
109
|
+
socket.end(JSON.stringify({
|
|
110
|
+
ok: true,
|
|
111
|
+
running: !processExited,
|
|
112
|
+
pid: proc.pid,
|
|
113
|
+
exitCode,
|
|
114
|
+
}));
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
socket.end(JSON.stringify({ ok: false, error: "unknown action" }));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
server.listen(sock);
|
|
123
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { socketPath, ensureSessionsDir } from "./paths";
|
|
6
|
+
import { sendMessage } from "./client";
|
|
7
|
+
|
|
8
|
+
const HELP = `usage: noninteractive <command> [args]
|
|
9
|
+
|
|
10
|
+
start <name> [args...] start a session (runs npx <name>)
|
|
11
|
+
read <name> read terminal output
|
|
12
|
+
send <name> <text> send keystrokes to session
|
|
13
|
+
stop <name> stop a session
|
|
14
|
+
list show active sessions`;
|
|
15
|
+
|
|
16
|
+
function getSelfCommand(): string[] {
|
|
17
|
+
if (process.argv[1] && /\.(ts|js)$/.test(process.argv[1])) {
|
|
18
|
+
return [process.argv[0], process.argv[1]];
|
|
19
|
+
}
|
|
20
|
+
return [process.argv[0]];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function start(name: string, args: string[]) {
|
|
24
|
+
const sock = socketPath(name);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
28
|
+
if (res.ok) {
|
|
29
|
+
process.stdout.write(res.output ?? "");
|
|
30
|
+
console.log(`\n[session '${name}' already running]`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
|
|
35
|
+
ensureSessionsDir();
|
|
36
|
+
try { const { unlinkSync } = await import("node:fs"); unlinkSync(sock); } catch {}
|
|
37
|
+
|
|
38
|
+
const self = getSelfCommand();
|
|
39
|
+
const child = spawn(self[0], [...self.slice(1), "__daemon__", name, "npx", name, ...args], {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < 50; i++) {
|
|
46
|
+
if (existsSync(sock)) {
|
|
47
|
+
await new Promise(r => setTimeout(r, 200));
|
|
48
|
+
try {
|
|
49
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
50
|
+
if (res.output) process.stdout.write(res.output);
|
|
51
|
+
} catch {}
|
|
52
|
+
console.log(`[session '${name}' started]`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await new Promise(r => setTimeout(r, 100));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.error("timeout: failed to start session");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function read(name: string) {
|
|
63
|
+
const sock = socketPath(name);
|
|
64
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
65
|
+
if (res.output !== undefined) process.stdout.write(res.output);
|
|
66
|
+
if (res.exited) console.log(`\n[exited ${res.exitCode}]`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function send(name: string, text: string) {
|
|
70
|
+
const sock = socketPath(name);
|
|
71
|
+
await sendMessage(sock, { action: "send", data: text });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function stop(name: string) {
|
|
75
|
+
const sock = socketPath(name);
|
|
76
|
+
await sendMessage(sock, { action: "stop" });
|
|
77
|
+
console.log(`session '${name}' stopped`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function list() {
|
|
81
|
+
const { readdirSync } = await import("node:fs");
|
|
82
|
+
ensureSessionsDir();
|
|
83
|
+
const files = readdirSync((await import("./paths")).SESSIONS_DIR);
|
|
84
|
+
const sessions = files.filter(f => f.endsWith(".sock")).map(f => f.replace(".sock", ""));
|
|
85
|
+
|
|
86
|
+
if (sessions.length === 0) {
|
|
87
|
+
console.log("no active sessions");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const name of sessions) {
|
|
92
|
+
const sock = socketPath(name);
|
|
93
|
+
try {
|
|
94
|
+
const res = await sendMessage(sock, { action: "status" });
|
|
95
|
+
const status = res.running ? "running" : `exited (${res.exitCode})`;
|
|
96
|
+
console.log(`${name} [${status}] pid=${res.pid}`);
|
|
97
|
+
} catch {
|
|
98
|
+
console.log(`${name} [dead]`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function main() {
|
|
104
|
+
const args = process.argv.slice(2);
|
|
105
|
+
|
|
106
|
+
if (args[0] === "__daemon__") {
|
|
107
|
+
const { runDaemon } = await import("./daemon");
|
|
108
|
+
return runDaemon(args[1], args[2], args.slice(3));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cmd = args[0];
|
|
112
|
+
|
|
113
|
+
switch (cmd) {
|
|
114
|
+
case "start": {
|
|
115
|
+
const name = args[1];
|
|
116
|
+
if (!name) { console.error("usage: noninteractive start <name> [args...]"); process.exit(1); }
|
|
117
|
+
return start(name, args.slice(2));
|
|
118
|
+
}
|
|
119
|
+
case "read": {
|
|
120
|
+
const name = args[1];
|
|
121
|
+
if (!name) { console.error("usage: noninteractive read <name>"); process.exit(1); }
|
|
122
|
+
return read(name);
|
|
123
|
+
}
|
|
124
|
+
case "send": {
|
|
125
|
+
const name = args[1];
|
|
126
|
+
const text = args[2];
|
|
127
|
+
if (!name || text === undefined) { console.error("usage: noninteractive send <name> <text>"); process.exit(1); }
|
|
128
|
+
return send(name, text);
|
|
129
|
+
}
|
|
130
|
+
case "stop": {
|
|
131
|
+
const name = args[1];
|
|
132
|
+
if (!name) { console.error("usage: noninteractive stop <name>"); process.exit(1); }
|
|
133
|
+
return stop(name);
|
|
134
|
+
}
|
|
135
|
+
case "list":
|
|
136
|
+
case "ls":
|
|
137
|
+
return list();
|
|
138
|
+
default:
|
|
139
|
+
console.log(HELP);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main().catch((err) => {
|
|
144
|
+
console.error(err.message);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
});
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
export const SESSIONS_DIR = resolve(process.env.HOME!, ".noninteractive", "sessions");
|
|
5
|
+
|
|
6
|
+
export function ensureSessionsDir() {
|
|
7
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function socketPath(name: string) {
|
|
11
|
+
return resolve(SESSIONS_DIR, `${name}.sock`);
|
|
12
|
+
}
|
package/src/ptybridge.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PTY bridge: spawns a command in a real PTY, pipes stdin/stdout."""
|
|
3
|
+
import pty, os, sys, select, fcntl, termios, struct
|
|
4
|
+
|
|
5
|
+
master, slave = pty.openpty()
|
|
6
|
+
|
|
7
|
+
# set terminal size so TUI libraries render correctly
|
|
8
|
+
winsize = struct.pack("HHHH", 24, 80, 0, 0)
|
|
9
|
+
fcntl.ioctl(slave, termios.TIOCSWINSZ, winsize)
|
|
10
|
+
|
|
11
|
+
pid = os.fork()
|
|
12
|
+
if pid == 0:
|
|
13
|
+
os.close(master)
|
|
14
|
+
os.setsid()
|
|
15
|
+
# make slave the controlling terminal
|
|
16
|
+
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
|
|
17
|
+
os.dup2(slave, 0)
|
|
18
|
+
os.dup2(slave, 1)
|
|
19
|
+
os.dup2(slave, 2)
|
|
20
|
+
os.close(slave)
|
|
21
|
+
os.execvp(sys.argv[1], sys.argv[1:])
|
|
22
|
+
|
|
23
|
+
os.close(slave)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
while True:
|
|
27
|
+
fds = [master]
|
|
28
|
+
try:
|
|
29
|
+
fds.append(sys.stdin.fileno())
|
|
30
|
+
except ValueError:
|
|
31
|
+
pass
|
|
32
|
+
r, _, _ = select.select(fds, [], [], 1.0)
|
|
33
|
+
if sys.stdin.fileno() in r:
|
|
34
|
+
data = os.read(sys.stdin.fileno(), 4096)
|
|
35
|
+
if not data:
|
|
36
|
+
continue
|
|
37
|
+
os.write(master, data)
|
|
38
|
+
if master in r:
|
|
39
|
+
data = os.read(master, 4096)
|
|
40
|
+
if not data:
|
|
41
|
+
break
|
|
42
|
+
os.write(sys.stdout.fileno(), data)
|
|
43
|
+
except OSError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
_, status = os.waitpid(pid, 0)
|
|
47
|
+
sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|