Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.

Commit 1bfcc37

Browse files
committed
feat: add exec:type stdin command, PM2 graceful fallback, keep runner alive\n\n- Add sendStdin RPC to task-runner.js and pool.sendStdin() to worker-pool.js\n- Add cmdType to index.js: gm-exec type <task_id> <input> sends data to running task stdin\n- PM2 graceful fallback: wrap require('pm2') in try/catch, fall back to detached spawn\n- Remove autoStarted/stopRunner() calls so runner stays alive between exec calls\n- Update usage text to mention type command\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 352b9c3 commit 1bfcc37

3 files changed

Lines changed: 69 additions & 18 deletions

File tree

src/index.js

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { resolve, dirname, join } from 'path';
55
import { fileURLToPath } from 'url';
66
import { tmpdir } from 'os';
77

8-
const pm2lib = require('pm2');
8+
let pm2lib = null;
9+
try { pm2lib = require('pm2'); } catch { /* pm2 not installed — fallback to direct spawn */ }
910

1011
const __dirname = dirname(fileURLToPath(import.meta.url));
1112
const RUNNER_SCRIPT = resolve(__dirname, 'task-runner.js');
@@ -33,12 +34,14 @@ function pm2describe(name) {
3334
}
3435

3536
async function withPm2(fn) {
37+
if (!pm2lib) throw new Error('pm2 unavailable');
3638
await pm2connect();
3739
try { return await fn(); }
3840
finally { await pm2disconnect(); }
3941
}
4042

4143
async function printRunningTools() {
44+
if (!pm2lib) return;
4245
try {
4346
await pm2connect();
4447
const list = await pm2list();
@@ -77,10 +80,16 @@ async function healthCheck() {
7780
async function ensureRunner() {
7881
if (await healthCheck()) return false;
7982
process.stderr.write('Auto-starting runner...\n');
80-
await withPm2(async () => {
81-
await pm2delete(PM2_NAME).catch(() => {});
82-
await pm2start({ script: 'bun', args: RUNNER_SCRIPT, name: PM2_NAME, autorestart: false, watch: false });
83-
});
83+
if (pm2lib) {
84+
await withPm2(async () => {
85+
await pm2delete(PM2_NAME).catch(() => {});
86+
await pm2start({ script: 'bun', args: RUNNER_SCRIPT, name: PM2_NAME, autorestart: false, watch: false });
87+
});
88+
} else {
89+
const { spawn } = await import('child_process');
90+
const child = spawn('bun', [RUNNER_SCRIPT], { detached: true, stdio: 'ignore' });
91+
child.unref();
92+
}
8493
for (let i = 0; i < 20; i++) {
8594
await new Promise(r => setTimeout(r, 500));
8695
if (await healthCheck()) return true;
@@ -89,6 +98,7 @@ async function ensureRunner() {
8998
}
9099

91100
async function stopRunner() {
101+
if (!pm2lib) return;
92102
await withPm2(() => pm2delete(PM2_NAME).catch(() => {}));
93103
}
94104

@@ -123,7 +133,7 @@ function rpcCall(method, params, timeoutMs = 10000) {
123133
}
124134

125135
async function runCode(code, runtime, workingDirectory) {
126-
const autoStarted = await ensureRunner();
136+
await ensureRunner();
127137
const taskId = await rpcCall('createTask', { code, runtime, workingDirectory }).then(r => r?.taskId ?? r);
128138

129139
const safetyTimeout = new Promise(r => {
@@ -151,6 +161,7 @@ async function runCode(code, runtime, workingDirectory) {
151161
console.log(`Task ID: ${id}\n`);
152162
console.log(` gm-exec sleep ${id} # wait for completion (up to 30s) — recommended`);
153163
console.log(` gm-exec status ${id} # drain output buffer (snapshot)`);
164+
console.log(` gm-exec type ${id} <input> # send stdin to running task`);
154165
console.log(` gm-exec close ${id} # delete task when done`);
155166
console.log(` gm-exec runner stop # stop runner when all tasks done`);
156167
console.log(`\nRunner kept alive: ${PM2_NAME} (PM2)`);
@@ -176,10 +187,16 @@ async function cmdRunnerStart() {
176187
console.log(`Runner already healthy on port ${readFileSync(PORT_FILE, 'utf8').trim()}`);
177188
return;
178189
}
179-
await withPm2(async () => {
180-
await pm2delete(PM2_NAME).catch(() => {});
181-
await pm2start({ script: 'bun', args: RUNNER_SCRIPT, name: PM2_NAME, autorestart: false, watch: false });
182-
});
190+
if (pm2lib) {
191+
await withPm2(async () => {
192+
await pm2delete(PM2_NAME).catch(() => {});
193+
await pm2start({ script: 'bun', args: RUNNER_SCRIPT, name: PM2_NAME, autorestart: false, watch: false });
194+
});
195+
} else {
196+
const { spawn } = await import('child_process');
197+
const child = spawn('bun', [RUNNER_SCRIPT], { detached: true, stdio: 'ignore' });
198+
child.unref();
199+
}
183200
for (let i = 0; i < 20; i++) {
184201
await new Promise(r => setTimeout(r, 500));
185202
if (await healthCheck()) { console.log(`Runner started on port ${readFileSync(PORT_FILE, 'utf8').trim()}`); return; }
@@ -193,6 +210,12 @@ async function cmdRunnerStop() {
193210
}
194211

195212
async function cmdRunnerStatus() {
213+
if (!pm2lib) {
214+
const alive = await healthCheck();
215+
console.log(`${PM2_NAME}: ${alive ? 'online (no pm2 — direct spawn)' : 'not running'}`);
216+
if (alive && existsSync(PORT_FILE)) console.log(`port: ${readFileSync(PORT_FILE, 'utf8').trim()}`);
217+
return;
218+
}
196219
const desc = await withPm2(() => pm2describe(PM2_NAME).catch(() => []));
197220
if (!desc || desc.length === 0) { console.log(`${PM2_NAME}: not found`); return; }
198221
const p = desc[0];
@@ -208,6 +231,7 @@ async function cmdRunnerStatus() {
208231
console.log(`\nRunner is active. If you have background tasks:`);
209232
console.log(` gm-exec sleep <task_id> # wait for task completion (up to 30s)`);
210233
console.log(` gm-exec status <task_id> # check task status`);
234+
console.log(` gm-exec type <task_id> <input> # send stdin to running task`);
211235
console.log(` gm-exec runner stop # stop runner when all tasks done`);
212236
}
213237
}
@@ -230,11 +254,10 @@ async function cmdBash(cmdArgs, positional) {
230254
}
231255

232256
async function cmdStatus(taskId) {
233-
const autoStarted = await ensureRunner();
257+
await ensureRunner();
234258
const rawId = parseInt(taskId.replace(/^task_/, ''), 10);
235259
const task = await rpcCall('getTask', { taskId: rawId }).then(r => r?.task ?? r);
236260
if (!task) {
237-
if (autoStarted) await stopRunner();
238261
throw Object.assign(new Error('Task not found'), { exitCode: 1, silent: true });
239262
}
240263
console.log(`Status: ${task.status}`);
@@ -254,17 +277,17 @@ async function cmdStatus(taskId) {
254277
if (task.status === 'running') {
255278
console.log(`\nTask still running. Options:`);
256279
console.log(` gm-exec sleep ${taskId} # wait for completion (up to 30s) — recommended`);
280+
console.log(` gm-exec type ${taskId} <input> # send stdin to running task`);
257281
console.log(` gm-exec status ${taskId} # check status again (snapshot)`);
258282
} else if (task.status === 'completed' || task.status === 'failed') {
259283
console.log(`\nTask finished. Clean up:`);
260284
console.log(` gm-exec close ${taskId} # delete task`);
261285
console.log(` gm-exec runner stop # stop runner if no more tasks`);
262286
}
263-
if (autoStarted) await stopRunner();
264287
}
265288

266289
async function cmdClose(taskId) {
267-
const autoStarted = await ensureRunner();
290+
await ensureRunner();
268291
const rawId = parseInt(taskId.replace(/^task_/, ''), 10);
269292
await rpcCall('deleteTask', { taskId: rawId });
270293
const res = await rpcCall('listTasks', {}).catch(() => ({ tasks: [] }));
@@ -277,12 +300,11 @@ async function cmdClose(taskId) {
277300
}
278301
} else {
279302
console.log(` gm-exec runner stop # no more tasks — stop runner`);
280-
if (autoStarted) await stopRunner();
281303
}
282304
}
283305

284306
async function cmdSleep(taskId, timeoutSeconds, nextOutputMode) {
285-
const autoStarted = await ensureRunner();
307+
await ensureRunner();
286308
const rawId = parseInt(taskId.replace(/^task_/, ''), 10);
287309
const timeout = (parseInt(timeoutSeconds, 10) || 30) * 1000;
288310
const startTime = Date.now();
@@ -309,7 +331,6 @@ async function cmdSleep(taskId, timeoutSeconds, nextOutputMode) {
309331
console.log(`\nTask finished (${task.status}). Clean up:`);
310332
console.log(` gm-exec close ${taskId} # delete task`);
311333
console.log(` gm-exec runner stop # stop runner if no more tasks`);
312-
if (autoStarted) await stopRunner();
313334
return;
314335
}
315336
if (nextOutputMode) {
@@ -323,7 +344,19 @@ async function cmdSleep(taskId, timeoutSeconds, nextOutputMode) {
323344
console.log(`\nTimeout after ${timeout / 1000}s. Task still running.`);
324345
console.log(` gm-exec sleep ${taskId} # wait again (up to 30s) — recommended`);
325346
console.log(` gm-exec status ${taskId} # check current status (snapshot)`);
326-
if (autoStarted) await stopRunner();
347+
}
348+
349+
async function cmdType(taskId, inputData) {
350+
await ensureRunner();
351+
const rawId = parseInt(taskId.replace(/^task_/, ''), 10);
352+
const data = inputData + '\n';
353+
const result = await rpcCall('sendStdin', { taskId: rawId, data }).then(r => r?.ok ?? r).catch(() => false);
354+
if (result) {
355+
console.log(`Sent to task ${taskId}`);
356+
} else {
357+
process.stderr.write(`Task ${taskId} not found or not running\n`);
358+
return 1;
359+
}
327360
}
328361

329362
function parseArgs(argv) {
@@ -362,6 +395,7 @@ Commands:
362395
status <task_id> Poll status + drain output of a background task
363396
sleep <task_id> [seconds]
364397
Wait for task completion (default 30s timeout)
398+
type <task_id> <input> Send input to stdin of a running background task
365399
close <task_id> Delete a background task
366400
runner start|stop|status
367401
Manage the task runner process (PM2)
@@ -401,6 +435,10 @@ try {
401435
} else if (cmd === 'close') {
402436
if (!rest[0]) { process.stderr.write('Task ID required\n'); exitCode = 1; }
403437
else await cmdClose(rest[0]);
438+
} else if (cmd === 'type') {
439+
if (!rest[0]) { process.stderr.write('Task ID required\n'); exitCode = 1; }
440+
else if (!rest[1]) { process.stderr.write('Input required\n'); exitCode = 1; }
441+
else exitCode = (await cmdType(rest[0], rest.slice(1).join(' '))) ?? 0;
404442
} else {
405443
process.stderr.write(`Unknown command: ${cmd}\n`);
406444
usage();

src/task-runner.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ async function handleRPC(body) {
9393
const result = await backgroundStore.waitForOutput(params.taskId, params.timeoutMs);
9494
return result;
9595
}
96+
case 'sendStdin': {
97+
const ok = pool.sendStdin(params.taskId, params.data);
98+
return { ok };
99+
}
96100
case 'shutdown':
97101
setImmediate(gracefulShutdown);
98102
return { ok: true };

src/workers/worker-pool.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,15 @@ export class WorkerPool extends EventEmitter {
248248
}
249249
}
250250

251+
sendStdin(backgroundTaskId, data) {
252+
for (const [, job] of [...this.activeJobs, ...this.backgroundJobs]) {
253+
if (job.backgroundTaskId === backgroundTaskId && job.worker) {
254+
try { job.worker.postMessage({ type: 'stdin', jobId: job.jobId, data }); return true; } catch { return false; }
255+
}
256+
}
257+
return false;
258+
}
259+
251260
async shutdown() {
252261
this.shuttingDown = true;
253262
if (this.healthCheckInterval) {

0 commit comments

Comments
 (0)