* Add improved development environment with backend in Docker and frontend locally - Created dev.bat script to run backend services in Docker and frontend locally - Added docker-compose.backend.yml for backend-only Docker setup - Updated package.json to run frontend on port 3737 - Fixed api.ts to use default port 8181 instead of throwing error - Script automatically stops production containers to avoid port conflicts - Provides instant HMR for frontend development * Refactor development environment setup: replace dev.bat with Makefile for cross-platform support and enhanced commands * Enhance development environment: add environment variable checks and update test commands for frontend and backend * Improve development environment with Docker Compose profiles This commit enhances the development workflow by replacing the separate docker-compose.backend.yml file with Docker Compose profiles, fixing critical service discovery issues, and adding comprehensive developer tooling through an improved Makefile system. Key improvements: - Replace docker-compose.backend.yml with cleaner profile approach - Fix service discovery by maintaining consistent container names - Fix port mappings (3737:3737 instead of 3737:5173) - Add make doctor for environment validation - Fix port configuration and frontend HMR - Improve error handling with .SHELLFLAGS in Makefile - Add comprehensive port configuration via environment variables - Simplify make dev-local to only run essential services - Add logging directory creation for local development - Document profile strategy in docker-compose.yml These changes provide three flexible development modes: - Hybrid mode (default): Backend in Docker, frontend local with HMR - Docker mode: Everything in Docker for production-like testing - Local mode: API server and UI run locally Co-authored-by: Zak Stam <zaksnet@users.noreply.github.com> * Fix make stop command to properly handle Docker Compose profiles The stop command now explicitly specifies all profiles to ensure all containers are stopped regardless of how they were started. * Fix README to document correct make commands - Changed 'make lint' to 'make lint-frontend' and 'make lint-backend' - Removed non-existent 'make logs-server' command - Added 'make watch-mcp' and 'make watch-agents' commands - All documented make commands now match what's available in Makefile * fix: Address critical issues from code review #435 - Create robust environment validation script (check-env.js) that properly parses .env files - Fix Docker healthcheck port mismatch (5173 -> 3737) - Remove hard-coded port flags from package.json to allow environment configuration - Fix Docker detection logic using /.dockerenv instead of HOSTNAME - Normalize container names to lowercase (archon-server, archon-mcp, etc.) - Improve stop-local command with port-based fallback for process killing - Fix API configuration fallback chain to include VITE_PORT - Fix Makefile shell variable expansion using runtime evaluation - Update .PHONY targets with comprehensive list - Add --profile flags to Docker Compose commands in README - Add VITE_ARCHON_SERVER_PORT to docker-compose.yml - Add Node.js 18+ to prerequisites - Use dynamic ports in Makefile help messages - Add lint alias combining frontend and backend linting - Update .env.example documentation - Scope .gitignore logs entry to /logs/ Co-Authored-By: Claude <noreply@anthropic.com> * Fix container name resolution for MCP server - Add dynamic container name resolution with three-tier strategy - Support environment variables for custom container names - Add service discovery labels to docker-compose services - Update BackendStartupError with correct container name references * Fix frontend test failures in API configuration tests - Update environment variable names to use VITE_ prefix that matches production code - Fix MCP client service tests to use singleton instance export - Update default behavior tests to expect fallback to port 8181 - All 77 frontend tests now pass * Fix make stop-local to avoid Docker daemon interference Replace aggressive kill -9 with targeted process termination: - Filter processes by command name (node/vite/python/uvicorn) before killing - Use graceful SIGTERM instead of SIGKILL - Add process verification to avoid killing Docker-related processes - Improve logging with descriptive step messages * refactor: Simplify development workflow based on comprehensive review - Reduced Makefile from 344 lines (43 targets) to 83 lines (8 essential targets) - Removed unnecessary environment variables (*_CONTAINER_NAME variables) - Fixed Windows compatibility by removing Unix-specific commands - Added security fixes to check-env.js (path validation) - Simplified MCP container discovery to use fixed container names - Fixed 'make stop' to properly handle Docker Compose profiles - Updated documentation to reflect simplified workflow - Restored original .env.example with comprehensive Supabase key documentation This addresses all critical issues from code review: - Cross-platform compatibility ✅ - Security vulnerabilities fixed ✅ - 81% reduction in complexity ✅ - Maintains all essential functionality ✅ All tests pass: Frontend (77/77), Backend (267/267) * feat: Add granular test and lint commands to Makefile - Split test command into test-fe and test-be for targeted testing - Split lint command into lint-fe and lint-be for targeted linting - Keep original test and lint commands that run both - Update help text with new commands for better developer experience * feat: Improve Docker Compose detection and prefer modern syntax - Prefer 'docker compose' (plugin) over 'docker-compose' (standalone) - Add better error handling in Makefile with proper exit on failures - Add Node.js check before running environment scripts - Pass environment variables correctly to frontend in hybrid mode - Update all documentation to use modern 'docker compose' syntax - Auto-detect which Docker Compose version is available * docs: Update CONTRIBUTING.md to reflect simplified development workflow - Add Node.js 18+ as prerequisite for hybrid development - Mark Make as optional throughout the documentation - Update all docker-compose commands to modern 'docker compose' syntax - Add Make command alternatives for testing (make test, test-fe, test-be) - Document make dev for hybrid development mode - Remove linting requirements until codebase errors are resolved * fix: Rename frontend service to archon-frontend for consistency Aligns frontend service naming with other services (archon-server, archon-mcp, archon-agents) for better consistency in Docker image naming patterns. --------- Co-authored-by: Zak Stam <zakscomputers@hotmail.com> Co-authored-by: Zak Stam <zaksnet@users.noreply.github.com>
349 lines
13 KiB
TypeScript
349 lines
13 KiB
TypeScript
/// <reference types="vitest" />
|
|
import path from "path";
|
|
import { defineConfig, loadEnv } from "vite";
|
|
import react from "@vitejs/plugin-react";
|
|
import { exec } from 'child_process';
|
|
import { readFile } from 'fs/promises';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
import type { ConfigEnv, UserConfig } from 'vite';
|
|
|
|
// https://vitejs.dev/config/
|
|
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
|
|
// Load environment variables
|
|
const env = loadEnv(mode, process.cwd(), '');
|
|
|
|
// Get host and port from environment variables or use defaults
|
|
// For internal Docker communication, use the service name
|
|
// For external access, use the HOST from environment
|
|
const isDocker = process.env.DOCKER_ENV === 'true' || existsSync('/.dockerenv');
|
|
const internalHost = 'archon-server'; // Docker service name for internal communication
|
|
const externalHost = process.env.HOST || 'localhost'; // Host for external access
|
|
const host = isDocker ? internalHost : externalHost;
|
|
const port = process.env.ARCHON_SERVER_PORT || env.ARCHON_SERVER_PORT || '8181';
|
|
|
|
return {
|
|
plugins: [
|
|
react(),
|
|
// Custom plugin to add test endpoint
|
|
{
|
|
name: 'test-runner',
|
|
configureServer(server) {
|
|
// Serve coverage directory statically
|
|
server.middlewares.use(async (req, res, next) => {
|
|
if (req.url?.startsWith('/coverage/')) {
|
|
const filePath = path.join(process.cwd(), req.url);
|
|
console.log('[VITE] Serving coverage file:', filePath);
|
|
try {
|
|
const data = await readFile(filePath);
|
|
const contentType = req.url.endsWith('.json') ? 'application/json' :
|
|
req.url.endsWith('.html') ? 'text/html' : 'text/plain';
|
|
res.setHeader('Content-Type', contentType);
|
|
res.end(data);
|
|
} catch (err) {
|
|
console.log('[VITE] Coverage file not found:', filePath);
|
|
res.statusCode = 404;
|
|
res.end('Not found');
|
|
}
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
|
|
// Test execution endpoint (basic tests)
|
|
server.middlewares.use('/api/run-tests', (req: any, res: any) => {
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405;
|
|
res.end('Method not allowed');
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
});
|
|
|
|
// Run vitest with proper configuration (includes JSON reporter)
|
|
const testProcess = exec('npm run test -- --run', {
|
|
cwd: process.cwd()
|
|
});
|
|
|
|
testProcess.stdout?.on('data', (data) => {
|
|
const text = data.toString();
|
|
// Split by newlines but preserve empty lines for better formatting
|
|
const lines = text.split('\n');
|
|
|
|
lines.forEach((line: string) => {
|
|
// Send all lines including empty ones for proper formatting
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: line, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
|
|
// Flush the response to ensure immediate delivery
|
|
if (res.flushHeaders) {
|
|
res.flushHeaders();
|
|
}
|
|
});
|
|
|
|
testProcess.stderr?.on('data', (data) => {
|
|
const lines = data.toString().split('\n').filter((line: string) => line.trim());
|
|
lines.forEach((line: string) => {
|
|
// Strip ANSI escape codes
|
|
const cleanLine = line.replace(/\\x1b\[[0-9;]*m/g, '');
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
});
|
|
|
|
testProcess.on('close', (code) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'completed',
|
|
exit_code: code,
|
|
status: code === 0 ? 'completed' : 'failed',
|
|
message: code === 0 ? 'Tests completed and results generated!' : 'Tests failed',
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
testProcess.on('error', (error) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'error',
|
|
message: error.message,
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
req.on('close', () => {
|
|
testProcess.kill();
|
|
});
|
|
});
|
|
|
|
// Test execution with coverage endpoint
|
|
server.middlewares.use('/api/run-tests-with-coverage', (req: any, res: any) => {
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405;
|
|
res.end('Method not allowed');
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
});
|
|
|
|
// Run vitest with coverage using the proper script (now includes both default and JSON reporters)
|
|
// Add CI=true to get cleaner output without HTML dumps
|
|
// Override the reporter to use verbose for better streaming output
|
|
// When running in Docker, we need to ensure the test results directory exists
|
|
const testResultsDir = path.join(process.cwd(), 'public', 'test-results');
|
|
if (!existsSync(testResultsDir)) {
|
|
mkdirSync(testResultsDir, { recursive: true });
|
|
}
|
|
|
|
const testProcess = exec('npm run test:coverage:stream', {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
FORCE_COLOR: '1',
|
|
CI: 'true',
|
|
NODE_ENV: 'test'
|
|
} // Enable color output and CI mode for cleaner output
|
|
});
|
|
|
|
testProcess.stdout?.on('data', (data) => {
|
|
const text = data.toString();
|
|
// Split by newlines but preserve empty lines for better formatting
|
|
const lines = text.split('\n');
|
|
|
|
lines.forEach((line: string) => {
|
|
// Strip ANSI escape codes to get clean text
|
|
const cleanLine = line.replace(/\\x1b\[[0-9;]*m/g, '');
|
|
|
|
// Send all lines for verbose reporter output
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
|
|
// Flush the response to ensure immediate delivery
|
|
if (res.flushHeaders) {
|
|
res.flushHeaders();
|
|
}
|
|
});
|
|
|
|
testProcess.stderr?.on('data', (data) => {
|
|
const lines = data.toString().split('\n').filter((line: string) => line.trim());
|
|
lines.forEach((line: string) => {
|
|
// Strip ANSI escape codes
|
|
const cleanLine = line.replace(/\\x1b\[[0-9;]*m/g, '');
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: cleanLine, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
});
|
|
|
|
testProcess.on('close', (code) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'completed',
|
|
exit_code: code,
|
|
status: code === 0 ? 'completed' : 'failed',
|
|
message: code === 0 ? 'Tests completed with coverage and results generated!' : 'Tests failed',
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
testProcess.on('error', (error) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'error',
|
|
message: error.message,
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
req.on('close', () => {
|
|
testProcess.kill();
|
|
});
|
|
});
|
|
|
|
// Coverage generation endpoint
|
|
server.middlewares.use('/api/generate-coverage', (req: any, res: any) => {
|
|
if (req.method !== 'POST') {
|
|
res.statusCode = 405;
|
|
res.end('Method not allowed');
|
|
return;
|
|
}
|
|
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'Content-Type',
|
|
});
|
|
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'status',
|
|
message: 'Starting coverage generation...',
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
|
|
// Run coverage generation
|
|
const coverageProcess = exec('npm run test:coverage', {
|
|
cwd: process.cwd()
|
|
});
|
|
|
|
coverageProcess.stdout?.on('data', (data) => {
|
|
const lines = data.toString().split('\n').filter((line: string) => line.trim());
|
|
lines.forEach((line: string) => {
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: line, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
});
|
|
|
|
coverageProcess.stderr?.on('data', (data) => {
|
|
const lines = data.toString().split('\n').filter((line: string) => line.trim());
|
|
lines.forEach((line: string) => {
|
|
res.write(`data: ${JSON.stringify({ type: 'output', message: line, timestamp: new Date().toISOString() })}\n\n`);
|
|
});
|
|
});
|
|
|
|
coverageProcess.on('close', (code) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'completed',
|
|
exit_code: code,
|
|
status: code === 0 ? 'completed' : 'failed',
|
|
message: code === 0 ? 'Coverage report generated successfully!' : 'Coverage generation failed',
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
coverageProcess.on('error', (error) => {
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'error',
|
|
message: error.message,
|
|
timestamp: new Date().toISOString()
|
|
})}\n\n`);
|
|
res.end();
|
|
});
|
|
|
|
req.on('close', () => {
|
|
coverageProcess.kill();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
],
|
|
server: {
|
|
host: '0.0.0.0', // Listen on all network interfaces with explicit IP
|
|
port: parseInt(process.env.ARCHON_UI_PORT || env.ARCHON_UI_PORT || '3737'), // Use configurable port
|
|
strictPort: true, // Exit if port is in use
|
|
proxy: {
|
|
'/api': {
|
|
target: `http://${host}:${port}`,
|
|
changeOrigin: true,
|
|
secure: false,
|
|
ws: true,
|
|
configure: (proxy, options) => {
|
|
proxy.on('error', (err, req, res) => {
|
|
console.log('🚨 [VITE PROXY ERROR]:', err.message);
|
|
console.log('🚨 [VITE PROXY ERROR] Target:', `http://${host}:${port}`);
|
|
console.log('🚨 [VITE PROXY ERROR] Request:', req.url);
|
|
});
|
|
proxy.on('proxyReq', (proxyReq, req, res) => {
|
|
console.log('🔄 [VITE PROXY] Forwarding:', req.method, req.url, 'to', `http://${host}:${port}${req.url}`);
|
|
});
|
|
}
|
|
},
|
|
// Socket.IO specific proxy configuration
|
|
'/socket.io': {
|
|
target: `http://${host}:${port}`,
|
|
changeOrigin: true,
|
|
ws: true
|
|
}
|
|
},
|
|
},
|
|
define: {
|
|
'import.meta.env.VITE_HOST': JSON.stringify(host),
|
|
'import.meta.env.VITE_PORT': JSON.stringify(port),
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
"@": path.resolve(__dirname, "./src"),
|
|
},
|
|
},
|
|
test: {
|
|
globals: true,
|
|
environment: 'jsdom',
|
|
setupFiles: './test/setup.ts',
|
|
css: true,
|
|
exclude: [
|
|
'**/node_modules/**',
|
|
'**/dist/**',
|
|
'**/cypress/**',
|
|
'**/.{idea,git,cache,output,temp}/**',
|
|
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
|
|
'**/*.test.{ts,tsx}',
|
|
],
|
|
env: {
|
|
VITE_HOST: host,
|
|
VITE_PORT: port,
|
|
},
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'json', 'html'],
|
|
exclude: [
|
|
'node_modules/',
|
|
'test/',
|
|
'**/*.d.ts',
|
|
'**/*.config.*',
|
|
'**/mockData.ts',
|
|
'**/*.test.{ts,tsx}',
|
|
],
|
|
}
|
|
}
|
|
};
|
|
});
|