ninja-terminals 2.4.0 → 2.4.2

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/server.js CHANGED
@@ -35,11 +35,14 @@ const {
35
35
  } = require('./lib/runtime-session');
36
36
 
37
37
  // ── Config ──────────────────────────────────────────────────
38
- const PREFERRED_PORT = parseInt(process.env.PORT || '3300', 10);
38
+ const PREFERRED_PORT = parseInt(process.env.PORT || process.env.HTTP_PORT || '3300', 10);
39
39
  const BIND_HOST = process.env.NINJA_BIND_HOST || '127.0.0.1';
40
40
  const DEFAULT_TERMINALS = parseInt(process.env.DEFAULT_TERMINALS || '4', 10);
41
- const CLAUDE_CMD = process.env.CLAUDE_CMD || 'claude --dangerously-skip-permissions';
42
- const SHELL = process.env.SHELL || '/bin/zsh';
41
+ const CLAUDE_CMD = process.env.CLAUDE_CMD || process.env.CLAUDE_CHROME_CMD || 'claude --chrome --model claude-opus-4-5-20251101';
42
+ // Windows has no $SHELL / /bin/zsh — default to PowerShell (handles drive
43
+ // changes via `cd` and emits clean ANSI for status detection).
44
+ const IS_WIN = process.platform === 'win32';
45
+ const SHELL = IS_WIN ? (process.env.NINJA_SHELL || 'powershell.exe') : (process.env.SHELL || '/bin/zsh');
43
46
  const PROJECT_DIR = __dirname;
44
47
  const DEFAULT_CWD = process.env.DEFAULT_CWD || null; // Set to target project path to avoid cross-project prompts
45
48
  const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false'; // Default true, set INJECT_GUIDANCE=false to disable
@@ -47,12 +50,17 @@ const INJECT_GUIDANCE = process.env.INJECT_GUIDANCE !== 'false'; // Default tru
47
50
  // Fleet modes — preset terminal configurations
48
51
  const FLEET_MODES = {
49
52
  claude: ['claude', 'claude', 'claude', 'claude'],
53
+ codex: ['codex', 'codex', 'codex', 'codex'],
54
+ opencode: ['opencode', 'opencode', 'opencode', 'opencode'],
50
55
  teams: ['claude', 'opencode', 'codex', 'shell'],
51
56
  mixed: ['claude', 'claude', 'opencode', 'shell'],
52
57
  shell: ['shell', 'shell', 'shell', 'shell'],
53
58
  duo: ['claude', 'opencode'],
54
59
  };
55
60
  const FLEET_MODE = process.env.NINJA_MODE || 'claude';
61
+ // Resolve the claude binary from PATH by default so it works on any machine.
62
+ // Override with CLAUDE_CMD (full command) or CLAUDE_BIN (binary path) if needed.
63
+ const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
56
64
 
57
65
  const sleep = ms => new Promise(r => setTimeout(r, ms));
58
66
 
@@ -152,7 +160,7 @@ function getTerminalRules(terminalId) {
152
160
  // ── Terminal Spawning ───────────────────────────────────────
153
161
 
154
162
  const AGENT_COMMANDS = {
155
- claude: process.env.CLAUDE_CMD || 'claude --dangerously-skip-permissions',
163
+ claude: process.env.CLAUDE_CMD || `${CLAUDE_BIN} --dangerously-skip-permissions`,
156
164
  opencode: 'opencode',
157
165
  codex: 'codex',
158
166
  shell: null, // No command — just shell
@@ -189,7 +197,7 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
189
197
  }
190
198
  }
191
199
 
192
- const ptyProcess = pty.spawn(SHELL, ['-l'], {
200
+ const ptyProcess = pty.spawn(SHELL, IS_WIN ? [] : ['-l'], {
193
201
  name: 'xterm-256color',
194
202
  cols,
195
203
  rows,
@@ -197,8 +205,16 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
197
205
  env: {
198
206
  ...cleanEnv,
199
207
  TERM: 'xterm-256color',
208
+ COLORTERM: 'truecolor',
209
+ FORCE_COLOR: '1',
210
+ CLICOLOR_FORCE: '1',
200
211
  HOME: require('os').homedir(),
201
- PATH: `${require('os').homedir()}/.local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`,
212
+ // On Windows, keep the native PATH untouched (mac/linux bin dirs would
213
+ // corrupt it via the ';' delimiter). On POSIX, prepend the usual bins.
214
+ PATH: IS_WIN
215
+ ? (process.env.PATH || '')
216
+ : [`${require('os').homedir()}/.local/bin`, '/opt/homebrew/bin', process.env.PATH || '']
217
+ .filter(Boolean).join(path.delimiter),
202
218
  SHELL_SESSIONS_DISABLE: '1',
203
219
  NINJA_TERMINAL_ID: String(id),
204
220
  },
@@ -206,11 +222,13 @@ function spawnTerminal(label, scope = [], cwd = null, tier = 'pro', agentType =
206
222
 
207
223
  // After shell starts, cd to work dir and launch agent (if not shell)
208
224
  const agentCmd = AGENT_COMMANDS[agentType] || null;
225
+ // Two separate writes (cd, then command) instead of `&&` — works across
226
+ // bash/zsh/PowerShell/cmd, which disagree on chaining (`&&` is unsupported
227
+ // in Windows PowerShell 5).
209
228
  setTimeout(() => {
229
+ ptyProcess.write(`cd "${workDir}"\r`);
210
230
  if (agentCmd) {
211
- ptyProcess.write(`cd "${workDir}" && ${agentCmd}\r`);
212
- } else {
213
- ptyProcess.write(`cd "${workDir}"\r`);
231
+ setTimeout(() => ptyProcess.write(`${agentCmd}\r`), 250);
214
232
  }
215
233
  }, 500);
216
234
 
@@ -1292,13 +1310,16 @@ async function startServer() {
1292
1310
  server.listen(selectedPort, BIND_HOST, () => {
1293
1311
  const url = `http://localhost:${selectedPort}`;
1294
1312
  console.log(`[bind] Listening on ${BIND_HOST}:${selectedPort}`);
1313
+ const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
1314
+ const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
1295
1315
  const session = writeRuntimeSession({
1296
1316
  port: selectedPort,
1297
1317
  host: BIND_HOST,
1298
1318
  url,
1299
1319
  cwd: DEFAULT_CWD || process.cwd(),
1300
- terminals: DEFAULT_TERMINALS,
1320
+ terminals: terminalCount,
1301
1321
  command: 'ninja-terminals',
1322
+ launchConfig: { mode: FLEET_MODE, terminalCount },
1302
1323
  });
1303
1324
 
1304
1325
  console.log(`Ninja Terminals v2 running on ${url}`);
@@ -1314,8 +1335,6 @@ async function startServer() {
1314
1335
  startSessionHeartbeat(sessionCache, handleSessionInvalidation, 5 * 60 * 1000);
1315
1336
 
1316
1337
  // Auto-spawn terminals based on fleet mode
1317
- const fleetConfig = FLEET_MODES[FLEET_MODE] || FLEET_MODES.claude;
1318
- const terminalCount = DEFAULT_TERMINALS > 0 ? Math.min(DEFAULT_TERMINALS, fleetConfig.length) : fleetConfig.length;
1319
1338
  const agentLabels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', shell: 'Shell' };
1320
1339
 
1321
1340
  console.log(`Auto-spawning ${terminalCount} terminals (mode: ${FLEET_MODE})...`);
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
-
8
- const NINJA_DIR = path.join(os.homedir(), '.ninja');
9
- const REQUEST_FILE = path.join(NINJA_DIR, 'ninja-request.json');
10
-
11
- const NINJA_PATTERNS = [
12
- /ninja\s*terminal/i,
13
- /use\s+ninja/i,
14
- /build\s+with\s+ninja/i,
15
- /orchestrat/i,
16
- /\bPRD\b.*\bbuild\b/i,
17
- /\bbuild\b.*\bPRD\b/i,
18
- ];
19
-
20
- function isNinjaRequest(prompt) {
21
- if (!prompt) return false;
22
- return NINJA_PATTERNS.some(pattern => pattern.test(prompt));
23
- }
24
-
25
- function writeNinjaRequest(cwd, promptPreview) {
26
- try {
27
- fs.mkdirSync(NINJA_DIR, { recursive: true, mode: 0o700 });
28
- const data = {
29
- timestamp: new Date().toISOString(),
30
- cwd: cwd || process.cwd(),
31
- promptPreview: (promptPreview || '').slice(0, 200),
32
- };
33
- fs.writeFileSync(REQUEST_FILE, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
34
- return data;
35
- } catch (err) {
36
- console.error(`Warning: Could not write ninja request: ${err.message}`);
37
- return null;
38
- }
39
- }
40
-
41
- module.exports = {
42
- NINJA_DIR,
43
- REQUEST_FILE,
44
- isNinjaRequest,
45
- writeNinjaRequest,
46
- };
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const { isNinjaRequest, writeNinjaRequest } = require('./ninja-common');
5
-
6
- function main() {
7
- let input = '';
8
- process.stdin.setEncoding('utf8');
9
- process.stdin.on('data', chunk => { input += chunk; });
10
- process.stdin.on('end', () => {
11
- try {
12
- const event = JSON.parse(input);
13
- const prompt = event.prompt || '';
14
- const cwd = event.cwd || process.cwd();
15
-
16
- if (isNinjaRequest(prompt)) {
17
- writeNinjaRequest(cwd, prompt);
18
- const output = {
19
- result: 'continue',
20
- message: `NINJA REQUEST DETECTED. Follow ORCHESTRATOR-PROMPT.md workflow.`,
21
- };
22
- console.log(JSON.stringify(output));
23
- } else {
24
- console.log(JSON.stringify({ result: 'continue' }));
25
- }
26
- } catch (err) {
27
- console.error(`Warning: ninja-prompt-submit hook error: ${err.message}`);
28
- console.log(JSON.stringify({ result: 'continue' }));
29
- }
30
- });
31
- }
32
-
33
- main();
@@ -1,95 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const http = require('http');
5
- const { execSync } = require('child_process');
6
-
7
- const PORT = parseInt(process.env.HTTP_PORT || '3300', 10);
8
- const MAX_WAIT_MS = 10000;
9
- const POLL_INTERVAL_MS = 500;
10
-
11
- function log(msg) {
12
- process.stderr.write(`[ninja-startup] ${msg}\n`);
13
- }
14
-
15
- function healthCheck(port) {
16
- return new Promise((resolve) => {
17
- const req = http.get(`http://localhost:${port}/health`, (res) => {
18
- let data = '';
19
- res.on('data', chunk => data += chunk);
20
- res.on('end', () => {
21
- try {
22
- const json = JSON.parse(data);
23
- resolve(json.status === 'ok');
24
- } catch {
25
- resolve(false);
26
- }
27
- });
28
- });
29
- req.on('error', () => resolve(false));
30
- req.setTimeout(2000, () => {
31
- req.destroy();
32
- resolve(false);
33
- });
34
- });
35
- }
36
-
37
- async function waitForHealth(port, maxWait = MAX_WAIT_MS) {
38
- const start = Date.now();
39
- while (Date.now() - start < maxWait) {
40
- if (await healthCheck(port)) return true;
41
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
42
- }
43
- return false;
44
- }
45
-
46
- function openInBrowser(url) {
47
- try {
48
- if (process.platform === 'darwin') {
49
- execSync(`open "${url}"`, { stdio: 'ignore' });
50
- } else if (process.platform === 'linux') {
51
- execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
52
- } else if (process.platform === 'win32') {
53
- execSync(`start "" "${url}"`, { stdio: 'ignore', shell: true });
54
- }
55
- } catch {
56
- log(`Could not auto-open ${url}`);
57
- }
58
- }
59
-
60
- async function main() {
61
- // MCP server should already be running (started by Claude Code via mcpServers config)
62
- // Just wait for it to be healthy
63
- log(`Checking Ninja Terminal server on port ${PORT}...`);
64
-
65
- const healthy = await waitForHealth(PORT);
66
-
67
- if (!healthy) {
68
- // Server not running - tell user to check MCP config
69
- console.log(JSON.stringify({
70
- status: 'error',
71
- message: `Ninja Terminal server not responding on port ${PORT}. Check MCP config or run: npx ninja-terminals`,
72
- }));
73
- process.exit(0); // Don't fail the hook, just report
74
- }
75
-
76
- const mainUrl = `http://localhost:${PORT}/`;
77
- const logViewerUrl = `http://localhost:${PORT}/log-viewer.html`;
78
-
79
- log('Opening Ninja UI in browser...');
80
- openInBrowser(mainUrl);
81
-
82
- await new Promise(r => setTimeout(r, 300));
83
- openInBrowser(logViewerUrl);
84
-
85
- console.log(JSON.stringify({
86
- status: 'ready',
87
- port: PORT,
88
- urls: { main: mainUrl, logViewer: logViewerUrl },
89
- }));
90
- }
91
-
92
- main().catch(err => {
93
- log(`Error: ${err.message}`);
94
- process.exit(0); // Don't fail session start
95
- });