noninteractive 0.3.4 → 0.3.6
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 +11 -3
- package/package.json +3 -1
- package/src/client.ts +42 -29
- package/src/daemon.ts +139 -118
- package/src/index.ts +270 -187
- package/src/paths.ts +5 -4
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npx noninteractive
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# Start a session (runs `npx workos` in a background PTY)
|
|
17
|
-
npx noninteractive
|
|
17
|
+
npx noninteractive workos
|
|
18
18
|
|
|
19
19
|
# Read what's on screen
|
|
20
20
|
npx noninteractive read workos
|
|
@@ -36,7 +36,7 @@ npx noninteractive list
|
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
# Start the installer
|
|
39
|
-
npx noninteractive
|
|
39
|
+
npx noninteractive workos
|
|
40
40
|
|
|
41
41
|
# Wait for it to load, then read the prompt
|
|
42
42
|
npx noninteractive read workos
|
|
@@ -62,9 +62,17 @@ npx noninteractive send workos ""
|
|
|
62
62
|
npx noninteractive stop workos
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Agent Skill
|
|
66
|
+
|
|
67
|
+
Install the [Agent Skill](https://agentskills.io) so your AI agent knows how to use noninteractive:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx skills add https://noninteractive.org
|
|
71
|
+
```
|
|
72
|
+
|
|
65
73
|
## How it works
|
|
66
74
|
|
|
67
|
-
1. `
|
|
75
|
+
1. `npx noninteractive <tool>` spawns a detached daemon that runs `npx <tool>` inside a real pseudo-terminal (PTY)
|
|
68
76
|
2. The daemon listens on a unix socket at `~/.noninteractive/sessions/<name>.sock`
|
|
69
77
|
3. `read`, `send`, and `stop` connect to that socket to interact with the running process
|
|
70
78
|
4. The PTY ensures the child process sees a real terminal — `isTTY` is true, ANSI colors work, interactive menus render correctly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noninteractive",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"noninteractive": "./src/index.ts"
|
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
"dev": "bun run src/index.ts",
|
|
14
14
|
"build": "bun build src/index.ts --compile --outfile bin/noninteractive",
|
|
15
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 .",
|
|
16
|
+
"check": "biome check --write src/",
|
|
16
17
|
"test": "bun test"
|
|
17
18
|
},
|
|
18
19
|
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "^2.4.5",
|
|
19
21
|
"@types/bun": "latest"
|
|
20
22
|
},
|
|
21
23
|
"peerDependencies": {
|
package/src/client.ts
CHANGED
|
@@ -1,37 +1,50 @@
|
|
|
1
1
|
import { createConnection } from "node:net";
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export interface DaemonResponse {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
output?: string;
|
|
6
|
+
exited?: boolean;
|
|
7
|
+
exitCode?: number | null;
|
|
8
|
+
running?: boolean;
|
|
9
|
+
pid?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sendMessage(
|
|
14
|
+
sockPath: string,
|
|
15
|
+
msg: Record<string, unknown>,
|
|
16
|
+
): Promise<DaemonResponse> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const socket = createConnection(sockPath);
|
|
19
|
+
let data = "";
|
|
7
20
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
socket.on("connect", () => {
|
|
22
|
+
socket.write(JSON.stringify(msg));
|
|
23
|
+
});
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
25
|
+
socket.on("data", (chunk) => {
|
|
26
|
+
data += chunk.toString();
|
|
27
|
+
});
|
|
15
28
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
socket.on("end", () => {
|
|
30
|
+
try {
|
|
31
|
+
resolve(JSON.parse(data));
|
|
32
|
+
} catch {
|
|
33
|
+
reject(new Error("invalid response from daemon"));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
socket.on("error", (err: NodeJS.ErrnoException) => {
|
|
38
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
39
|
+
reject(new Error("session not found"));
|
|
40
|
+
} else {
|
|
41
|
+
reject(err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
31
44
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
socket.destroy();
|
|
47
|
+
reject(new Error("connection timeout"));
|
|
48
|
+
}, 5000);
|
|
49
|
+
});
|
|
37
50
|
}
|
package/src/daemon.ts
CHANGED
|
@@ -1,125 +1,146 @@
|
|
|
1
|
-
import { createServer } from "node:net";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
3
2
|
import { unlinkSync } from "node:fs";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { createServer, type Socket } from "node:net";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { ensureSessionsDir, socketPath } from "./paths";
|
|
6
|
+
|
|
7
|
+
interface DaemonMessage {
|
|
8
|
+
action: "read" | "send" | "stop" | "status";
|
|
9
|
+
data?: string;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
function getPtyBridge(): string {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
const arch = process.arch;
|
|
15
|
+
const binaryName = `ptybridge-${platform}-${arch}`;
|
|
16
|
+
|
|
17
|
+
const candidates = [
|
|
18
|
+
resolve(dirname(process.argv[1] || process.execPath), "native", binaryName),
|
|
19
|
+
resolve(dirname(import.meta.dirname), "native", binaryName),
|
|
20
|
+
resolve(import.meta.dirname, "..", "native", binaryName),
|
|
21
|
+
];
|
|
22
|
+
for (const p of candidates) {
|
|
23
|
+
try {
|
|
24
|
+
const { statSync } = require("node:fs");
|
|
25
|
+
if (statSync(p).isFile()) return p;
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
return candidates[0];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
export function runDaemon(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
31
|
+
export function runDaemon(
|
|
32
|
+
sessionName: string,
|
|
33
|
+
executable: string,
|
|
34
|
+
args: string[],
|
|
35
|
+
) {
|
|
36
|
+
ensureSessionsDir();
|
|
37
|
+
const sock = socketPath(sessionName);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(sock);
|
|
41
|
+
} catch {}
|
|
42
|
+
|
|
43
|
+
let outputBuffer = "";
|
|
44
|
+
let processExited = false;
|
|
45
|
+
let exitCode: number | null = null;
|
|
46
|
+
|
|
47
|
+
const ptyBridge = getPtyBridge();
|
|
48
|
+
const proc = spawn(ptyBridge, [executable, ...args], {
|
|
49
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
50
|
+
env: { ...process.env, TERM: "xterm-256color" },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const { stdout, stderr, stdin } = proc;
|
|
54
|
+
|
|
55
|
+
stdout?.on("data", (chunk: Buffer) => {
|
|
56
|
+
outputBuffer += chunk.toString();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
stderr?.on("data", (chunk: Buffer) => {
|
|
60
|
+
outputBuffer += chunk.toString();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
proc.on("exit", (code) => {
|
|
64
|
+
processExited = true;
|
|
65
|
+
exitCode = code;
|
|
66
|
+
outputBuffer += `\n[exited ${code}]`;
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
server.close();
|
|
70
|
+
try {
|
|
71
|
+
unlinkSync(sock);
|
|
72
|
+
} catch {}
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}, 60_000);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
proc.on("error", (err) => {
|
|
78
|
+
outputBuffer += `\n[error: ${err.message}]`;
|
|
79
|
+
processExited = true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const server = createServer((socket) => {
|
|
83
|
+
let buf = "";
|
|
84
|
+
|
|
85
|
+
socket.on("data", (chunk) => {
|
|
86
|
+
buf += chunk.toString();
|
|
87
|
+
try {
|
|
88
|
+
const msg = JSON.parse(buf);
|
|
89
|
+
buf = "";
|
|
90
|
+
handle(msg, socket);
|
|
91
|
+
} catch {}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
function handle(msg: DaemonMessage, socket: Socket) {
|
|
96
|
+
switch (msg.action) {
|
|
97
|
+
case "read":
|
|
98
|
+
socket.end(
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
ok: true,
|
|
101
|
+
output: outputBuffer,
|
|
102
|
+
exited: processExited,
|
|
103
|
+
exitCode,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case "send":
|
|
109
|
+
if (processExited) {
|
|
110
|
+
socket.end(JSON.stringify({ ok: false, error: "process exited" }));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
stdin?.write(`${msg.data}\r`);
|
|
114
|
+
socket.end(JSON.stringify({ ok: true }));
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case "stop":
|
|
118
|
+
proc.kill("SIGTERM");
|
|
119
|
+
socket.end(JSON.stringify({ ok: true }));
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
server.close();
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(sock);
|
|
124
|
+
} catch {}
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}, 500);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case "status":
|
|
130
|
+
socket.end(
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
ok: true,
|
|
133
|
+
running: !processExited,
|
|
134
|
+
pid: proc.pid,
|
|
135
|
+
exitCode,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
socket.end(JSON.stringify({ ok: false, error: "unknown action" }));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
server.listen(sock);
|
|
125
146
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,232 +2,315 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { socketPath, ensureSessionsDir } from "./paths";
|
|
6
5
|
import { sendMessage } from "./client";
|
|
6
|
+
import { ensureSessionsDir, socketPath } from "./paths";
|
|
7
7
|
|
|
8
8
|
const HELP = `noninteractive — run interactive CLI commands non-interactively.
|
|
9
9
|
|
|
10
|
-
usage: npx noninteractive <
|
|
10
|
+
usage: npx noninteractive <tool> [args...]
|
|
11
11
|
|
|
12
12
|
commands:
|
|
13
|
-
|
|
13
|
+
<tool> [args...] start a session (runs npx <tool> in a PTY)
|
|
14
14
|
read <session> read current terminal output
|
|
15
15
|
send <session> <text> send keystrokes (use "" for Enter)
|
|
16
16
|
stop <session> stop a session
|
|
17
17
|
list show active sessions
|
|
18
|
+
start <cmd> [args...] explicit start (for non-npx commands)
|
|
18
19
|
|
|
19
|
-
the
|
|
20
|
-
the session name is auto-derived from the command (e.g. "npx vercel" → session "vercel").
|
|
20
|
+
the session name is auto-derived from the tool (e.g. "workos" → session "workos").
|
|
21
21
|
|
|
22
22
|
example workflow:
|
|
23
|
-
npx noninteractive
|
|
24
|
-
npx noninteractive read
|
|
25
|
-
npx noninteractive send
|
|
26
|
-
npx noninteractive send
|
|
27
|
-
npx noninteractive read
|
|
28
|
-
npx noninteractive stop
|
|
23
|
+
npx noninteractive workos # starts "npx workos", session = "workos"
|
|
24
|
+
npx noninteractive read workos # see what's on screen
|
|
25
|
+
npx noninteractive send workos "" # press Enter
|
|
26
|
+
npx noninteractive send workos "y" # type "y" and press Enter
|
|
27
|
+
npx noninteractive read workos # see updated output
|
|
28
|
+
npx noninteractive stop workos # done, stop the session
|
|
29
29
|
|
|
30
30
|
more examples:
|
|
31
|
-
npx noninteractive
|
|
32
|
-
npx noninteractive
|
|
33
|
-
npx noninteractive start
|
|
31
|
+
npx noninteractive vercel # session "vercel"
|
|
32
|
+
npx noninteractive supabase init # session "supabase"
|
|
33
|
+
npx noninteractive start vercel login # explicit start for non-npx commands`;
|
|
34
34
|
|
|
35
35
|
function getSelfCommand(): string[] {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
if (process.argv[1] && /\.(ts|js)$/.test(process.argv[1])) {
|
|
37
|
+
return [process.argv[0], process.argv[1]];
|
|
38
|
+
}
|
|
39
|
+
return [process.argv[0]];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function deriveSessionName(cmd: string, args: string[]): string {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
const parts = [cmd, ...args];
|
|
44
|
+
// skip npx/bunx prefix to get the real command name
|
|
45
|
+
let i = 0;
|
|
46
|
+
if (parts[i] === "npx" || parts[i] === "bunx") i++;
|
|
47
|
+
// skip flags like -y, --yes
|
|
48
|
+
while (i < parts.length && parts[i].startsWith("-")) i++;
|
|
49
|
+
const name = parts[i] || cmd;
|
|
50
|
+
// strip npm scope @foo/bar -> bar
|
|
51
|
+
return name.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9_-]/g, "");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async function start(cmdArgs: string[]) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
55
|
+
const executable = cmdArgs[0];
|
|
56
|
+
const args = cmdArgs.slice(1);
|
|
57
|
+
const name = deriveSessionName(executable, args);
|
|
58
|
+
const sock = socketPath(name);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
62
|
+
if (res.ok) {
|
|
63
|
+
process.stdout.write(res.output ?? "");
|
|
64
|
+
if (res.exited) {
|
|
65
|
+
console.log(
|
|
66
|
+
`\n[session '${name}' already exists but exited ${res.exitCode} — stopping it]`,
|
|
67
|
+
);
|
|
68
|
+
try {
|
|
69
|
+
await sendMessage(sock, { action: "stop" });
|
|
70
|
+
} catch {}
|
|
71
|
+
// fall through to start a new session
|
|
72
|
+
} else {
|
|
73
|
+
console.log(
|
|
74
|
+
`\n[session '${name}' already running — read the output above, then use:]`,
|
|
75
|
+
);
|
|
76
|
+
console.log(
|
|
77
|
+
` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`,
|
|
78
|
+
);
|
|
79
|
+
console.log(
|
|
80
|
+
` npx noninteractive read ${name} # read updated output`,
|
|
81
|
+
);
|
|
82
|
+
console.log(
|
|
83
|
+
` npx noninteractive stop ${name} # stop the session`,
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {}
|
|
89
|
+
|
|
90
|
+
ensureSessionsDir();
|
|
91
|
+
try {
|
|
92
|
+
const { unlinkSync } = await import("node:fs");
|
|
93
|
+
unlinkSync(sock);
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
const self = getSelfCommand();
|
|
97
|
+
const child = spawn(
|
|
98
|
+
self[0],
|
|
99
|
+
[...self.slice(1), "__daemon__", name, executable, ...args],
|
|
100
|
+
{
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: "ignore",
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
child.unref();
|
|
106
|
+
|
|
107
|
+
// wait for socket to appear
|
|
108
|
+
for (let i = 0; i < 50; i++) {
|
|
109
|
+
if (existsSync(sock)) break;
|
|
110
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!existsSync(sock)) {
|
|
114
|
+
console.error(`error: failed to start session '${name}'.`);
|
|
115
|
+
console.error(`the command was: ${executable} ${args.join(" ")}`);
|
|
116
|
+
console.error(`\nmake sure the command exists. examples:`);
|
|
117
|
+
console.error(
|
|
118
|
+
` npx noninteractive start npx vercel # run an npx package`,
|
|
119
|
+
);
|
|
120
|
+
console.error(
|
|
121
|
+
` npx noninteractive start vercel login # run a command directly`,
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// poll until we get meaningful output (up to 10s)
|
|
127
|
+
const stripAnsi = (s: string) =>
|
|
128
|
+
s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, "");
|
|
129
|
+
for (let i = 0; i < 50; i++) {
|
|
130
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
131
|
+
try {
|
|
132
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
133
|
+
const clean = stripAnsi(res.output ?? "").trim();
|
|
134
|
+
if (clean.length > 10) {
|
|
135
|
+
process.stdout.write(res.output);
|
|
136
|
+
if (res.exited) {
|
|
137
|
+
console.log(
|
|
138
|
+
`\n[session '${name}' exited ${res.exitCode} — the command failed]`,
|
|
139
|
+
);
|
|
140
|
+
console.log(
|
|
141
|
+
`hint: the first argument to "start" is the command to run, NOT a session name.`,
|
|
142
|
+
);
|
|
143
|
+
console.log(
|
|
144
|
+
` npx noninteractive start npx vercel # run an npx package`,
|
|
145
|
+
);
|
|
146
|
+
console.log(
|
|
147
|
+
` npx noninteractive start vercel login # run a command directly`,
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(
|
|
151
|
+
`\n[session '${name}' started — read the output above, then use:]`,
|
|
152
|
+
);
|
|
153
|
+
console.log(
|
|
154
|
+
` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`,
|
|
155
|
+
);
|
|
156
|
+
console.log(
|
|
157
|
+
` npx noninteractive read ${name} # read updated output`,
|
|
158
|
+
);
|
|
159
|
+
console.log(
|
|
160
|
+
` npx noninteractive stop ${name} # stop the session`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (res.exited) {
|
|
166
|
+
process.stdout.write(res.output ?? "");
|
|
167
|
+
console.log(
|
|
168
|
+
`\n[session '${name}' exited ${res.exitCode} — the command failed]`,
|
|
169
|
+
);
|
|
170
|
+
console.log(
|
|
171
|
+
`hint: the first argument to "start" is the command to run, NOT a session name.`,
|
|
172
|
+
);
|
|
173
|
+
console.log(
|
|
174
|
+
` npx noninteractive start npx vercel # run an npx package`,
|
|
175
|
+
);
|
|
176
|
+
console.log(
|
|
177
|
+
` npx noninteractive start vercel login # run a command directly`,
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`[session '${name}' started but no output yet — use:]`);
|
|
185
|
+
console.log(` npx noninteractive read ${name} # read output`);
|
|
186
|
+
console.log(
|
|
187
|
+
` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`,
|
|
188
|
+
);
|
|
189
|
+
console.log(
|
|
190
|
+
` npx noninteractive stop ${name} # stop the session`,
|
|
191
|
+
);
|
|
140
192
|
}
|
|
141
193
|
|
|
142
194
|
async function read(name: string) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
const sock = socketPath(name);
|
|
196
|
+
const res = await sendMessage(sock, { action: "read" });
|
|
197
|
+
if (res.output !== undefined) process.stdout.write(res.output);
|
|
198
|
+
if (res.exited) console.log(`\n[exited ${res.exitCode}]`);
|
|
147
199
|
}
|
|
148
200
|
|
|
149
201
|
async function send(name: string, text: string) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
const sock = socketPath(name);
|
|
203
|
+
await sendMessage(sock, { action: "send", data: text });
|
|
204
|
+
console.log(
|
|
205
|
+
`[sent to '${name}' — run "npx noninteractive read ${name}" to see the result]`,
|
|
206
|
+
);
|
|
153
207
|
}
|
|
154
208
|
|
|
155
209
|
async function stop(name: string) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
210
|
+
const sock = socketPath(name);
|
|
211
|
+
await sendMessage(sock, { action: "stop" });
|
|
212
|
+
console.log(`session '${name}' stopped`);
|
|
159
213
|
}
|
|
160
214
|
|
|
161
215
|
async function list() {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
216
|
+
const { readdirSync } = await import("node:fs");
|
|
217
|
+
ensureSessionsDir();
|
|
218
|
+
const files = readdirSync((await import("./paths")).SESSIONS_DIR);
|
|
219
|
+
const sessions = files
|
|
220
|
+
.filter((f) => f.endsWith(".sock"))
|
|
221
|
+
.map((f) => f.replace(".sock", ""));
|
|
222
|
+
|
|
223
|
+
if (sessions.length === 0) {
|
|
224
|
+
console.log("no active sessions");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const name of sessions) {
|
|
229
|
+
const sock = socketPath(name);
|
|
230
|
+
try {
|
|
231
|
+
const res = await sendMessage(sock, { action: "status" });
|
|
232
|
+
const status = res.running ? "running" : `exited (${res.exitCode})`;
|
|
233
|
+
console.log(`${name} [${status}] pid=${res.pid}`);
|
|
234
|
+
} catch {
|
|
235
|
+
console.log(`${name} [dead]`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
182
238
|
}
|
|
183
239
|
|
|
184
240
|
async function main() {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
const args = process.argv.slice(2);
|
|
242
|
+
|
|
243
|
+
if (args[0] === "__daemon__") {
|
|
244
|
+
const { runDaemon } = await import("./daemon");
|
|
245
|
+
return runDaemon(args[1], args[2], args.slice(3));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const cmd = args[0];
|
|
249
|
+
|
|
250
|
+
switch (cmd) {
|
|
251
|
+
case "start": {
|
|
252
|
+
if (args.length < 2) {
|
|
253
|
+
console.error(
|
|
254
|
+
"usage: noninteractive start <cmd> [args...]\n\nexample: npx noninteractive start npx vercel",
|
|
255
|
+
);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
return start(args.slice(1));
|
|
259
|
+
}
|
|
260
|
+
case "read": {
|
|
261
|
+
const name = args[1];
|
|
262
|
+
if (!name) {
|
|
263
|
+
console.error(
|
|
264
|
+
"usage: noninteractive read <session>\n\nexample: npx noninteractive read vercel",
|
|
265
|
+
);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
return read(name);
|
|
269
|
+
}
|
|
270
|
+
case "send": {
|
|
271
|
+
const name = args[1];
|
|
272
|
+
const text = args[2];
|
|
273
|
+
if (!name || text === undefined) {
|
|
274
|
+
console.error(
|
|
275
|
+
'usage: noninteractive send <session> <text>\n\nexample: npx noninteractive send vercel "y"',
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
return send(name, text);
|
|
280
|
+
}
|
|
281
|
+
case "stop": {
|
|
282
|
+
const name = args[1];
|
|
283
|
+
if (!name) {
|
|
284
|
+
console.error(
|
|
285
|
+
"usage: noninteractive stop <session>\n\nexample: npx noninteractive stop vercel",
|
|
286
|
+
);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
return stop(name);
|
|
290
|
+
}
|
|
291
|
+
case "list":
|
|
292
|
+
case "ls":
|
|
293
|
+
return list();
|
|
294
|
+
case "version":
|
|
295
|
+
case "--version":
|
|
296
|
+
case "-v": {
|
|
297
|
+
const { version } = require("../package.json");
|
|
298
|
+
console.log(`noninteractive v${version}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
case undefined:
|
|
302
|
+
case "help":
|
|
303
|
+
case "--help":
|
|
304
|
+
case "-h":
|
|
305
|
+
console.log(HELP);
|
|
306
|
+
break;
|
|
307
|
+
default:
|
|
308
|
+
// treat unknown commands as: start npx <args>
|
|
309
|
+
return start(["npx", ...args]);
|
|
310
|
+
}
|
|
228
311
|
}
|
|
229
312
|
|
|
230
313
|
main().catch((err) => {
|
|
231
|
-
|
|
232
|
-
|
|
314
|
+
console.error(err.message);
|
|
315
|
+
process.exit(1);
|
|
233
316
|
});
|
package/src/paths.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
3
4
|
|
|
4
|
-
export const SESSIONS_DIR = resolve(
|
|
5
|
+
export const SESSIONS_DIR = resolve(homedir(), ".noninteractive", "sessions");
|
|
5
6
|
|
|
6
7
|
export function ensureSessionsDir() {
|
|
7
|
-
|
|
8
|
+
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function socketPath(name: string) {
|
|
11
|
-
|
|
12
|
+
return resolve(SESSIONS_DIR, `${name}.sock`);
|
|
12
13
|
}
|