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 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 start workos
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 start workos
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. `start` spawns a detached daemon process that runs the target command inside a real pseudo-terminal (PTY)
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.4",
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 function sendMessage(sockPath: string, msg: Record<string, unknown>): Promise<any> {
4
- return new Promise((resolve, reject) => {
5
- const socket = createConnection(sockPath);
6
- let data = "";
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
- socket.on("connect", () => {
9
- socket.write(JSON.stringify(msg));
10
- });
21
+ socket.on("connect", () => {
22
+ socket.write(JSON.stringify(msg));
23
+ });
11
24
 
12
- socket.on("data", (chunk) => {
13
- data += chunk.toString();
14
- });
25
+ socket.on("data", (chunk) => {
26
+ data += chunk.toString();
27
+ });
15
28
 
16
- socket.on("end", () => {
17
- try {
18
- resolve(JSON.parse(data));
19
- } catch {
20
- reject(new Error("invalid response from daemon"));
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
- 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
- });
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
- setTimeout(() => {
33
- socket.destroy();
34
- reject(new Error("connection timeout"));
35
- }, 5000);
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 { resolve, dirname } from "node:path";
5
- import { socketPath, ensureSessionsDir } from "./paths";
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
- const platform = process.platform;
9
- const arch = process.arch;
10
- const binaryName = `ptybridge-${platform}-${arch}`;
11
-
12
- const candidates = [
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),
16
- ];
17
- for (const p of candidates) {
18
- try {
19
- const { statSync } = require("node:fs");
20
- if (statSync(p).isFile()) return p;
21
- } catch {}
22
- }
23
- return candidates[0];
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(sessionName: string, executable: string, args: string[]) {
27
- ensureSessionsDir();
28
- const sock = socketPath(sessionName);
29
-
30
- try { unlinkSync(sock); } catch {}
31
-
32
- let outputBuffer = "";
33
- let processExited = false;
34
- let exitCode: number | null = null;
35
-
36
- const ptyBridge = getPtyBridge();
37
- const proc = spawn(ptyBridge, [executable, ...args], {
38
- stdio: ["pipe", "pipe", "pipe"],
39
- env: { ...process.env, TERM: "xterm-256color" },
40
- });
41
-
42
- proc.stdout!.on("data", (chunk: Buffer) => {
43
- outputBuffer += chunk.toString();
44
- });
45
-
46
- proc.stderr!.on("data", (chunk: Buffer) => {
47
- outputBuffer += chunk.toString();
48
- });
49
-
50
- proc.on("exit", (code) => {
51
- processExited = true;
52
- exitCode = code;
53
- outputBuffer += `\n[exited ${code}]`;
54
-
55
- setTimeout(() => {
56
- server.close();
57
- try { unlinkSync(sock); } catch {}
58
- process.exit(0);
59
- }, 60_000);
60
- });
61
-
62
- proc.on("error", (err) => {
63
- outputBuffer += `\n[error: ${err.message}]`;
64
- processExited = true;
65
- });
66
-
67
- const server = createServer((socket) => {
68
- let buf = "";
69
-
70
- socket.on("data", (chunk) => {
71
- buf += chunk.toString();
72
- try {
73
- const msg = JSON.parse(buf);
74
- buf = "";
75
- handle(msg, socket);
76
- } catch {}
77
- });
78
- });
79
-
80
- function handle(msg: any, socket: any) {
81
- switch (msg.action) {
82
- case "read":
83
- socket.end(JSON.stringify({
84
- ok: true,
85
- output: outputBuffer,
86
- exited: processExited,
87
- exitCode,
88
- }));
89
- break;
90
-
91
- case "send":
92
- if (processExited) {
93
- socket.end(JSON.stringify({ ok: false, error: "process exited" }));
94
- break;
95
- }
96
- proc.stdin!.write(msg.data + "\r");
97
- socket.end(JSON.stringify({ ok: true }));
98
- break;
99
-
100
- case "stop":
101
- proc.kill("SIGTERM");
102
- socket.end(JSON.stringify({ ok: true }));
103
- setTimeout(() => {
104
- server.close();
105
- try { unlinkSync(sock); } catch {}
106
- process.exit(0);
107
- }, 500);
108
- break;
109
-
110
- case "status":
111
- socket.end(JSON.stringify({
112
- ok: true,
113
- running: !processExited,
114
- pid: proc.pid,
115
- exitCode,
116
- }));
117
- break;
118
-
119
- default:
120
- socket.end(JSON.stringify({ ok: false, error: "unknown action" }));
121
- }
122
- }
123
-
124
- server.listen(sock);
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 <command> [args]
10
+ usage: npx noninteractive <tool> [args...]
11
11
 
12
12
  commands:
13
- start <cmd> [args...] start a session running <cmd>
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 first argument to "start" is the command to run, NOT a session name.
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 start npx vercel # starts "npx vercel", session name = "vercel"
24
- npx noninteractive read vercel # see what's on screen
25
- npx noninteractive send vercel "" # press Enter
26
- npx noninteractive send vercel "y" # type "y" and press Enter
27
- npx noninteractive read vercel # see updated output
28
- npx noninteractive stop vercel # done, stop the session
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 start npx workos # session "workos"
32
- npx noninteractive start vercel login # session "vercel"
33
- npx noninteractive start npx supabase init # session "supabase"`;
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
- if (process.argv[1] && /\.(ts|js)$/.test(process.argv[1])) {
37
- return [process.argv[0], process.argv[1]];
38
- }
39
- return [process.argv[0]];
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
- 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, "");
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
- 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(`\n[session '${name}' already exists but exited ${res.exitCode} — stopping it]`);
66
- try { await sendMessage(sock, { action: "stop" }); } catch {}
67
- // fall through to start a new session
68
- } else {
69
- console.log(`\n[session '${name}' already running — read the output above, then use:]`);
70
- console.log(` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`);
71
- console.log(` npx noninteractive read ${name} # read updated output`);
72
- console.log(` npx noninteractive stop ${name} # stop the session`);
73
- return;
74
- }
75
- }
76
- } catch {}
77
-
78
- ensureSessionsDir();
79
- try { const { unlinkSync } = await import("node:fs"); unlinkSync(sock); } catch {}
80
-
81
- const self = getSelfCommand();
82
- const child = spawn(self[0], [...self.slice(1), "__daemon__", name, executable, ...args], {
83
- detached: true,
84
- stdio: "ignore",
85
- });
86
- child.unref();
87
-
88
- // wait for socket to appear
89
- for (let i = 0; i < 50; i++) {
90
- if (existsSync(sock)) break;
91
- await new Promise(r => setTimeout(r, 100));
92
- }
93
-
94
- if (!existsSync(sock)) {
95
- console.error(`error: failed to start session '${name}'.`);
96
- console.error(`the command was: ${executable} ${args.join(" ")}`);
97
- console.error(`\nmake sure the command exists. examples:`);
98
- console.error(` npx noninteractive start npx vercel # run an npx package`);
99
- console.error(` npx noninteractive start vercel login # run a command directly`);
100
- process.exit(1);
101
- }
102
-
103
- // poll until we get meaningful output (up to 10s)
104
- const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, "");
105
- for (let i = 0; i < 50; i++) {
106
- await new Promise(r => setTimeout(r, 200));
107
- try {
108
- const res = await sendMessage(sock, { action: "read" });
109
- const clean = stripAnsi(res.output ?? "").trim();
110
- if (clean.length > 10) {
111
- process.stdout.write(res.output);
112
- if (res.exited) {
113
- console.log(`\n[session '${name}' exited ${res.exitCode} — the command failed]`);
114
- console.log(`hint: the first argument to "start" is the command to run, NOT a session name.`);
115
- console.log(` npx noninteractive start npx vercel # run an npx package`);
116
- console.log(` npx noninteractive start vercel login # run a command directly`);
117
- } else {
118
- console.log(`\n[session '${name}' started read the output above, then use:]`);
119
- console.log(` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`);
120
- console.log(` npx noninteractive read ${name} # read updated output`);
121
- console.log(` npx noninteractive stop ${name} # stop the session`);
122
- }
123
- return;
124
- }
125
- if (res.exited) {
126
- process.stdout.write(res.output ?? "");
127
- console.log(`\n[session '${name}' exited ${res.exitCode} the command failed]`);
128
- console.log(`hint: the first argument to "start" is the command to run, NOT a session name.`);
129
- console.log(` npx noninteractive start npx vercel # run an npx package`);
130
- console.log(` npx noninteractive start vercel login # run a command directly`);
131
- return;
132
- }
133
- } catch {}
134
- }
135
-
136
- console.log(`[session '${name}' started but no output yet — use:]`);
137
- console.log(` npx noninteractive read ${name} # read output`);
138
- console.log(` npx noninteractive send ${name} "<text>" # send keystrokes (use "" for Enter)`);
139
- console.log(` npx noninteractive stop ${name} # stop the session`);
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
- const sock = socketPath(name);
144
- const res = await sendMessage(sock, { action: "read" });
145
- if (res.output !== undefined) process.stdout.write(res.output);
146
- if (res.exited) console.log(`\n[exited ${res.exitCode}]`);
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
- const sock = socketPath(name);
151
- await sendMessage(sock, { action: "send", data: text });
152
- console.log(`[sent to '${name}' — run "npx noninteractive read ${name}" to see the result]`);
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
- const sock = socketPath(name);
157
- await sendMessage(sock, { action: "stop" });
158
- console.log(`session '${name}' stopped`);
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
- const { readdirSync } = await import("node:fs");
163
- ensureSessionsDir();
164
- const files = readdirSync((await import("./paths")).SESSIONS_DIR);
165
- const sessions = files.filter(f => f.endsWith(".sock")).map(f => f.replace(".sock", ""));
166
-
167
- if (sessions.length === 0) {
168
- console.log("no active sessions");
169
- return;
170
- }
171
-
172
- for (const name of sessions) {
173
- const sock = socketPath(name);
174
- try {
175
- const res = await sendMessage(sock, { action: "status" });
176
- const status = res.running ? "running" : `exited (${res.exitCode})`;
177
- console.log(`${name} [${status}] pid=${res.pid}`);
178
- } catch {
179
- console.log(`${name} [dead]`);
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
- const args = process.argv.slice(2);
186
-
187
- if (args[0] === "__daemon__") {
188
- const { runDaemon } = await import("./daemon");
189
- return runDaemon(args[1], args[2], args.slice(3));
190
- }
191
-
192
- const cmd = args[0];
193
-
194
- switch (cmd) {
195
- case "start": {
196
- if (args.length < 2) { console.error("usage: noninteractive start <cmd> [args...]\n\nexample: npx noninteractive start npx vercel"); process.exit(1); }
197
- return start(args.slice(1));
198
- }
199
- case "read": {
200
- const name = args[1];
201
- if (!name) { console.error("usage: noninteractive read <session>\n\nexample: npx noninteractive read vercel"); process.exit(1); }
202
- return read(name);
203
- }
204
- case "send": {
205
- const name = args[1];
206
- const text = args[2];
207
- if (!name || text === undefined) { console.error("usage: noninteractive send <session> <text>\n\nexample: npx noninteractive send vercel \"y\""); process.exit(1); }
208
- return send(name, text);
209
- }
210
- case "stop": {
211
- const name = args[1];
212
- if (!name) { console.error("usage: noninteractive stop <session>\n\nexample: npx noninteractive stop vercel"); process.exit(1); }
213
- return stop(name);
214
- }
215
- case "list":
216
- case "ls":
217
- return list();
218
- case "version":
219
- case "--version":
220
- case "-v": {
221
- const { version } = require("../package.json");
222
- console.log(`noninteractive v${version}`);
223
- return;
224
- }
225
- default:
226
- console.log(HELP);
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
- console.error(err.message);
232
- process.exit(1);
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(process.env.HOME!, ".noninteractive", "sessions");
5
+ export const SESSIONS_DIR = resolve(homedir(), ".noninteractive", "sessions");
5
6
 
6
7
  export function ensureSessionsDir() {
7
- mkdirSync(SESSIONS_DIR, { recursive: true });
8
+ mkdirSync(SESSIONS_DIR, { recursive: true });
8
9
  }
9
10
 
10
11
  export function socketPath(name: string) {
11
- return resolve(SESSIONS_DIR, `${name}.sock`);
12
+ return resolve(SESSIONS_DIR, `${name}.sock`);
12
13
  }