@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
495 lines • 19.2 kB
JavaScript
/**
* Simplified Process UI without keypress dependency
* Uses basic stdin reading for compatibility
*/
import chalk from "chalk";
import { ProcessStatus } from "./types.js";
// Color compatibility
const colors = {
gray: chalk.gray,
yellow: chalk.yellow,
red: chalk.red,
green: chalk.green,
cyan: chalk.cyan,
blue: chalk.blue,
bold: chalk.bold,
white: chalk.white,
dim: chalk.dim,
};
export class ProcessUI {
processManager;
running = false;
selectedIndex = 0;
constructor(processManager) {
this.processManager = processManager;
this.setupEventListeners();
}
setupEventListeners() {
this.processManager.on("statusChanged", ({ processId: _processId, status: _status }) => {
if (this.running) {
this.render();
}
});
this.processManager.on("processError", ({ processId, error }) => {
if (this.running) {
console.log(colors.red(`\nProcess ${processId} error: ${error.message}`));
}
});
// Setup graceful shutdown handlers
process.on("SIGINT", () => {
this.stop();
process.exit(0);
});
process.on("SIGTERM", () => {
this.stop();
process.exit(0);
});
}
async start() {
this.running = true;
this.setupEventListeners();
// Set raw mode for single character input
if (process.stdin.setRawMode) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
this.render();
// Check for auto-launch to Claude interactive shell
await this.checkAutoLaunchClaude();
// Main input loop
while (this.running) {
const input = await this.readInput();
await this.handleCommand(input);
if (this.running) {
this.render();
}
}
// Cleanup
if (process.stdin.setRawMode) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
}
async checkAutoLaunchClaude() {
const stats = this.processManager.getSystemStats();
// Check if all processes are running successfully
if (stats.runningProcesses === stats.totalProcesses && stats.errorProcesses === 0) {
console.log();
console.log(colors.green.bold("🎉 All processes started successfully!"));
console.log();
console.log(colors.cyan("Auto-launching Claude interactive shell in 5 seconds..."));
console.log(colors.gray("Press any key to stay in process manager"));
// Countdown with ability to cancel
let countdown = 5;
let cancelled = false;
const countdownInterval = setInterval(() => {
if (cancelled) {
clearInterval(countdownInterval);
return;
}
process.stdout.write(`\r${colors.cyan(`Launching in ${countdown}...`)} ${colors.gray("(Press any key to cancel)")}`);
countdown--;
if (countdown < 0) {
clearInterval(countdownInterval);
if (!cancelled) {
console.log();
this.launchClaudeInteractiveShell();
}
}
}, 1000);
// Listen for any key press to cancel
const cancelPromise = new Promise((resolve) => {
const onData = () => {
cancelled = true;
clearInterval(countdownInterval);
process.stdin.off("data", onData);
console.log();
console.log(colors.yellow("Auto-launch cancelled. Staying in process manager."));
console.log();
resolve();
};
process.stdin.once("data", onData);
});
// Wait for either countdown or cancellation
await Promise.race([
cancelPromise,
new Promise(resolve => setTimeout(resolve, 6000)),
]);
}
}
async launchClaudeInteractiveShell() {
try {
console.log(colors.green.bold("🚀 Launching Claude Interactive Shell..."));
console.log(colors.gray("─".repeat(60)));
// Import and start Claude interactive shell
const { spawn } = await import("child_process");
// Try different Claude command variations
const claudeCommands = [
"claude",
"npx claude",
"npx @anthropic-ai/claude",
"npx claude-ai",
];
let claudeLaunched = false;
for (const cmd of claudeCommands) {
try {
console.log(colors.blue(`Trying: ${cmd}`));
const [command, ...args] = cmd.split(" ");
const claudeProcess = spawn(command, args, {
stdio: "inherit",
shell: true,
env: {
...process.env,
CLAUDE_FLOW_ACTIVE: "true",
CLAUDE_FLOW_PORT: "3000",
},
});
claudeProcess.on("error", (error) => {
console.log(colors.yellow(`${cmd} not available: ${error.message}`));
});
claudeProcess.on("exit", (code) => {
if (code === 0) {
console.log(colors.green("Claude session ended successfully"));
}
else {
console.log(colors.yellow(`Claude exited with code ${code}`));
}
// Return to process manager
console.log(colors.cyan("Returning to Claude-Flow Process Manager..."));
this.render();
});
claudeLaunched = true;
break;
}
catch (error) {
continue; // Try next command
}
}
if (!claudeLaunched) {
console.log(colors.yellow("⚠️ Claude CLI not found. Install with: npm install -g @anthropic-ai/claude"));
console.log(colors.gray("Alternatively, you can:"));
console.log(colors.gray("• Use the MCP server at http://localhost:3000"));
console.log(colors.gray("• Connect your preferred Claude client to the running services"));
console.log();
console.log(colors.gray("Press any key to continue..."));
await this.waitForKey();
}
}
catch (error) {
console.log(colors.red(`Failed to launch Claude: ${error.message}`));
console.log(colors.gray("Press any key to continue..."));
await this.waitForKey();
}
}
stop() {
this.running = false;
}
async handleCommand(input) {
const processes = this.processManager.getAllProcesses();
switch (input.toLowerCase()) {
case "q":
case "quit":
case "exit":
await this.handleExit();
break;
case "a":
case "all":
await this.startAll();
break;
case "z":
case "stop-all":
await this.stopAll();
break;
case "r":
case "refresh":
this.render();
break;
case "h":
case "help":
case "?":
this.showHelp();
break;
default: {
// Check if it's a number (process selection)
const num = parseInt(input);
if (!isNaN(num) && num >= 1 && num <= processes.length) {
this.selectedIndex = num - 1;
await this.showProcessMenu(processes[this.selectedIndex]);
}
else {
console.log(colors.yellow("Invalid command. Type \"h\" for help."));
}
break;
}
}
}
render() {
console.clear();
const processes = this.processManager.getAllProcesses();
const stats = this.processManager.getSystemStats();
// Header
console.log(colors.cyan.bold("🧠 Claude-Flow Process Manager"));
console.log(colors.gray("─".repeat(60)));
// System stats
console.log(colors.white("System Status:"), colors.green(`${stats.runningProcesses}/${stats.totalProcesses} running`));
if (stats.errorProcesses > 0) {
console.log(colors.red(`⚠️ ${stats.errorProcesses} processes with errors`));
}
console.log();
// Process list
console.log(colors.white.bold("Processes:"));
console.log(colors.gray("─".repeat(60)));
processes.forEach((process, index) => {
const num = `[${index + 1}]`.padEnd(4);
const status = this.getStatusDisplay(process.status);
const name = process.name.padEnd(25);
console.log(`${colors.gray(num)} ${status} ${colors.white(name)}`);
if (process.metrics?.lastError) {
console.log(colors.red(` Error: ${process.metrics.lastError}`));
}
});
// Footer
console.log(colors.gray("─".repeat(60)));
console.log(colors.gray("Commands: [1-9] Select process [a] Start All [z] Stop All"));
console.log(colors.gray("[r] Refresh [h] Help [q] Quit"));
}
async showProcessMenu(process) {
console.log();
console.log(colors.cyan.bold(`Selected: ${process.name}`));
console.log(colors.gray("─".repeat(40)));
if (process.status === ProcessStatus.STOPPED) {
console.log("[s] Start");
}
else if (process.status === ProcessStatus.RUNNING) {
console.log("[x] Stop");
console.log("[r] Restart");
}
console.log("[d] Details");
console.log("[c] Cancel");
const decoder = new TextDecoder();
const encoder = new TextEncoder();
await new Promise((resolve) => {
globalThis.process.stdout.write("\nAction: ", () => resolve());
});
const buf = new Uint8Array(1024);
const n = await new Promise((resolve) => {
globalThis.process.stdin.once("data", (data) => {
const bytes = encoder.encode(data.toString());
buf.set(bytes);
resolve(bytes.length);
});
});
if (n === null)
return;
const action = decoder.decode(buf.subarray(0, n)).trim().toLowerCase();
switch (action) {
case "s":
if (process.status === ProcessStatus.STOPPED) {
await this.startProcess(process.id);
}
break;
case "x":
if (process.status === ProcessStatus.RUNNING) {
await this.stopProcess(process.id);
}
break;
case "r":
if (process.status === ProcessStatus.RUNNING) {
await this.restartProcess(process.id);
}
break;
case "d":
this.showProcessDetails(process);
await this.waitForKey();
break;
}
this.render();
}
showProcessDetails(process) {
console.log();
console.log(colors.cyan.bold(`📋 Process Details: ${process.name}`));
console.log(colors.gray("─".repeat(60)));
console.log(colors.white("ID:"), process.id);
console.log(colors.white("Type:"), process.type);
console.log(colors.white("Status:"), this.getStatusDisplay(process.status), process.status);
if (process.pid) {
console.log(colors.white("PID:"), process.pid);
}
if (process.startTime) {
const uptime = Date.now() - process.startTime;
console.log(colors.white("Uptime:"), this.formatUptime(uptime));
}
if (process.metrics) {
console.log();
console.log(colors.white.bold("Metrics:"));
if (process.metrics.cpu !== undefined) {
console.log(colors.white("CPU:"), `${process.metrics.cpu.toFixed(1)}%`);
}
if (process.metrics.memory !== undefined) {
console.log(colors.white("Memory:"), `${process.metrics.memory.toFixed(0)} MB`);
}
if (process.metrics.restarts !== undefined) {
console.log(colors.white("Restarts:"), process.metrics.restarts);
}
if (process.metrics.lastError) {
console.log(colors.red("Last Error:"), process.metrics.lastError);
}
}
console.log();
console.log(colors.gray("Press any key to continue..."));
}
async waitForKey() {
const _buf = new Uint8Array(1);
await new Promise((resolve) => {
process.stdin.once("data", () => resolve(undefined));
});
}
getStatusDisplay(status) {
switch (status) {
case ProcessStatus.RUNNING:
return colors.green("●");
case ProcessStatus.STOPPED:
return colors.gray("○");
case ProcessStatus.STARTING:
return colors.yellow("◐");
case ProcessStatus.STOPPING:
return colors.yellow("◑");
case ProcessStatus.ERROR:
return colors.red("✗");
case ProcessStatus.CRASHED:
return colors.red("☠");
default:
return colors.gray("?");
}
}
formatUptime(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
}
else if (hours > 0) {
return `${hours}h ${minutes % 60}m`;
}
else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
else {
return `${seconds}s`;
}
}
showHelp() {
console.log();
console.log(colors.cyan.bold("🧠 Claude-Flow Process Manager - Help"));
console.log(colors.gray("─".repeat(60)));
console.log();
console.log(colors.white.bold("Commands:"));
console.log(" 1-9 - Select process by number");
console.log(" a - Start all processes");
console.log(" z - Stop all processes");
console.log(" r - Refresh display");
console.log(" h/? - Show this help");
console.log(" q - Quit");
console.log();
console.log(colors.white.bold("Process Actions:"));
console.log(" s - Start selected process");
console.log(" x - Stop selected process");
console.log(" r - Restart selected process");
console.log(" d - Show process details");
console.log();
console.log(colors.gray("Press any key to continue..."));
}
async startProcess(processId) {
try {
console.log(colors.yellow(`Starting ${processId}...`));
await this.processManager.startProcess(processId);
console.log(colors.green(`✓ Started ${processId}`));
}
catch (error) {
console.log(colors.red(`✗ Failed to start ${processId}: ${error.message}`));
}
await this.waitForKey();
}
async stopProcess(processId) {
try {
console.log(colors.yellow(`Stopping ${processId}...`));
await this.processManager.stopProcess(processId);
console.log(colors.green(`✓ Stopped ${processId}`));
}
catch (error) {
console.log(colors.red(`✗ Failed to stop ${processId}: ${error.message}`));
}
await this.waitForKey();
}
async restartProcess(processId) {
try {
console.log(colors.yellow(`Restarting ${processId}...`));
await this.processManager.restartProcess(processId);
console.log(colors.green(`✓ Restarted ${processId}`));
}
catch (error) {
console.log(colors.red(`✗ Failed to restart ${processId}: ${error.message}`));
}
await this.waitForKey();
}
async startAll() {
try {
console.log(colors.yellow("Starting all processes..."));
await this.processManager.startAll();
console.log(colors.green("✓ All processes started"));
}
catch (error) {
console.log(colors.red(`✗ Failed to start all: ${error.message}`));
}
await this.waitForKey();
this.render();
}
async stopAll() {
try {
console.log(colors.yellow("Stopping all processes..."));
await this.processManager.stopAll();
console.log(colors.green("✓ All processes stopped"));
}
catch (error) {
console.log(colors.red(`✗ Failed to stop all: ${error.message}`));
}
await this.waitForKey();
this.render();
}
async handleExit() {
const processes = this.processManager.getAllProcesses();
const hasRunning = processes.some(p => p.status === ProcessStatus.RUNNING);
if (hasRunning) {
console.log();
console.log(colors.yellow("⚠️ Some processes are still running."));
console.log("Stop all processes before exiting? [y/N]: ");
const decoder = new TextDecoder();
const buf = new Uint8Array(1024);
const n = await new Promise((resolve) => {
process.stdin.once("data", (data) => {
const bytes = Buffer.from(data);
bytes.copy(buf);
resolve(bytes.length);
});
});
if (n && decoder.decode(buf.subarray(0, n)).trim().toLowerCase() === "y") {
await this.stopAll();
}
}
await this.stop();
}
async readInput() {
return new Promise((resolve) => {
const decoder = new TextDecoder();
const onData = (data) => {
process.stdin.off("data", onData);
const input = decoder.decode(data).trim();
resolve(input);
};
process.stdin.once("data", onData);
});
}
}
//# sourceMappingURL=process-ui-simple.js.map