ninja-terminals 2.4.6 → 2.4.7

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.
@@ -5,7 +5,7 @@ const http = require('http');
5
5
  const net = require('net');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
- const { spawn } = require('child_process');
8
+ const { spawn, execSync } = require('child_process');
9
9
 
10
10
  const SESSION_DIR = path.join(os.homedir(), '.ninja');
11
11
  const SESSION_FILE = path.join(SESSION_DIR, 'session.json');
@@ -54,6 +54,36 @@ async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts
54
54
  throw new Error(`No available port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`);
55
55
  }
56
56
 
57
+ // Terminate a previous Ninja runtime recorded in the session file so a new
58
+ // launch REPLACES it instead of orphaning it (which caused servers to pile up
59
+ // and the port to climb 3300 -> 3301 -> 3302 on each relaunch). Verifies the
60
+ // PID is actually a Ninja process before killing (guards against PID reuse).
61
+ // Returns true if it killed something. selfPid is skipped (don't kill caller).
62
+ async function killStaleRuntime(session, selfPid = process.pid) {
63
+ const pid = session && session.pid;
64
+ if (!pid || pid === selfPid) return false;
65
+
66
+ // Is the process even alive?
67
+ try { process.kill(pid, 0); } catch { return false; }
68
+
69
+ // Verify it's a Ninja server (avoid killing an unrelated reused PID).
70
+ if (process.platform !== 'win32') {
71
+ try {
72
+ const cmd = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
73
+ if (!/server\.js|cli\.js|ninja/i.test(cmd)) return false;
74
+ } catch { return false; }
75
+ }
76
+
77
+ try {
78
+ process.kill(pid, 'SIGTERM');
79
+ // Give it a moment to release its port so the new server can reclaim it.
80
+ await new Promise((resolve) => setTimeout(resolve, 800));
81
+ return true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
57
87
  function writeRuntimeSession(session) {
58
88
  ensureSessionDir();
59
89
  const payload = {
@@ -327,6 +357,7 @@ module.exports = {
327
357
  VISUAL_LEDGER_FILE,
328
358
  VALID_VISUAL_STAGES,
329
359
  findAvailablePort,
360
+ killStaleRuntime,
330
361
  writeRuntimeSession,
331
362
  updateRuntimeSession,
332
363
  readRuntimeSession,
package/ninja-ensure.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  readRuntimeSession,
12
12
  healthCheckSession,
13
13
  writeRuntimeSession,
14
+ killStaleRuntime,
14
15
  requestJson,
15
16
  } = require('./lib/runtime-session');
16
17
  const {
@@ -311,8 +312,10 @@ async function main() {
311
312
  session = existingSession;
312
313
  action = 'reuse';
313
314
  } else if (health.ok) {
314
- log('Healthy session found, but it does not match the requested launch config');
315
- log('Will start a fresh runtime for this explicit request');
315
+ log('Healthy session found, but it does not match the requested launch config (or --fresh)');
316
+ log('Replacing it to avoid orphaned servers and a climbing port...');
317
+ const killed = await killStaleRuntime(existingSession);
318
+ if (killed) log(`Terminated previous runtime (pid ${existingSession.pid})`);
316
319
  }
317
320
  }
318
321
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninja-terminals",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "description": "MCP server for multi-terminal Claude Code orchestration with DAG task management, parallel execution, and self-improvement",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -26,6 +26,8 @@ const { runPostSession } = require('./lib/post-session');
26
26
  const {
27
27
  SESSION_FILE,
28
28
  findAvailablePort,
29
+ killStaleRuntime,
30
+ readRuntimeSession,
29
31
  writeRuntimeSession,
30
32
  updateRuntimeSession,
31
33
  clearRuntimeSession,
@@ -1305,6 +1307,12 @@ app.post('/api/auth/register', async (req, res) => {
1305
1307
  // ── Start ───────────────────────────────────────────────────
1306
1308
 
1307
1309
  async function startServer() {
1310
+ // Replace any previous Ninja runtime instead of orphaning it (prevents a
1311
+ // pile-up of dead servers and a port that climbs on each relaunch).
1312
+ const previous = readRuntimeSession();
1313
+ const replaced = await killStaleRuntime(previous);
1314
+ if (replaced) console.log(`[runtime] replaced previous Ninja runtime (pid ${previous.pid})`);
1315
+
1308
1316
  const selectedPort = await findAvailablePort(PREFERRED_PORT);
1309
1317
  if (selectedPort !== PREFERRED_PORT) {
1310
1318
  console.log(`[port] ${PREFERRED_PORT} is unavailable; using ${selectedPort}`);