noninteractive 0.3.6 → 0.3.8

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
@@ -16,14 +16,17 @@ npx noninteractive
16
16
  # Start a session (runs `npx workos` in a background PTY)
17
17
  npx noninteractive workos
18
18
 
19
- # Read what's on screen
20
- npx noninteractive read workos
19
+ # Send keystrokes and wait for new output
20
+ npx noninteractive send workos "" --wait
21
21
 
22
- # Send keystrokes (Enter to confirm a prompt)
23
- npx noninteractive send workos ""
22
+ # Send text input and wait for response
23
+ npx noninteractive send workos "my-api-key" --wait
24
24
 
25
- # Send text input
26
- npx noninteractive send workos "my-api-key"
25
+ # Wait for new output without sending (e.g. OAuth callback)
26
+ npx noninteractive read workos --wait
27
+
28
+ # Read current output (non-blocking)
29
+ npx noninteractive read workos
27
30
 
28
31
  # Stop a session
29
32
  npx noninteractive stop workos
@@ -37,18 +40,12 @@ npx noninteractive list
37
40
  ```bash
38
41
  # Start the installer
39
42
  npx noninteractive workos
40
-
41
- # Wait for it to load, then read the prompt
42
- npx noninteractive read workos
43
43
  # ◆ Run the AuthKit installer?
44
44
  # │ ● Yes / ○ No
45
45
  # └
46
46
 
47
- # Press Enter to confirm "Yes"
48
- npx noninteractive send workos ""
49
-
50
- # Read the next prompt
51
- npx noninteractive read workos
47
+ # Press Enter to confirm "Yes", wait for next prompt
48
+ npx noninteractive send workos "" --wait
52
49
  # ◆ You are on main. Create a feature branch?
53
50
  # │ ● Create feat/add-workos-authkit
54
51
  # │ ○ Continue on current branch
@@ -56,12 +53,29 @@ npx noninteractive read workos
56
53
  # └
57
54
 
58
55
  # Press Enter to confirm
59
- npx noninteractive send workos ""
56
+ npx noninteractive send workos "" --wait
60
57
 
61
58
  # Done? Stop the session
62
59
  npx noninteractive stop workos
63
60
  ```
64
61
 
62
+ ## The `--wait` flag
63
+
64
+ Both `send` and `read` support `--wait` (`-w`), which blocks until new output appears instead of returning immediately. This eliminates polling loops and reduces tool calls by ~7-10x.
65
+
66
+ ```bash
67
+ # Old way (polling): send + read + read + read...
68
+ npx noninteractive send workos ""
69
+ npx noninteractive read workos # maybe not ready yet
70
+ npx noninteractive read workos # still waiting...
71
+ npx noninteractive read workos # finally got output
72
+
73
+ # New way (blocking): send --wait
74
+ npx noninteractive send workos "" --wait # returns when output appears
75
+ ```
76
+
77
+ Use `--timeout <ms>` to set the max wait time (default: 30000ms).
78
+
65
79
  ## Agent Skill
66
80
 
67
81
  Install the [Agent Skill](https://agentskills.io) so your AI agent knows how to use noninteractive:
@@ -74,8 +88,9 @@ npx skills add https://noninteractive.org
74
88
 
75
89
  1. `npx noninteractive <tool>` spawns a detached daemon that runs `npx <tool>` inside a real pseudo-terminal (PTY)
76
90
  2. The daemon listens on a unix socket at `~/.noninteractive/sessions/<name>.sock`
77
- 3. `read`, `send`, and `stop` connect to that socket to interact with the running process
78
- 4. The PTY ensures the child process sees a real terminal `isTTY` is true, ANSI colors work, interactive menus render correctly
91
+ 3. `send --wait` sends keystrokes and blocks until new output appears one call instead of polling
92
+ 4. `read --wait` blocks until output changesperfect for OAuth flows and long operations
93
+ 5. The PTY ensures the child process sees a real terminal — `isTTY` is true, ANSI colors work, interactive menus render correctly
79
94
 
80
95
  ## Why
81
96
 
@@ -0,0 +1,604 @@
1
+ #!/usr/bin/env node
2
+ // @bun
3
+ import { createRequire } from "node:module";
4
+ var __create = Object.create;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __toESM = (mod, isNodeMode, target) => {
10
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
11
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
12
+ for (let key of __getOwnPropNames(mod))
13
+ if (!__hasOwnProp.call(to, key))
14
+ __defProp(to, key, {
15
+ get: () => mod[key],
16
+ enumerable: true
17
+ });
18
+ return to;
19
+ };
20
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
21
+ var __export = (target, all) => {
22
+ for (var name in all)
23
+ __defProp(target, name, {
24
+ get: all[name],
25
+ enumerable: true,
26
+ configurable: true,
27
+ set: (newValue) => all[name] = () => newValue
28
+ });
29
+ };
30
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
31
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
32
+
33
+ // src/paths.ts
34
+ var exports_paths = {};
35
+ __export(exports_paths, {
36
+ socketPath: () => socketPath,
37
+ ensureSessionsDir: () => ensureSessionsDir,
38
+ SESSIONS_DIR: () => SESSIONS_DIR
39
+ });
40
+ import { mkdirSync } from "node:fs";
41
+ import { homedir } from "node:os";
42
+ import { resolve } from "node:path";
43
+ function ensureSessionsDir() {
44
+ mkdirSync(SESSIONS_DIR, { recursive: true });
45
+ }
46
+ function socketPath(name) {
47
+ return resolve(SESSIONS_DIR, `${name}.sock`);
48
+ }
49
+ var SESSIONS_DIR;
50
+ var init_paths = __esm(() => {
51
+ SESSIONS_DIR = resolve(homedir(), ".noninteractive", "sessions");
52
+ });
53
+
54
+ // src/paths.ts
55
+ import { mkdirSync as mkdirSync2 } from "node:fs";
56
+ import { homedir as homedir2 } from "node:os";
57
+ import { resolve as resolve2 } from "node:path";
58
+ function ensureSessionsDir2() {
59
+ mkdirSync2(SESSIONS_DIR2, { recursive: true });
60
+ }
61
+ function socketPath2(name) {
62
+ return resolve2(SESSIONS_DIR2, `${name}.sock`);
63
+ }
64
+ var SESSIONS_DIR2;
65
+ var init_paths2 = __esm(() => {
66
+ SESSIONS_DIR2 = resolve2(homedir2(), ".noninteractive", "sessions");
67
+ });
68
+
69
+ // src/daemon.ts
70
+ var exports_daemon = {};
71
+ __export(exports_daemon, {
72
+ runDaemon: () => runDaemon
73
+ });
74
+ import { spawn } from "node:child_process";
75
+ import { unlinkSync } from "node:fs";
76
+ import { createServer } from "node:net";
77
+ import { dirname, resolve as resolve3 } from "node:path";
78
+ function getPtyBridge() {
79
+ const platform = process.platform;
80
+ const arch = process.arch;
81
+ const binaryName = `ptybridge-${platform}-${arch}`;
82
+ const scriptDir = dirname(process.argv[1] || process.execPath);
83
+ const candidates = [
84
+ resolve3(scriptDir, "..", "native", binaryName),
85
+ resolve3(scriptDir, "native", binaryName),
86
+ resolve3(dirname(import.meta.dirname), "native", binaryName),
87
+ resolve3(import.meta.dirname, "..", "native", binaryName)
88
+ ];
89
+ for (const p of candidates) {
90
+ try {
91
+ const { statSync } = __require("node:fs");
92
+ if (statSync(p).isFile())
93
+ return p;
94
+ } catch {}
95
+ }
96
+ return candidates[0];
97
+ }
98
+ function runDaemon(sessionName, executable, args) {
99
+ ensureSessionsDir2();
100
+ const sock = socketPath2(sessionName);
101
+ try {
102
+ unlinkSync(sock);
103
+ } catch {}
104
+ let outputBuffer = "";
105
+ let processExited = false;
106
+ let exitCode = null;
107
+ const waiters = [];
108
+ function notifyWaiters() {
109
+ while (waiters.length > 0) {
110
+ const w = waiters.shift();
111
+ clearTimeout(w.timer);
112
+ w.resolve(outputBuffer);
113
+ }
114
+ }
115
+ const ptyBridge = getPtyBridge();
116
+ const proc = spawn(ptyBridge, [executable, ...args], {
117
+ stdio: ["pipe", "pipe", "pipe"],
118
+ env: { ...process.env, TERM: "xterm-256color" }
119
+ });
120
+ const { stdout, stderr, stdin } = proc;
121
+ stdout?.on("data", (chunk) => {
122
+ outputBuffer += chunk.toString();
123
+ notifyWaiters();
124
+ });
125
+ stderr?.on("data", (chunk) => {
126
+ outputBuffer += chunk.toString();
127
+ notifyWaiters();
128
+ });
129
+ proc.on("exit", (code) => {
130
+ processExited = true;
131
+ exitCode = code;
132
+ outputBuffer += `
133
+ [exited ${code}]`;
134
+ notifyWaiters();
135
+ setTimeout(() => {
136
+ server.close();
137
+ try {
138
+ unlinkSync(sock);
139
+ } catch {}
140
+ process.exit(0);
141
+ }, 60000);
142
+ });
143
+ proc.on("error", (err) => {
144
+ outputBuffer += `
145
+ [error: ${err.message}]`;
146
+ processExited = true;
147
+ });
148
+ const server = createServer((socket) => {
149
+ let buf = "";
150
+ socket.on("data", (chunk) => {
151
+ buf += chunk.toString();
152
+ try {
153
+ const msg = JSON.parse(buf);
154
+ buf = "";
155
+ handle(msg, socket);
156
+ } catch {}
157
+ });
158
+ });
159
+ function respondWithOutput(socket) {
160
+ socket.end(JSON.stringify({
161
+ ok: true,
162
+ output: outputBuffer,
163
+ exited: processExited,
164
+ exitCode
165
+ }));
166
+ }
167
+ function waitForNewOutput(socket, sinceLength, timeout) {
168
+ if (outputBuffer.length > sinceLength || processExited) {
169
+ respondWithOutput(socket);
170
+ return;
171
+ }
172
+ const waiter = {
173
+ resolve: () => respondWithOutput(socket),
174
+ timer: setTimeout(() => {
175
+ const idx = waiters.indexOf(waiter);
176
+ if (idx !== -1)
177
+ waiters.splice(idx, 1);
178
+ respondWithOutput(socket);
179
+ }, timeout)
180
+ };
181
+ waiters.push(waiter);
182
+ }
183
+ function handle(msg, socket) {
184
+ switch (msg.action) {
185
+ case "read":
186
+ if (msg.wait) {
187
+ const timeout = msg.timeout ?? 30000;
188
+ waitForNewOutput(socket, outputBuffer.length, timeout);
189
+ } else {
190
+ respondWithOutput(socket);
191
+ }
192
+ break;
193
+ case "send":
194
+ if (processExited) {
195
+ socket.end(JSON.stringify({ ok: false, error: "process exited" }));
196
+ break;
197
+ }
198
+ stdin?.write(`${msg.data}\r`);
199
+ socket.end(JSON.stringify({ ok: true }));
200
+ break;
201
+ case "sendread": {
202
+ if (processExited) {
203
+ socket.end(JSON.stringify({ ok: false, error: "process exited" }));
204
+ break;
205
+ }
206
+ const beforeLength = outputBuffer.length;
207
+ const timeout = msg.timeout ?? 30000;
208
+ stdin?.write(`${msg.data}\r`);
209
+ waitForNewOutput(socket, beforeLength, timeout);
210
+ break;
211
+ }
212
+ case "stop":
213
+ proc.kill("SIGTERM");
214
+ socket.end(JSON.stringify({ ok: true }));
215
+ setTimeout(() => {
216
+ server.close();
217
+ try {
218
+ unlinkSync(sock);
219
+ } catch {}
220
+ process.exit(0);
221
+ }, 500);
222
+ break;
223
+ case "status":
224
+ socket.end(JSON.stringify({
225
+ ok: true,
226
+ running: !processExited,
227
+ pid: proc.pid,
228
+ exitCode
229
+ }));
230
+ break;
231
+ default:
232
+ socket.end(JSON.stringify({ ok: false, error: "unknown action" }));
233
+ }
234
+ }
235
+ server.listen(sock);
236
+ }
237
+ var init_daemon = __esm(() => {
238
+ init_paths2();
239
+ });
240
+
241
+ // package.json
242
+ var require_package = __commonJS((exports, module) => {
243
+ module.exports = {
244
+ name: "noninteractive",
245
+ version: "0.3.8",
246
+ type: "module",
247
+ bin: {
248
+ noninteractive: "./bin/noninteractive.js"
249
+ },
250
+ files: [
251
+ "bin/noninteractive.js",
252
+ "native/**/*"
253
+ ],
254
+ scripts: {
255
+ dev: "bun run src/index.ts",
256
+ build: `bun build src/index.ts --outfile bin/noninteractive.js --target node && node -e "const f='bin/noninteractive.js';const c=require('fs').readFileSync(f,'utf8');require('fs').writeFileSync(f,c.replace('#!/usr/bin/env bun','#!/usr/bin/env node'))" && chmod +x bin/noninteractive.js`,
257
+ "build:standalone": "bun build src/index.ts --compile --outfile bin/noninteractive",
258
+ "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 .",
259
+ check: "biome check --write src/",
260
+ test: "bun test",
261
+ prepublishOnly: "bun run build"
262
+ },
263
+ devDependencies: {
264
+ "@biomejs/biome": "^2.4.5",
265
+ "@types/bun": "latest"
266
+ },
267
+ engines: {
268
+ node: ">=18"
269
+ }
270
+ };
271
+ });
272
+
273
+ // src/index.ts
274
+ import { spawn as spawn2 } from "child_process";
275
+ import { existsSync } from "fs";
276
+
277
+ // src/client.ts
278
+ import { createConnection } from "node:net";
279
+ function sendMessage(sockPath, msg, timeoutMs) {
280
+ const effectiveTimeout = timeoutMs ?? 5000;
281
+ return new Promise((resolve, reject) => {
282
+ const socket = createConnection(sockPath);
283
+ let data = "";
284
+ let resolved = false;
285
+ let timer;
286
+ function tryResolve() {
287
+ if (resolved)
288
+ return;
289
+ try {
290
+ const parsed = JSON.parse(data);
291
+ resolved = true;
292
+ clearTimeout(timer);
293
+ socket.destroy();
294
+ resolve(parsed);
295
+ } catch {}
296
+ }
297
+ socket.on("connect", () => {
298
+ socket.write(JSON.stringify(msg));
299
+ });
300
+ socket.on("data", (chunk) => {
301
+ data += chunk.toString();
302
+ tryResolve();
303
+ });
304
+ socket.on("end", () => {
305
+ tryResolve();
306
+ if (!resolved) {
307
+ resolved = true;
308
+ clearTimeout(timer);
309
+ reject(new Error("invalid response from daemon"));
310
+ }
311
+ });
312
+ socket.on("error", (err) => {
313
+ if (resolved)
314
+ return;
315
+ resolved = true;
316
+ clearTimeout(timer);
317
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
318
+ reject(new Error("session not found"));
319
+ } else {
320
+ reject(err);
321
+ }
322
+ });
323
+ timer = setTimeout(() => {
324
+ if (resolved)
325
+ return;
326
+ resolved = true;
327
+ socket.destroy();
328
+ reject(new Error("connection timeout"));
329
+ }, effectiveTimeout);
330
+ });
331
+ }
332
+
333
+ // src/index.ts
334
+ init_paths();
335
+ var HELP = `noninteractive \u2014 run interactive CLI commands non-interactively.
336
+
337
+ usage: npx noninteractive <tool> [args...]
338
+
339
+ commands:
340
+ <tool> [args...] start a session (runs npx <tool> in a PTY)
341
+ send <session> <text> [--wait] send keystrokes (--wait waits for new output)
342
+ read <session> [--wait] [--timeout N] read terminal output (--wait blocks until new output)
343
+ stop <session> stop a session
344
+ list show active sessions
345
+ start <cmd> [args...] explicit start (for non-npx commands)
346
+
347
+ flags:
348
+ --wait, -w block until new output appears (for send and read)
349
+ --timeout <ms> max wait time in ms (default: 30000, used with --wait)
350
+
351
+ the session name is auto-derived from the tool (e.g. "workos" \u2192 session "workos").
352
+
353
+ example workflow (recommended \u2014 uses --wait to minimize round-trips):
354
+ npx noninteractive workos # starts "npx workos", session = "workos"
355
+ npx noninteractive send workos "" --wait # press Enter, wait for response
356
+ npx noninteractive send workos "y" --wait # type "y", wait for response
357
+ npx noninteractive read workos --wait # wait for new output (e.g. OAuth callback)
358
+ npx noninteractive stop workos # done, stop the session
359
+
360
+ more examples:
361
+ npx noninteractive vercel # session "vercel"
362
+ npx noninteractive supabase init # session "supabase"
363
+ npx noninteractive start vercel login # explicit start for non-npx commands`;
364
+ function getSelfCommand() {
365
+ if (process.argv[1] && /\.(ts|js)$/.test(process.argv[1])) {
366
+ return [process.argv[0], process.argv[1]];
367
+ }
368
+ return [process.argv[0]];
369
+ }
370
+ function deriveSessionName(cmd, args) {
371
+ const parts = [cmd, ...args];
372
+ let i = 0;
373
+ if (parts[i] === "npx" || parts[i] === "bunx")
374
+ i++;
375
+ while (i < parts.length && parts[i].startsWith("-"))
376
+ i++;
377
+ const name = parts[i] || cmd;
378
+ return name.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9_-]/g, "");
379
+ }
380
+ async function start(cmdArgs) {
381
+ const executable = cmdArgs[0];
382
+ const args = cmdArgs.slice(1);
383
+ const name = deriveSessionName(executable, args);
384
+ const sock = socketPath(name);
385
+ try {
386
+ const res = await sendMessage(sock, { action: "read" });
387
+ if (res.ok) {
388
+ process.stdout.write(res.output ?? "");
389
+ if (res.exited) {
390
+ console.log(`
391
+ [session '${name}' already exists but exited ${res.exitCode} \u2014 stopping it]`);
392
+ try {
393
+ await sendMessage(sock, { action: "stop" });
394
+ } catch {}
395
+ } else {
396
+ console.log(`
397
+ [session '${name}' already running \u2014 read the output above, then use:]`);
398
+ console.log(` npx noninteractive send ${name} "<text>" --wait # send and wait for response`);
399
+ console.log(` npx noninteractive read ${name} --wait # wait for new output`);
400
+ console.log(` npx noninteractive stop ${name} # stop the session`);
401
+ return;
402
+ }
403
+ }
404
+ } catch {}
405
+ ensureSessionsDir();
406
+ try {
407
+ const { unlinkSync: unlinkSync2 } = await import("fs");
408
+ unlinkSync2(sock);
409
+ } catch {}
410
+ const self = getSelfCommand();
411
+ const child = spawn2(self[0], [...self.slice(1), "__daemon__", name, executable, ...args], {
412
+ detached: true,
413
+ stdio: "ignore"
414
+ });
415
+ child.unref();
416
+ for (let i = 0;i < 50; i++) {
417
+ if (existsSync(sock))
418
+ break;
419
+ await new Promise((r) => setTimeout(r, 100));
420
+ }
421
+ if (!existsSync(sock)) {
422
+ console.error(`error: failed to start session '${name}'.`);
423
+ console.error(`the command was: ${executable} ${args.join(" ")}`);
424
+ console.error(`
425
+ make sure the command exists. examples:`);
426
+ console.error(` npx noninteractive start npx vercel # run an npx package`);
427
+ console.error(` npx noninteractive start vercel login # run a command directly`);
428
+ process.exit(1);
429
+ }
430
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, "");
431
+ for (let i = 0;i < 50; i++) {
432
+ await new Promise((r) => setTimeout(r, 200));
433
+ try {
434
+ const res = await sendMessage(sock, { action: "read" });
435
+ const clean = stripAnsi(res.output ?? "").trim();
436
+ if (clean.length > 10) {
437
+ process.stdout.write(res.output);
438
+ if (res.exited) {
439
+ console.log(`
440
+ [session '${name}' exited ${res.exitCode} \u2014 the command failed]`);
441
+ console.log(`hint: the first argument to "start" is the command to run, NOT a session name.`);
442
+ console.log(` npx noninteractive start npx vercel # run an npx package`);
443
+ console.log(` npx noninteractive start vercel login # run a command directly`);
444
+ } else {
445
+ console.log(`
446
+ [session '${name}' started \u2014 read the output above, then use:]`);
447
+ console.log(` npx noninteractive send ${name} "<text>" --wait # send and wait for response`);
448
+ console.log(` npx noninteractive read ${name} --wait # wait for new output`);
449
+ console.log(` npx noninteractive stop ${name} # stop the session`);
450
+ }
451
+ return;
452
+ }
453
+ if (res.exited) {
454
+ process.stdout.write(res.output ?? "");
455
+ console.log(`
456
+ [session '${name}' exited ${res.exitCode} \u2014 the command failed]`);
457
+ console.log(`hint: the first argument to "start" is the command to run, NOT a session name.`);
458
+ console.log(` npx noninteractive start npx vercel # run an npx package`);
459
+ console.log(` npx noninteractive start vercel login # run a command directly`);
460
+ return;
461
+ }
462
+ } catch {}
463
+ }
464
+ console.log(`[session '${name}' started but no output yet \u2014 use:]`);
465
+ console.log(` npx noninteractive send ${name} "<text>" --wait # send and wait for response`);
466
+ console.log(` npx noninteractive read ${name} --wait # wait for new output`);
467
+ console.log(` npx noninteractive stop ${name} # stop the session`);
468
+ }
469
+ async function read(name, wait, timeout) {
470
+ const sock = socketPath(name);
471
+ const msg = { action: "read" };
472
+ if (wait) {
473
+ msg.wait = true;
474
+ msg.timeout = timeout;
475
+ }
476
+ const clientTimeout = wait ? timeout + 5000 : 5000;
477
+ const res = await sendMessage(sock, msg, clientTimeout);
478
+ if (res.output !== undefined)
479
+ process.stdout.write(res.output);
480
+ if (res.exited)
481
+ console.log(`
482
+ [exited ${res.exitCode}]`);
483
+ }
484
+ async function send(name, text, wait, timeout) {
485
+ const sock = socketPath(name);
486
+ if (wait) {
487
+ const res = await sendMessage(sock, { action: "sendread", data: text, timeout }, timeout + 5000);
488
+ if (res.output !== undefined)
489
+ process.stdout.write(res.output);
490
+ if (res.exited)
491
+ console.log(`
492
+ [exited ${res.exitCode}]`);
493
+ } else {
494
+ await sendMessage(sock, { action: "send", data: text });
495
+ console.log(`[sent to '${name}' \u2014 run "npx noninteractive read ${name}" to see the result]`);
496
+ }
497
+ }
498
+ async function stop(name) {
499
+ const sock = socketPath(name);
500
+ await sendMessage(sock, { action: "stop" });
501
+ console.log(`session '${name}' stopped`);
502
+ }
503
+ async function list() {
504
+ const { readdirSync } = await import("fs");
505
+ ensureSessionsDir();
506
+ const files = readdirSync((await Promise.resolve().then(() => (init_paths(), exports_paths))).SESSIONS_DIR);
507
+ const sessions = files.filter((f) => f.endsWith(".sock")).map((f) => f.replace(".sock", ""));
508
+ if (sessions.length === 0) {
509
+ console.log("no active sessions");
510
+ return;
511
+ }
512
+ for (const name of sessions) {
513
+ const sock = socketPath(name);
514
+ try {
515
+ const res = await sendMessage(sock, { action: "status" });
516
+ const status = res.running ? "running" : `exited (${res.exitCode})`;
517
+ console.log(`${name} [${status}] pid=${res.pid}`);
518
+ } catch {
519
+ console.log(`${name} [dead]`);
520
+ }
521
+ }
522
+ }
523
+ async function main() {
524
+ const args = process.argv.slice(2);
525
+ if (args[0] === "__daemon__") {
526
+ const { runDaemon: runDaemon2 } = await Promise.resolve().then(() => (init_daemon(), exports_daemon));
527
+ return runDaemon2(args[1], args[2], args.slice(3));
528
+ }
529
+ const cmd = args[0];
530
+ switch (cmd) {
531
+ case "start": {
532
+ if (args.length < 2) {
533
+ console.error(`usage: noninteractive start <cmd> [args...]
534
+
535
+ example: npx noninteractive start npx vercel`);
536
+ process.exit(1);
537
+ }
538
+ return start(args.slice(1));
539
+ }
540
+ case "read": {
541
+ const readArgs = args.slice(1);
542
+ const name = readArgs.find((a) => !a.startsWith("-"));
543
+ if (!name) {
544
+ console.error(`usage: noninteractive read <session> [-w|--wait] [--timeout <ms>]
545
+
546
+ example: npx noninteractive read vercel --wait`);
547
+ process.exit(1);
548
+ }
549
+ const wait = readArgs.includes("-w") || readArgs.includes("--wait");
550
+ const timeoutIdx = readArgs.indexOf("--timeout");
551
+ const timeout = timeoutIdx !== -1 ? Number(readArgs[timeoutIdx + 1]) : 30000;
552
+ return read(name, wait, timeout);
553
+ }
554
+ case "sendread":
555
+ case "send": {
556
+ const sendArgs = args.slice(1);
557
+ const positional = sendArgs.filter((a) => !a.startsWith("-"));
558
+ const name = positional[0];
559
+ const text = positional[1];
560
+ if (!name || text === undefined) {
561
+ console.error(`usage: noninteractive send <session> <text> [--wait] [--timeout <ms>]
562
+
563
+ example: npx noninteractive send workos "" --wait`);
564
+ process.exit(1);
565
+ }
566
+ const wait = cmd === "sendread" || sendArgs.includes("-w") || sendArgs.includes("--wait");
567
+ const timeoutIdx = sendArgs.indexOf("--timeout");
568
+ const timeout = timeoutIdx !== -1 ? Number(sendArgs[timeoutIdx + 1]) : 30000;
569
+ return send(name, text, wait, timeout);
570
+ }
571
+ case "stop": {
572
+ const name = args[1];
573
+ if (!name) {
574
+ console.error(`usage: noninteractive stop <session>
575
+
576
+ example: npx noninteractive stop vercel`);
577
+ process.exit(1);
578
+ }
579
+ return stop(name);
580
+ }
581
+ case "list":
582
+ case "ls":
583
+ return list();
584
+ case "version":
585
+ case "--version":
586
+ case "-v": {
587
+ const { version } = require_package();
588
+ console.log(`noninteractive v${version}`);
589
+ return;
590
+ }
591
+ case undefined:
592
+ case "help":
593
+ case "--help":
594
+ case "-h":
595
+ console.log(HELP);
596
+ break;
597
+ default:
598
+ return start(["npx", ...args]);
599
+ }
600
+ }
601
+ main().catch((err) => {
602
+ console.error(err.message);
603
+ process.exit(1);
604
+ });
package/package.json CHANGED
@@ -1,26 +1,28 @@
1
1
  {
2
2
  "name": "noninteractive",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "bin": {
6
- "noninteractive": "./src/index.ts"
6
+ "noninteractive": "./bin/noninteractive.js"
7
7
  },
8
8
  "files": [
9
- "src/**/*",
9
+ "bin/noninteractive.js",
10
10
  "native/**/*"
11
11
  ],
12
12
  "scripts": {
13
13
  "dev": "bun run src/index.ts",
14
- "build": "bun build src/index.ts --compile --outfile bin/noninteractive",
14
+ "build": "bun build src/index.ts --outfile bin/noninteractive.js --target node && node -e \"const f='bin/noninteractive.js';const c=require('fs').readFileSync(f,'utf8');require('fs').writeFileSync(f,c.replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && chmod +x bin/noninteractive.js",
15
+ "build:standalone": "bun build src/index.ts --compile --outfile bin/noninteractive",
15
16
  "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
17
  "check": "biome check --write src/",
17
- "test": "bun test"
18
+ "test": "bun test",
19
+ "prepublishOnly": "bun run build"
18
20
  },
19
21
  "devDependencies": {
20
22
  "@biomejs/biome": "^2.4.5",
21
23
  "@types/bun": "latest"
22
24
  },
23
- "peerDependencies": {
24
- "typescript": "^5"
25
+ "engines": {
26
+ "node": ">=18"
25
27
  }
26
28
  }
package/src/client.ts DELETED
@@ -1,50 +0,0 @@
1
- import { createConnection } from "node:net";
2
-
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 = "";
20
-
21
- socket.on("connect", () => {
22
- socket.write(JSON.stringify(msg));
23
- });
24
-
25
- socket.on("data", (chunk) => {
26
- data += chunk.toString();
27
- });
28
-
29
- socket.on("end", () => {
30
- try {
31
- resolve(JSON.parse(data));
32
- } catch {
33
- reject(new Error("invalid response from daemon"));
34
- }
35
- });
36
-
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
- });
44
-
45
- setTimeout(() => {
46
- socket.destroy();
47
- reject(new Error("connection timeout"));
48
- }, 5000);
49
- });
50
- }
package/src/daemon.ts DELETED
@@ -1,146 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { unlinkSync } from "node:fs";
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
- }
11
-
12
- function getPtyBridge(): string {
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];
29
- }
30
-
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);
146
- }
package/src/index.ts DELETED
@@ -1,316 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { spawn } from "node:child_process";
4
- import { existsSync } from "node:fs";
5
- import { sendMessage } from "./client";
6
- import { ensureSessionsDir, socketPath } from "./paths";
7
-
8
- const HELP = `noninteractive — run interactive CLI commands non-interactively.
9
-
10
- usage: npx noninteractive <tool> [args...]
11
-
12
- commands:
13
- <tool> [args...] start a session (runs npx <tool> in a PTY)
14
- read <session> read current terminal output
15
- send <session> <text> send keystrokes (use "" for Enter)
16
- stop <session> stop a session
17
- list show active sessions
18
- start <cmd> [args...] explicit start (for non-npx commands)
19
-
20
- the session name is auto-derived from the tool (e.g. "workos" → session "workos").
21
-
22
- example workflow:
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
-
30
- more examples:
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
-
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]];
40
- }
41
-
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, "");
52
- }
53
-
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(
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
- );
192
- }
193
-
194
- async function read(name: string) {
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}]`);
199
- }
200
-
201
- async function send(name: string, text: string) {
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
- );
207
- }
208
-
209
- async function stop(name: string) {
210
- const sock = socketPath(name);
211
- await sendMessage(sock, { action: "stop" });
212
- console.log(`session '${name}' stopped`);
213
- }
214
-
215
- async function list() {
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
- }
238
- }
239
-
240
- async function main() {
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
- }
311
- }
312
-
313
- main().catch((err) => {
314
- console.error(err.message);
315
- process.exit(1);
316
- });
package/src/paths.ts DELETED
@@ -1,13 +0,0 @@
1
- import { mkdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { resolve } from "node:path";
4
-
5
- export const SESSIONS_DIR = resolve(homedir(), ".noninteractive", "sessions");
6
-
7
- export function ensureSessionsDir() {
8
- mkdirSync(SESSIONS_DIR, { recursive: true });
9
- }
10
-
11
- export function socketPath(name: string) {
12
- return resolve(SESSIONS_DIR, `${name}.sock`);
13
- }