ninja-terminals 2.4.6 → 2.4.9
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/cli.js +1 -1
- package/lib/runtime-session.js +141 -4
- package/mcp-server.js +3 -2
- package/ninja-ensure.js +12 -2
- package/package.json +1 -1
- package/public/app.js +2 -1
- package/public/index.html +5 -0
- package/server.js +22 -4
package/cli.js
CHANGED
|
@@ -185,7 +185,7 @@ MCP tools available after restart:
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
const port = parseInt(getArg('--port', '3300'), 10);
|
|
188
|
-
const terminals = parseInt(getArg('--terminals', '
|
|
188
|
+
const terminals = parseInt(getArg('--terminals', '4'), 10); // Default fleet size
|
|
189
189
|
const cwd = getArg('--cwd', process.cwd());
|
|
190
190
|
const token = getArg('--token', null);
|
|
191
191
|
const offline = hasFlag('--offline');
|
package/lib/runtime-session.js
CHANGED
|
@@ -5,10 +5,16 @@ 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
|
+
// NINJA_HOME lets a second, isolated Ninja runtime coexist with another agent's default
|
|
11
|
+
// runtime: it gets its own session.json/lock so the two never SIGTERM each other on launch.
|
|
12
|
+
// Defaults to ~/.ninja, so existing single-runtime behavior is unchanged.
|
|
13
|
+
const SESSION_DIR = path.join(process.env.NINJA_HOME || os.homedir(), '.ninja');
|
|
11
14
|
const SESSION_FILE = path.join(SESSION_DIR, 'session.json');
|
|
15
|
+
// Per-port session files live here, one per running runtime. session.json above
|
|
16
|
+
// stays as a "most-recent" pointer so existing readers keep working unchanged.
|
|
17
|
+
const SESSIONS_DIR = path.join(SESSION_DIR, 'sessions');
|
|
12
18
|
const TOKEN_FILE = path.join(SESSION_DIR, 'token');
|
|
13
19
|
const LEDGER_FILE = path.join(SESSION_DIR, 'dispatch-ledger.ndjson');
|
|
14
20
|
const VERIFICATION_LEDGER_FILE = path.join(SESSION_DIR, 'verification-ledger.ndjson');
|
|
@@ -18,6 +24,10 @@ function ensureSessionDir() {
|
|
|
18
24
|
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
function sessionFileForPort(port) {
|
|
28
|
+
return path.join(SESSIONS_DIR, `${port}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
// Probe a single host: 'free' (bindable), 'inuse' (EADDRINUSE), or
|
|
22
32
|
// 'unavailable' (stack absent / other error — don't treat as a collision).
|
|
23
33
|
function probePort(port, host) {
|
|
@@ -54,6 +64,81 @@ async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts
|
|
|
54
64
|
throw new Error(`No available port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`);
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
// Terminate a previous Ninja runtime recorded in the session file so a new
|
|
68
|
+
// launch REPLACES it instead of orphaning it (which caused servers to pile up
|
|
69
|
+
// and the port to climb 3300 -> 3301 -> 3302 on each relaunch). Verifies the
|
|
70
|
+
// PID is actually a Ninja process before killing (guards against PID reuse).
|
|
71
|
+
// Returns true if it killed something. selfPid is skipped (don't kill caller).
|
|
72
|
+
async function killStaleRuntime(session, selfPid = process.pid) {
|
|
73
|
+
const pid = session && session.pid;
|
|
74
|
+
if (!pid || pid === selfPid) return false;
|
|
75
|
+
|
|
76
|
+
// Is the process even alive?
|
|
77
|
+
try { process.kill(pid, 0); } catch { return false; }
|
|
78
|
+
|
|
79
|
+
// NEVER kill a HEALTHY runtime — it may be another instance doing real work.
|
|
80
|
+
// Only a dead/hung runtime is a "stale" one worth reclaiming. This is the
|
|
81
|
+
// guard that stops a new launch from SIGTERMing a live parallel fleet.
|
|
82
|
+
if (session.port) {
|
|
83
|
+
const health = await healthCheckSession(
|
|
84
|
+
{ host: session.host || '127.0.0.1', port: session.port },
|
|
85
|
+
1500,
|
|
86
|
+
);
|
|
87
|
+
if (health.ok) return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Verify it's a Ninja server (avoid killing an unrelated reused PID).
|
|
91
|
+
if (process.platform !== 'win32') {
|
|
92
|
+
try {
|
|
93
|
+
const cmd = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
|
|
94
|
+
if (!/server\.js|cli\.js|ninja/i.test(cmd)) return false;
|
|
95
|
+
} catch { return false; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, 'SIGTERM');
|
|
100
|
+
// Give it a moment to release its port so the new server can reclaim it.
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
102
|
+
return true;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Reclaim the preferred port from any *Ninja* server still holding it. The
|
|
109
|
+
// pid-based killStaleRuntime only knows the single pid in session.json, so
|
|
110
|
+
// orphans on climbed ports (3301/3302 from earlier crashed relaunches) survive
|
|
111
|
+
// and findAvailablePort climbs past them — each climb is a new server and a new
|
|
112
|
+
// browser tab (the 2/4/6-tab pile-up). Sweeping the port itself kills them all.
|
|
113
|
+
// Only SIGTERMs processes verified as Ninja; a non-Ninja listener is left alone
|
|
114
|
+
// (caller then climbs past it as before). POSIX-only (lsof); no-op elsewhere.
|
|
115
|
+
async function reclaimPort(port, selfPid = process.pid) {
|
|
116
|
+
if (process.platform === 'win32') return 0;
|
|
117
|
+
// Never reclaim a HEALTHY fleet. If the port answers /health, it's a live
|
|
118
|
+
// Ninja instance — leave it alone and let the caller climb to the next port.
|
|
119
|
+
// Only sweep when the listener is dead/hung (a true orphan).
|
|
120
|
+
const holder = await healthCheckSession({ host: '127.0.0.1', port }, 1500);
|
|
121
|
+
if (holder.ok) return 0;
|
|
122
|
+
let pids;
|
|
123
|
+
try {
|
|
124
|
+
pids = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, { encoding: 'utf8' })
|
|
125
|
+
.split('\n').map((s) => parseInt(s, 10)).filter((p) => p && p !== selfPid);
|
|
126
|
+
} catch {
|
|
127
|
+
return 0; // lsof absent or nothing on the port
|
|
128
|
+
}
|
|
129
|
+
let killed = 0;
|
|
130
|
+
for (const pid of new Set(pids)) {
|
|
131
|
+
try {
|
|
132
|
+
const cmd = execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
|
|
133
|
+
if (!/server\.js|cli\.js|ninja/i.test(cmd)) continue; // not ours — leave it
|
|
134
|
+
process.kill(pid, 'SIGTERM');
|
|
135
|
+
killed++;
|
|
136
|
+
} catch { /* gone already / not killable */ }
|
|
137
|
+
}
|
|
138
|
+
if (killed) await new Promise((resolve) => setTimeout(resolve, 800)); // let ports release
|
|
139
|
+
return killed;
|
|
140
|
+
}
|
|
141
|
+
|
|
57
142
|
function writeRuntimeSession(session) {
|
|
58
143
|
ensureSessionDir();
|
|
59
144
|
const payload = {
|
|
@@ -63,10 +148,36 @@ function writeRuntimeSession(session) {
|
|
|
63
148
|
createdAt: new Date().toISOString(),
|
|
64
149
|
...session,
|
|
65
150
|
};
|
|
66
|
-
|
|
151
|
+
const json = JSON.stringify(payload, null, 2) + '\n';
|
|
152
|
+
// Pointer: "most-recent runtime" — keeps every existing reader working.
|
|
153
|
+
fs.writeFileSync(SESSION_FILE, json, { mode: 0o600 });
|
|
154
|
+
// Per-port file: lets parallel fleets coexist without clobbering each other.
|
|
155
|
+
if (payload.port) {
|
|
156
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
157
|
+
fs.writeFileSync(sessionFileForPort(payload.port), json, { mode: 0o600 });
|
|
158
|
+
}
|
|
67
159
|
return payload;
|
|
68
160
|
}
|
|
69
161
|
|
|
162
|
+
// All currently-recorded runtimes (one per per-port file). Most-recent first.
|
|
163
|
+
function listRuntimeSessions() {
|
|
164
|
+
let files;
|
|
165
|
+
try {
|
|
166
|
+
files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const sessions = files.map((f) => {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'));
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}).filter(Boolean);
|
|
177
|
+
sessions.sort((a, b) => String(b.updatedAt || b.createdAt || '').localeCompare(String(a.updatedAt || a.createdAt || '')));
|
|
178
|
+
return sessions;
|
|
179
|
+
}
|
|
180
|
+
|
|
70
181
|
function updateRuntimeSession(patch) {
|
|
71
182
|
const current = readRuntimeSession() || {};
|
|
72
183
|
return writeRuntimeSession({ ...current, ...patch, updatedAt: new Date().toISOString() });
|
|
@@ -81,7 +192,29 @@ function readRuntimeSession() {
|
|
|
81
192
|
}
|
|
82
193
|
}
|
|
83
194
|
|
|
84
|
-
|
|
195
|
+
// Remove a runtime's record on shutdown. With a port, only THIS runtime's
|
|
196
|
+
// per-port file is removed; the shared pointer is repointed to a surviving
|
|
197
|
+
// fleet (sync, best-effort — no health check, this runs in process 'exit')
|
|
198
|
+
// instead of being deleted, so other live fleets stay discoverable. Called
|
|
199
|
+
// with no port it keeps the old behavior (delete the pointer).
|
|
200
|
+
function clearRuntimeSession(port) {
|
|
201
|
+
if (port) {
|
|
202
|
+
try { fs.unlinkSync(sessionFileForPort(port)); } catch { /* already gone */ }
|
|
203
|
+
let pointer = null;
|
|
204
|
+
try { pointer = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch { /* none */ }
|
|
205
|
+
// Only touch the pointer if it was pointing at us.
|
|
206
|
+
if (!pointer || pointer.port === port) {
|
|
207
|
+
const survivors = listRuntimeSessions();
|
|
208
|
+
if (survivors.length) {
|
|
209
|
+
try {
|
|
210
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(survivors[0], null, 2) + '\n', { mode: 0o600 });
|
|
211
|
+
} catch { /* best effort */ }
|
|
212
|
+
} else {
|
|
213
|
+
try { fs.unlinkSync(SESSION_FILE); } catch { /* already gone */ }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
85
218
|
try {
|
|
86
219
|
fs.unlinkSync(SESSION_FILE);
|
|
87
220
|
} catch {
|
|
@@ -321,16 +454,20 @@ function hasVisualAfter(afterTimestamp, options = {}) {
|
|
|
321
454
|
module.exports = {
|
|
322
455
|
SESSION_DIR,
|
|
323
456
|
SESSION_FILE,
|
|
457
|
+
SESSIONS_DIR,
|
|
324
458
|
TOKEN_FILE,
|
|
325
459
|
LEDGER_FILE,
|
|
326
460
|
VERIFICATION_LEDGER_FILE,
|
|
327
461
|
VISUAL_LEDGER_FILE,
|
|
328
462
|
VALID_VISUAL_STAGES,
|
|
329
463
|
findAvailablePort,
|
|
464
|
+
killStaleRuntime,
|
|
465
|
+
reclaimPort,
|
|
330
466
|
writeRuntimeSession,
|
|
331
467
|
updateRuntimeSession,
|
|
332
468
|
readRuntimeSession,
|
|
333
469
|
clearRuntimeSession,
|
|
470
|
+
listRuntimeSessions,
|
|
334
471
|
writeAuthToken,
|
|
335
472
|
readAuthToken,
|
|
336
473
|
healthCheckSession,
|
package/mcp-server.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Also starts HTTP server on port 3300 for browser UI.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const pkg = require('./package.json');
|
|
10
11
|
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
11
12
|
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
12
13
|
const {
|
|
@@ -359,7 +360,7 @@ httpServer.on('upgrade', (req, socket, head) => {
|
|
|
359
360
|
app.get('/health', (_req, res) => {
|
|
360
361
|
res.json({
|
|
361
362
|
status: 'ok',
|
|
362
|
-
version:
|
|
363
|
+
version: `${pkg.version}-mcp`,
|
|
363
364
|
terminals: terminals.size,
|
|
364
365
|
mode: 'mcp',
|
|
365
366
|
});
|
|
@@ -510,7 +511,7 @@ app.post('/api/terminals/:id/input', async (req, res) => {
|
|
|
510
511
|
// ── MCP Server Setup ────────────────────────────────────────
|
|
511
512
|
|
|
512
513
|
const mcpServer = new Server(
|
|
513
|
-
{ name: 'ninja-terminals', version:
|
|
514
|
+
{ name: 'ninja-terminals', version: pkg.version },
|
|
514
515
|
{ capabilities: { tools: {} } }
|
|
515
516
|
);
|
|
516
517
|
|
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 {
|
|
@@ -310,9 +311,18 @@ async function main() {
|
|
|
310
311
|
if (health.ok && !fresh && sessionMatchesLaunchConfig(existingSession, requestedCwd, launchConfig)) {
|
|
311
312
|
session = existingSession;
|
|
312
313
|
action = 'reuse';
|
|
314
|
+
} else if (health.ok && fresh) {
|
|
315
|
+
// --fresh explicitly asked to start over: only safe to reclaim a runtime
|
|
316
|
+
// that isn't healthy. killStaleRuntime health-gates, so a live fleet is
|
|
317
|
+
// never killed here either — we just start alongside it.
|
|
318
|
+
const killed = await killStaleRuntime(existingSession);
|
|
319
|
+
if (killed) log(`Terminated dead previous runtime (pid ${existingSession.pid})`);
|
|
313
320
|
} else if (health.ok) {
|
|
314
|
-
|
|
315
|
-
|
|
321
|
+
// A healthy fleet is running but doesn't match this launch (different
|
|
322
|
+
// project/mode, or older than the reuse window). Do NOT kill it — that
|
|
323
|
+
// would destroy a parallel instance's work. Start a new server alongside;
|
|
324
|
+
// it climbs to the next free port and the two coexist.
|
|
325
|
+
log('A different healthy Ninja fleet is running — starting a new one alongside it (it will use the next free port).');
|
|
316
326
|
}
|
|
317
327
|
}
|
|
318
328
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1033,7 +1033,8 @@ function setupAddTerminal() {
|
|
|
1033
1033
|
if (!btn) return;
|
|
1034
1034
|
|
|
1035
1035
|
// Store last used directory
|
|
1036
|
-
|
|
1036
|
+
// ponytail: empty default → server falls back to its own process.cwd(); never hardcode an author machine path
|
|
1037
|
+
let lastCwd = localStorage.getItem('ninja-last-cwd') || '';
|
|
1037
1038
|
let lastAgentType = localStorage.getItem('ninja-last-agent-type') || 'claude';
|
|
1038
1039
|
|
|
1039
1040
|
if (preset) {
|
package/public/index.html
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
|
|
10
10
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css">
|
|
11
11
|
<link rel="stylesheet" href="style.css">
|
|
12
|
+
<!-- PostHog analytics -->
|
|
13
|
+
<script>
|
|
14
|
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture register register_once register_for_session unregister unregister_for_session getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey getNextSurveyStep identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty createPersonProfile opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing debug getPageViewId captureTraceFeedback captureTraceMetric".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
15
|
+
posthog.init('phc_AmUYDCH793ekSGYpDxfoH8raVceuXjxnPxqF844i7u7A',{api_host:'https://us.i.posthog.com',defaults:'2025-05-24'})
|
|
16
|
+
</script>
|
|
12
17
|
</head>
|
|
13
18
|
<body>
|
|
14
19
|
<!-- Learnings Modal -->
|
package/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const pkg = require('./package.json');
|
|
1
2
|
const express = require('express');
|
|
2
3
|
const http = require('http');
|
|
3
4
|
const { WebSocketServer } = require('ws');
|
|
@@ -26,6 +27,9 @@ const { runPostSession } = require('./lib/post-session');
|
|
|
26
27
|
const {
|
|
27
28
|
SESSION_FILE,
|
|
28
29
|
findAvailablePort,
|
|
30
|
+
killStaleRuntime,
|
|
31
|
+
reclaimPort,
|
|
32
|
+
readRuntimeSession,
|
|
29
33
|
writeRuntimeSession,
|
|
30
34
|
updateRuntimeSession,
|
|
31
35
|
clearRuntimeSession,
|
|
@@ -179,8 +183,10 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
|
|
|
179
183
|
// Resolve working directory — custom cwd or default to PROJECT_DIR
|
|
180
184
|
// Validate cwd to prevent broken paths like "\" from corrupting terminal startup
|
|
181
185
|
let workDir = cwd || PROJECT_DIR;
|
|
182
|
-
|
|
183
|
-
|
|
186
|
+
// ponytail: must also exist on disk — a hardcoded/stale absolute path passes isAbsolute
|
|
187
|
+
// but kills pty.spawn ("Connection closed"). Fall back instead of dying.
|
|
188
|
+
if (!workDir || workDir.length < 2 || !path.isAbsolute(workDir) || !fs.existsSync(workDir)) {
|
|
189
|
+
console.warn(`[spawn] Invalid or missing cwd "${workDir}", falling back to PROJECT_DIR`);
|
|
184
190
|
workDir = PROJECT_DIR;
|
|
185
191
|
}
|
|
186
192
|
const settingsDir = workDir;
|
|
@@ -496,7 +502,7 @@ app.post('/api/upload', requireAuth, upload.single('file'), (req, res) => {
|
|
|
496
502
|
app.get('/health', (_req, res) => {
|
|
497
503
|
res.json({
|
|
498
504
|
status: 'ok',
|
|
499
|
-
version:
|
|
505
|
+
version: pkg.version,
|
|
500
506
|
terminals: terminals.size,
|
|
501
507
|
sseClients: sse.clientCount,
|
|
502
508
|
uptime: process.uptime(),
|
|
@@ -1305,6 +1311,18 @@ app.post('/api/auth/register', async (req, res) => {
|
|
|
1305
1311
|
// ── Start ───────────────────────────────────────────────────
|
|
1306
1312
|
|
|
1307
1313
|
async function startServer() {
|
|
1314
|
+
// Clean up only DEAD runtimes — never a healthy one. killStaleRuntime and
|
|
1315
|
+
// reclaimPort both health-gate: a runtime that answers /health is a live
|
|
1316
|
+
// parallel fleet (possibly another instance doing real work) and is left
|
|
1317
|
+
// untouched; this server then climbs to the next free port and coexists.
|
|
1318
|
+
// Only dead/hung leftovers get reclaimed (keeps the no-orphan-pileup win).
|
|
1319
|
+
const previous = readRuntimeSession();
|
|
1320
|
+
const replaced = await killStaleRuntime(previous);
|
|
1321
|
+
if (replaced) console.log(`[runtime] cleaned up dead Ninja runtime (pid ${previous.pid})`);
|
|
1322
|
+
|
|
1323
|
+
const reclaimed = await reclaimPort(PREFERRED_PORT);
|
|
1324
|
+
if (reclaimed) console.log(`[runtime] reclaimed port ${PREFERRED_PORT} from ${reclaimed} dead Ninja orphan(s)`);
|
|
1325
|
+
|
|
1308
1326
|
const selectedPort = await findAvailablePort(PREFERRED_PORT);
|
|
1309
1327
|
if (selectedPort !== PREFERRED_PORT) {
|
|
1310
1328
|
console.log(`[port] ${PREFERRED_PORT} is unavailable; using ${selectedPort}`);
|
|
@@ -1353,7 +1371,7 @@ async function startServer() {
|
|
|
1353
1371
|
process.on('exit', () => {
|
|
1354
1372
|
try {
|
|
1355
1373
|
const addr = server.address();
|
|
1356
|
-
if (addr && addr.port) clearRuntimeSession();
|
|
1374
|
+
if (addr && addr.port) clearRuntimeSession(addr.port);
|
|
1357
1375
|
} catch {
|
|
1358
1376
|
// ignore shutdown cleanup failures
|
|
1359
1377
|
}
|