mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
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:
+136
-1
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user