noninteractive 0.1.2 → 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.
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "noninteractive",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "noninteractive": "./src/index.ts"
7
7
  },
8
+ "files": [
9
+ "src/**/*",
10
+ "native/**/*"
11
+ ],
8
12
  "scripts": {
9
13
  "dev": "bun run src/index.ts",
10
14
  "build": "bun build src/index.ts --compile --outfile bin/noninteractive",
15
+ "build:pty": "cd ptybridge && GOOS=darwin GOARCH=arm64 go build -o ../native/ptybridge-darwin-arm64 . && GOOS=darwin GOARCH=amd64 go build -o ../native/ptybridge-darwin-amd64 . && GOOS=linux GOARCH=amd64 go build -o ../native/ptybridge-linux-amd64 . && GOOS=linux GOARCH=arm64 go build -o ../native/ptybridge-linux-arm64 .",
11
16
  "test": "bun test"
12
17
  },
13
18
  "devDependencies": {
package/src/daemon.ts CHANGED
@@ -5,12 +5,14 @@ import { resolve, dirname } from "node:path";
5
5
  import { socketPath, ensureSessionsDir } from "./paths";
6
6
 
7
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
8
+ const platform = process.platform;
9
+ const arch = process.arch;
10
+ const binaryName = `ptybridge-${platform}-${arch}`;
11
+
10
12
  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"),
13
+ resolve(dirname(process.argv[1] || process.execPath), "native", binaryName),
14
+ resolve(dirname(import.meta.dirname), "native", binaryName),
15
+ resolve(import.meta.dirname, "..", "native", binaryName),
14
16
  ];
15
17
  for (const p of candidates) {
16
18
  try {
@@ -18,7 +20,7 @@ function getPtyBridge(): string {
18
20
  if (statSync(p).isFile()) return p;
19
21
  } catch {}
20
22
  }
21
- return resolve(import.meta.dirname, "ptybridge.py");
23
+ return candidates[0];
22
24
  }
23
25
 
24
26
  export function runDaemon(sessionName: string, executable: string, args: string[]) {
@@ -32,7 +34,7 @@ export function runDaemon(sessionName: string, executable: string, args: string[
32
34
  let exitCode: number | null = null;
33
35
 
34
36
  const ptyBridge = getPtyBridge();
35
- const proc = spawn("python3", [ptyBridge, executable, ...args], {
37
+ const proc = spawn(ptyBridge, [executable, ...args], {
36
38
  stdio: ["pipe", "pipe", "pipe"],
37
39
  env: { ...process.env, TERM: "xterm-256color" },
38
40
  });
package/src/index.ts CHANGED
@@ -42,19 +42,39 @@ async function start(name: string, args: string[]) {
42
42
  });
43
43
  child.unref();
44
44
 
45
+ // wait for socket to appear
45
46
  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
- }
47
+ if (existsSync(sock)) break;
55
48
  await new Promise(r => setTimeout(r, 100));
56
49
  }
57
50
 
51
+ if (!existsSync(sock)) {
52
+ console.error("timeout: failed to start session");
53
+ process.exit(1);
54
+ }
55
+
56
+ // poll until we get meaningful output (up to 10s)
57
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, "");
58
+ for (let i = 0; i < 50; i++) {
59
+ await new Promise(r => setTimeout(r, 200));
60
+ try {
61
+ const res = await sendMessage(sock, { action: "read" });
62
+ const clean = stripAnsi(res.output ?? "").trim();
63
+ if (clean.length > 10) {
64
+ process.stdout.write(res.output);
65
+ console.log(`\n[session '${name}' started]`);
66
+ return;
67
+ }
68
+ if (res.exited) {
69
+ process.stdout.write(res.output ?? "");
70
+ console.log(`\n[session '${name}' exited ${res.exitCode}]`);
71
+ return;
72
+ }
73
+ } catch {}
74
+ }
75
+
76
+ console.log(`[session '${name}' started]`);
77
+
58
78
  console.error("timeout: failed to start session");
59
79
  process.exit(1);
60
80
  }
package/CLAUDE.md DELETED
@@ -1,144 +0,0 @@
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/index.ts DELETED
@@ -1 +0,0 @@
1
- console.log("Hello via Bun!");
package/prompt.md DELETED
@@ -1,11 +0,0 @@
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/ptybridge.py DELETED
@@ -1,47 +0,0 @@
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 DELETED
@@ -1,29 +0,0 @@
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
- }