ninja-terminals 2.4.4 → 2.4.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/cli.js +11 -27
- package/lib/runtime-session.js +21 -9
- package/mcp-server.js +2 -2
- package/package.json +1 -1
- package/server.js +1 -1
package/cli.js
CHANGED
|
@@ -225,6 +225,14 @@ process.env.HTTP_PORT = String(port);
|
|
|
225
225
|
process.env.DEFAULT_TERMINALS = String(terminals);
|
|
226
226
|
process.env.DEFAULT_CWD = cwd;
|
|
227
227
|
|
|
228
|
+
// Single browser-open path: server.js opens the tab in its listen callback
|
|
229
|
+
// (knows the actual selected port, uses 127.0.0.1). Enable it by default for
|
|
230
|
+
// CLI launches; opt out with --no-open. This replaces cli.js's own opener,
|
|
231
|
+
// which previously fired a second, redundant tab.
|
|
232
|
+
if (!hasFlag('--no-open') && !process.env.NINJA_OPEN_BROWSER) {
|
|
233
|
+
process.env.NINJA_OPEN_BROWSER = '1';
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
// Auth env vars
|
|
229
237
|
if (token) {
|
|
230
238
|
process.env.NINJA_AUTH_TOKEN = token;
|
|
@@ -233,33 +241,9 @@ if (offline) {
|
|
|
233
241
|
process.env.NINJA_OFFLINE = '1';
|
|
234
242
|
}
|
|
235
243
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const { spawn } = require('child_process');
|
|
240
|
-
const platform = process.platform;
|
|
241
|
-
let cmd, cmdArgs;
|
|
242
|
-
|
|
243
|
-
if (platform === 'darwin') {
|
|
244
|
-
cmd = 'open';
|
|
245
|
-
cmdArgs = [url];
|
|
246
|
-
} else if (platform === 'win32') {
|
|
247
|
-
cmd = 'cmd';
|
|
248
|
-
cmdArgs = ['/c', 'start', url];
|
|
249
|
-
} else {
|
|
250
|
-
cmd = 'xdg-open';
|
|
251
|
-
cmdArgs = [url];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
spawn(cmd, cmdArgs, { stdio: 'ignore', detached: true }).unref();
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Delay browser open until after the server listen callback fires.
|
|
258
|
-
// server.js calls server.listen() synchronously on require, so we
|
|
259
|
-
// schedule the open after the current tick stack clears.
|
|
260
|
-
setTimeout(() => {
|
|
261
|
-
openBrowser(`http://localhost:${port}`);
|
|
262
|
-
}, 1500);
|
|
244
|
+
// Browser auto-open is handled by server.js's listen callback (single path,
|
|
245
|
+
// gated by NINJA_OPEN_BROWSER which we set above). cli.js no longer opens its
|
|
246
|
+
// own tab — that produced a duplicate.
|
|
263
247
|
|
|
264
248
|
// ── Start the server ─────────────────────────────────────────
|
|
265
249
|
|
package/lib/runtime-session.js
CHANGED
|
@@ -18,17 +18,29 @@ function ensureSessionDir() {
|
|
|
18
18
|
fs.mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// Probe a single host: 'free' (bindable), 'inuse' (EADDRINUSE), or
|
|
22
|
+
// 'unavailable' (stack absent / other error — don't treat as a collision).
|
|
23
|
+
function probePort(port, host) {
|
|
22
24
|
return new Promise((resolve) => {
|
|
23
25
|
const server = net.createServer();
|
|
24
|
-
server.once('error', () => resolve(
|
|
25
|
-
server.once('listening', () =>
|
|
26
|
-
server.close(() => resolve(true));
|
|
27
|
-
});
|
|
26
|
+
server.once('error', (err) => resolve(err.code === 'EADDRINUSE' ? 'inuse' : 'unavailable'));
|
|
27
|
+
server.once('listening', () => server.close(() => resolve('free')));
|
|
28
28
|
server.listen(port, host);
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// A port is available only if it's NOT in use on EITHER IP stack. A listener
|
|
33
|
+
// on IPv6 wildcard (e.g. *:3300, common with other local MCP servers) leaves
|
|
34
|
+
// IPv4 127.0.0.1 genuinely free, so an IPv4-only check would miss it and the
|
|
35
|
+
// browser (which resolves localhost -> IPv6 first) would hit the wrong server.
|
|
36
|
+
async function isPortAvailable(port) {
|
|
37
|
+
const [v4, v6] = await Promise.all([
|
|
38
|
+
probePort(port, '127.0.0.1'),
|
|
39
|
+
probePort(port, '::1'),
|
|
40
|
+
]);
|
|
41
|
+
return v4 !== 'inuse' && v6 !== 'inuse';
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts = 50) {
|
|
33
45
|
const start = Number.parseInt(preferredPort, 10);
|
|
34
46
|
if (!Number.isInteger(start) || start < 1 || start > 65535) {
|
|
@@ -36,7 +48,7 @@ async function findAvailablePort(preferredPort, host = '127.0.0.1', maxAttempts
|
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
for (let port = start; port < start + maxAttempts && port <= 65535; port++) {
|
|
39
|
-
if (await isPortAvailable(port
|
|
51
|
+
if (await isPortAvailable(port)) return port;
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
throw new Error(`No available port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`);
|
|
@@ -47,7 +59,7 @@ function writeRuntimeSession(session) {
|
|
|
47
59
|
const payload = {
|
|
48
60
|
version: 1,
|
|
49
61
|
pid: process.pid,
|
|
50
|
-
host: '
|
|
62
|
+
host: '127.0.0.1',
|
|
51
63
|
createdAt: new Date().toISOString(),
|
|
52
64
|
...session,
|
|
53
65
|
};
|
|
@@ -90,7 +102,7 @@ function readAuthToken() {
|
|
|
90
102
|
}
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
function requestJson({ host = '
|
|
105
|
+
function requestJson({ host = '127.0.0.1', port, path: reqPath, token, method = 'GET', body = null, timeoutMs = 3000 }) {
|
|
94
106
|
return new Promise((resolve, reject) => {
|
|
95
107
|
const payload = body ? JSON.stringify(body) : null;
|
|
96
108
|
const headers = {};
|
|
@@ -134,7 +146,7 @@ async function healthCheckSession(session, timeoutMs = 3000) {
|
|
|
134
146
|
if (!session || !session.port) return { ok: false, error: 'No runtime session port' };
|
|
135
147
|
try {
|
|
136
148
|
const res = await requestJson({
|
|
137
|
-
host: session.host || '
|
|
149
|
+
host: session.host || '127.0.0.1',
|
|
138
150
|
port: session.port,
|
|
139
151
|
path: '/health',
|
|
140
152
|
timeoutMs,
|
package/mcp-server.js
CHANGED
|
@@ -668,8 +668,8 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
668
668
|
label: terminal.label,
|
|
669
669
|
status: terminal.status,
|
|
670
670
|
cwd: terminal.cwd,
|
|
671
|
-
webUrl: `http://
|
|
672
|
-
wsUrl: `ws://
|
|
671
|
+
webUrl: `http://127.0.0.1:${HTTP_PORT}`,
|
|
672
|
+
wsUrl: `ws://127.0.0.1:${HTTP_PORT}/ws/${terminal.id}`,
|
|
673
673
|
}, null, 2),
|
|
674
674
|
}],
|
|
675
675
|
};
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1311,7 +1311,7 @@ async function startServer() {
|
|
|
1311
1311
|
}
|
|
1312
1312
|
|
|
1313
1313
|
server.listen(selectedPort, BIND_HOST, () => {
|
|
1314
|
-
const url = `http://
|
|
1314
|
+
const url = `http://127.0.0.1:${selectedPort}`;
|
|
1315
1315
|
console.log(`[bind] Listening on ${BIND_HOST}:${selectedPort}`);
|
|
1316
1316
|
const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
|
|
1317
1317
|
const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
|