agent-team-composer
Version:
Transform README files into GitHub project plans with AI-powered agent teams
248 lines • 9.58 kB
JavaScript
import express from 'express';
import cors from 'cors';
import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { GitHubService } from '../services/github-service.js';
import { z } from 'zod';
import { asyncHandler } from '../utils/error-handler.js';
import { TelemetryService } from '../services/telemetry.js';
import { ResilienceManager } from '../services/resilience.js';
const app = express();
// Configure CORS for production
const corsOptions = {
origin: process.env.NODE_ENV === 'production'
? process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
: true,
credentials: true,
maxAge: 86400 // 24 hours
};
app.use(cors(corsOptions));
app.use(express.json());
// Health check endpoint
app.get('/api/health', (req, res) => {
const telemetry = TelemetryService.getInstance();
const resilience = ResilienceManager.getInstance();
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '1.0.0',
metrics: {
extractionSuccessRate: telemetry.getExtractionSuccessRate(),
methodSuccessRates: telemetry.getMethodSuccessRates(),
resilience: resilience.getStatus()
}
});
});
// Telemetry endpoint for monitoring
app.get('/api/telemetry', (req, res) => {
const telemetry = TelemetryService.getInstance();
res.json({
report: JSON.parse(telemetry.generateDailyReport()),
commonErrors: telemetry.getMostCommonErrors()
});
});
let projectData = null;
let dataFilePath = null;
// API endpoint to get project data
app.get('/api/project', (req, res) => {
if (!projectData) {
return res.status(404).json({ error: 'No project data loaded' });
}
res.json(projectData);
});
// API endpoint to get README content from the current working directory
app.get('/api/readme', asyncHandler(async (req, res) => {
try {
const readmePath = path.join(process.cwd(), 'README.md');
const content = await fs.readFile(readmePath, 'utf8');
res.json({ content });
}
catch (error) {
if (error.code === 'ENOENT') {
return res.status(404).json({
error: 'README.md not found',
details: 'No README.md file found in the current directory'
});
}
throw error;
}
}));
// API endpoint to update project data
app.put('/api/project', async (req, res) => {
projectData = req.body;
// Save to temp file if path is available
if (dataFilePath) {
await fs.writeFile(dataFilePath, JSON.stringify(projectData, null, 2));
}
res.json({ success: true });
});
// API endpoint to generate project plan from parsed metadata
app.post('/api/generate-plan', asyncHandler(async (req, res) => {
const { metadata, readmeContent } = req.body;
if (!metadata || !readmeContent) {
return res.status(400).json({
error: 'Missing required fields',
details: 'Both metadata and readmeContent are required'
});
}
try {
// Import the LLM orchestrator
const { generateProjectPlan } = await import('../services/llm-orchestrator.js');
// Generate the project plan
const projectPlan = await generateProjectPlan(metadata, readmeContent);
// Save to temp file
projectData = projectPlan;
if (dataFilePath) {
await fs.writeFile(dataFilePath, JSON.stringify(projectData, null, 2));
}
res.json(projectPlan);
}
catch (error) {
console.error('Plan generation error:', error);
res.status(500).json({
error: 'Failed to generate project plan',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}));
// Input validation schema
const CreateIssuesSchema = z.object({
repository: z.string().regex(/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/),
phases: z.array(z.any()) // Will be validated in GitHubService
});
// API endpoint to check GitHub authentication
app.get('/api/github/auth', asyncHandler(async (req, res) => {
const githubService = new GitHubService();
const authStatus = await githubService.checkAuthentication();
res.json(authStatus);
}));
// API endpoint to get user repositories
app.get('/api/github/repositories', asyncHandler(async (req, res) => {
const githubService = new GitHubService();
// Check authentication first
const authStatus = await githubService.checkAuthentication();
if (!authStatus.authenticated) {
return res.status(401).json({
error: 'GitHub authentication required',
details: 'Please set GITHUB_TOKEN environment variable'
});
}
// Fetch repositories
const repos = await githubService.getUserRepositories();
res.json(repos);
}));
// API endpoint to get repository branches
app.get('/api/github/branches', asyncHandler(async (req, res) => {
const { repo } = req.query;
if (!repo || typeof repo !== 'string') {
return res.status(400).json({
error: 'Repository parameter required'
});
}
const githubService = new GitHubService();
const branches = await githubService.getRepositoryBranches(repo);
res.json(branches);
}));
// API endpoint to create GitHub issues
app.post('/api/github/create-issues', async (req, res) => {
try {
// Validate input
const validatedInput = CreateIssuesSchema.parse(req.body);
const { repository, phases } = validatedInput;
console.log(`Creating issues for ${repository}...`);
// Use GitHub API instead of CLI
const githubService = new GitHubService();
// Check authentication first
const authStatus = await githubService.checkAuthentication();
if (!authStatus.authenticated) {
return res.status(401).json({
error: 'GitHub authentication required',
details: 'Please set GITHUB_TOKEN environment variable'
});
}
// Create issues
const results = await githubService.createIssuesFromPhases(repository, phases);
res.json({ success: true, results });
}
catch (error) {
console.error('GitHub creation error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Invalid request data',
details: error.errors
});
}
res.status(500).json({
error: 'Failed to create GitHub issues',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
});
export async function startServer(port, tempDataPath) {
// Load project data
dataFilePath = tempDataPath;
const dataContent = await fs.readFile(tempDataPath, 'utf8');
projectData = JSON.parse(dataContent);
// Determine the package root directory (where the dist files are located)
// When running from npm/npx, __dirname will be inside the installed package
const currentFileUrl = import.meta.url;
const currentFilePath = fileURLToPath(currentFileUrl);
const currentDir = path.dirname(currentFilePath);
// Navigate up from dist/server to the package root
const packageRoot = path.resolve(currentDir, '..', '..');
const distPath = path.join(packageRoot, 'dist');
// Always serve the built files from the package, not from cwd
// This ensures we serve Agent Composer UI, not the user's local files
try {
// Check if dist directory exists in the package
await fs.access(distPath);
// Serve static files from the package's dist directory
app.use(express.static(distPath));
// Fallback to index.html for client-side routing
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
catch (error) {
console.error('Could not find built files in package directory:', distPath);
console.error('This may indicate the package was not built properly.');
throw new Error('Agent Composer UI files not found. Please reinstall the package.');
}
return new Promise((resolve, reject) => {
let currentPort = port;
let retries = 0;
const maxRetries = 10;
const tryListen = () => {
const server = app.listen(currentPort, () => {
const url = `http://localhost:${currentPort}`;
console.log(`Server running at ${url}`);
resolve(url);
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down server...');
server.close();
// Clean up temp file
if (dataFilePath) {
fs.unlink(dataFilePath).catch(() => { });
}
process.exit(0);
});
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE' && retries < maxRetries) {
retries++;
currentPort++;
console.log(`Port ${currentPort - 1} is in use, trying port ${currentPort}...`);
server.close();
tryListen();
}
else {
reject(err);
}
});
};
tryListen();
});
}
//# sourceMappingURL=index.js.map