agent-ecosystem/src/main/standalone.ts
2026-02-22 23:36:11 +02:00

194 lines
6.4 KiB
TypeScript

/**
* Standalone (non-Electron) entry point for Claude Agent Teams UI.
*
* Runs the HTTP server + API without Electron, suitable for Docker
* or any headless/remote environment. The renderer is served as
* static files over HTTP.
*
* Environment variables:
* - HOST: Bind address (default '0.0.0.0')
* - PORT: Listen port (default 3456)
* - CLAUDE_ROOT: Path to .claude directory (default ~/.claude)
* - CORS_ORIGIN: CORS origin policy (default '*')
*/
import { createLogger } from '@shared/utils/logger';
import { HttpServer } from './services/infrastructure/HttpServer';
import {
getProjectsBasePath,
getTodosBasePath,
setClaudeBasePathOverride,
} from './utils/pathDecoder';
import { LocalFileSystemProvider, NotificationManager, ServiceContext } from './services';
import type { HttpServices } from './http';
import type { SshConnectionManager } from './services/infrastructure/SshConnectionManager';
import type { UpdaterService } from './services/infrastructure/UpdaterService';
const logger = createLogger('Standalone');
// =============================================================================
// Configuration
// =============================================================================
const HOST = process.env.HOST ?? '0.0.0.0';
const PORT = parseInt(process.env.PORT ?? '3456', 10);
const CLAUDE_ROOT = process.env.CLAUDE_ROOT;
// Default CORS to allow all in standalone mode (Docker isolation replaces CORS)
if (!process.env.CORS_ORIGIN) {
process.env.CORS_ORIGIN = '*';
}
// =============================================================================
// Stub services (Electron-only features unavailable in standalone)
// =============================================================================
/** No-op UpdaterService stub — auto-updater requires Electron. */
const updaterServiceStub = {
checkForUpdates: async () => {},
downloadUpdate: async () => {},
quitAndInstall: () => {},
setMainWindow: () => {},
} as unknown as UpdaterService;
/** No-op SshConnectionManager stub — SSH is managed per-user in the Electron app. */
const sshConnectionManagerStub = {
getStatus: () => ({
state: 'disconnected' as const,
host: null,
error: null,
remoteProjectsPath: null,
}),
getProvider: () => new LocalFileSystemProvider(),
isRemote: () => false,
connect: async () => {},
disconnect: () => {},
testConnection: async () => ({ success: false, error: 'SSH not available in standalone mode' }),
getConfigHosts: async () => [],
resolveHostConfig: async () => null,
dispose: () => {},
on: () => sshConnectionManagerStub,
off: () => sshConnectionManagerStub,
emit: () => false,
} as unknown as SshConnectionManager;
// =============================================================================
// Application State
// =============================================================================
let localContext: ServiceContext;
let notificationManager: NotificationManager;
let httpServer: HttpServer;
// =============================================================================
// Lifecycle
// =============================================================================
async function start(): Promise<void> {
logger.info('Starting standalone server...');
// Apply Claude root override if set
if (CLAUDE_ROOT) {
setClaudeBasePathOverride(CLAUDE_ROOT);
logger.info(`Using CLAUDE_ROOT: ${CLAUDE_ROOT}`);
}
const projectsDir = getProjectsBasePath();
const todosDir = getTodosBasePath();
logger.info(`Projects directory: ${projectsDir}`);
logger.info(`Todos directory: ${todosDir}`);
// Create local context (the only context in standalone mode)
localContext = new ServiceContext({
id: 'local',
type: 'local',
fsProvider: new LocalFileSystemProvider(),
projectsDir,
todosDir,
});
localContext.start();
// Initialize notification manager
notificationManager = NotificationManager.getInstance();
localContext.fileWatcher.setNotificationManager(notificationManager);
// Create HTTP server
httpServer = new HttpServer();
// Wire file watcher events to SSE broadcast
localContext.fileWatcher.on('file-change', (event: unknown) => {
httpServer.broadcast('file-change', event);
});
localContext.fileWatcher.on('todo-change', (event: unknown) => {
httpServer.broadcast('todo-change', event);
});
// Forward notification events to SSE
notificationManager.on('notification-new', (notification: unknown) => {
httpServer.broadcast('notification:new', notification);
});
notificationManager.on('notification-updated', (data: unknown) => {
httpServer.broadcast('notification:updated', data);
});
notificationManager.on('notification-clicked', (data: unknown) => {
httpServer.broadcast('notification:clicked', data);
});
// Build services for HTTP routes
const services: HttpServices = {
projectScanner: localContext.projectScanner,
sessionParser: localContext.sessionParser,
subagentResolver: localContext.subagentResolver,
chunkBuilder: localContext.chunkBuilder,
dataCache: localContext.dataCache,
updaterService: updaterServiceStub,
sshConnectionManager: sshConnectionManagerStub,
};
// No-op mode switch handler (no SSH in standalone)
const modeSwitchHandler = async (): Promise<void> => {};
// Start the server
const port = await httpServer.start(services, modeSwitchHandler, PORT, HOST);
logger.info(`Standalone server running at http://${HOST}:${port}`);
logger.info('Open in your browser to view Claude Code sessions');
}
async function shutdown(): Promise<void> {
logger.info('Shutting down...');
if (httpServer?.isRunning()) {
await httpServer.stop();
}
if (localContext) {
localContext.dispose();
}
logger.info('Shutdown complete');
process.exit(0);
}
// =============================================================================
// Signal Handlers
// =============================================================================
process.on('SIGTERM', () => void shutdown());
process.on('SIGINT', () => void shutdown());
process.on('unhandledRejection', (reason) => {
logger.error('Unhandled promise rejection:', reason);
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught exception:', error);
});
// =============================================================================
// Start
// =============================================================================
void start();