feat(tui): add web server auto-start, shell mode, and feature parity

TUI now checks if web server is running on startup and offers to start
it in the background. Added new CLI options: --with-web (auto-start),
--no-web (skip check), -p (port).

TUI feature parity with web interface:
- Shell mode: press 'h' in cases view to start bash instead of Claude
- Multi-start: press 'm' to start 1-20 sessions at once
- Respawn toggle: Ctrl+R to enable/disable respawn on Claude sessions
- Session rename: API support via useSessionManager hook

Security fixes from previous analysis:
- Command injection prevention in screen-manager.ts
- Path traversal protection in server.ts
- Input validation for shell-interpolated values

Also fixes memory leak in session.ts (timer tracking) and flaky test
timeout in session-cleanup.test.ts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 09:54:42 +01:00
co-authored by Claude Opus 4.5
parent 3782ec2a74
commit d5ae376e07
13 changed files with 1258 additions and 192 deletions
+136 -1
View File
@@ -11,6 +11,7 @@
* - Real-time terminal output via screen hardcopy polling
* - Ralph Wiggum loop tracking
* - Respawn status monitoring
* - Auto-detection and startup of web server if not running
*
* @example
* ```bash
@@ -25,6 +26,8 @@
*/
import { render } from 'ink';
import { spawn } from 'child_process';
import { createInterface } from 'readline';
import { App } from './App.js';
/**
@@ -43,6 +46,88 @@ function isRawModeSupported(): boolean {
);
}
/**
* Checks if the Claudeman web server is running.
*
* @param port - Port to check (default 3000)
* @returns Promise that resolves to true if server is running
*/
async function isWebServerRunning(port: number = 3000): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
const response = await fetch(`http://localhost:${port}/api/status`, {
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok;
} catch {
return false;
}
}
/**
* Prompts the user with a yes/no question.
*
* @param question - The question to ask
* @returns Promise that resolves to true for yes, false for no
*/
async function promptYesNo(question: string): Promise<boolean> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${question} [Y/n] `, (answer) => {
rl.close();
const normalized = answer.trim().toLowerCase();
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
});
});
}
/**
* Starts the Claudeman web server in the background.
*
* @param port - Port to run on (default 3000)
* @returns true if server started successfully
*/
function startWebServerInBackground(port: number = 3000): boolean {
try {
// Find the current script's directory to locate the web server entry
const child = spawn('node', [
'--import', 'tsx/esm',
`${process.cwd()}/src/index.ts`,
'web',
'-p', String(port),
], {
detached: true,
stdio: 'ignore',
cwd: process.cwd(),
});
child.unref();
return true;
} catch {
return false;
}
}
/**
* TUI startup options.
*/
interface TUIOptions {
/** Auto-start web server without prompting if not running */
autoStartWeb?: boolean;
/** Skip web server check entirely */
skipWebCheck?: boolean;
/** Web server port */
port?: number;
}
/**
* Starts the TUI application in the current terminal.
*
@@ -51,10 +136,16 @@ function isRawModeSupported(): boolean {
* The terminal is cleared for a full-screen experience.
* This function blocks until the user exits the TUI.
*
* Before starting, checks if the web server is running and offers to start it
* in the background if not (unless skipWebCheck is true).
*
* @param options - TUI startup options
* @throws Exits with code 1 if TTY/raw mode is not supported
* @returns Promise that resolves when the TUI exits
*/
export async function startTUI(): Promise<void> {
export async function startTUI(options: TUIOptions = {}): Promise<void> {
const { autoStartWeb = false, skipWebCheck = false, port = 3000 } = options;
// Check if we're in an interactive terminal
if (!isRawModeSupported()) {
console.error('Error: TUI requires an interactive terminal with TTY support.');
@@ -62,6 +153,50 @@ export async function startTUI(): Promise<void> {
process.exit(1);
}
// Check if web server is running (unless skipped)
if (!skipWebCheck) {
const serverRunning = await isWebServerRunning(port);
if (!serverRunning) {
console.log('\x1b[33mClaudeman web server is not running.\x1b[0m');
console.log('The TUI requires the web server to create and manage sessions.\n');
let startServer = autoStartWeb;
if (!autoStartWeb) {
startServer = await promptYesNo('Would you like to start the web server in the background?');
}
if (startServer) {
console.log(`\x1b[32mStarting web server on port ${port}...\x1b[0m`);
const success = startWebServerInBackground(port);
if (success) {
// Wait a moment for the server to start
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify it started
const nowRunning = await isWebServerRunning(port);
if (nowRunning) {
console.log('\x1b[32mWeb server started successfully!\x1b[0m\n');
} else {
console.log('\x1b[33mServer may still be starting... continuing with TUI.\x1b[0m');
console.log('If you have issues, try running "claudeman web" in a separate terminal.\n');
}
} else {
console.log('\x1b[31mFailed to start web server.\x1b[0m');
console.log('Please run "claudeman web" in a separate terminal first.\n');
}
} else {
console.log('\nYou can start the web server anytime with: claudeman web');
console.log('Some TUI features may not work without the web server.\n');
}
// Brief pause before clearing screen
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Clear the terminal for full-screen experience
process.stdout.write('\x1b[2J\x1b[H');