diff --git a/bmad-web/.env.example b/bmad-web/.env.example new file mode 100644 index 00000000..f6a22ec2 --- /dev/null +++ b/bmad-web/.env.example @@ -0,0 +1,22 @@ +# BMAD Web Environment Variables + +# API Configuration +PORT=4000 +NODE_ENV=development +FRONTEND_URL=http://localhost:3000 + +# Authentication +JWT_SECRET=your-super-secret-jwt-key-change-in-production + +# Database (for production) +DATABASE_URL=postgresql://user:password@localhost:5432/bmad_web + +# Redis (for production) +REDIS_URL=redis://localhost:6379 + +# BMAD Core Path (optional, defaults to parent directory) +BMAD_ROOT=../ + +# OpenAI/Anthropic API Keys (for AI features) +OPENAI_API_KEY=your-openai-api-key +ANTHROPIC_API_KEY=your-anthropic-api-key diff --git a/bmad-web/README.md b/bmad-web/README.md new file mode 100644 index 00000000..93a65798 --- /dev/null +++ b/bmad-web/README.md @@ -0,0 +1,257 @@ +# BMAD Web - Consumer Frontend Application + +Uma aplicacao web completa para consumidores finais do BMAD-METHOD (Build More, Architect Dreams). + +## Visao Geral + +Este projeto transforma o BMAD-METHOD (atualmente uma CLI) em uma aplicacao web acessivel para usuarios finais, permitindo que qualquer pessoa utilize o framework de desenvolvimento agil impulsionado por IA. + +## Estrutura do Projeto + +``` +bmad-web/ +├── apps/ +│ ├── web/ # Frontend Next.js +│ └── api/ # Backend Express + WebSocket +├── packages/ +│ ├── bmad-core/ # Core BMAD como biblioteca +│ ├── ui/ # Componentes UI compartilhados +│ └── config/ # Configuracoes compartilhadas +└── docs/ # Documentacao +``` + +## Stack Tecnologico + +### Frontend +- **Next.js 14** - Framework React com App Router +- **TypeScript** - Type safety +- **Tailwind CSS** - Estilos +- **shadcn/ui** - Componentes +- **Zustand** - Estado global +- **TanStack Query** - Data fetching +- **Socket.io Client** - Real-time + +### Backend +- **Node.js 20+** - Runtime +- **Express** - HTTP Server +- **Socket.io** - WebSocket +- **Prisma** - ORM (para producao) +- **JWT** - Autenticacao + +## Requisitos + +- Node.js 20.0.0 ou superior +- npm 10.0.0 ou superior + +## Instalacao + +1. Clone o repositorio: +```bash +git clone https://github.com/seu-usuario/BMAD-METHOD.git +cd BMAD-METHOD/bmad-web +``` + +2. Instale as dependencias: +```bash +npm install +``` + +3. Configure as variaveis de ambiente: +```bash +cp .env.example .env +cp apps/api/.env.example apps/api/.env +cp apps/web/.env.local.example apps/web/.env.local +``` + +4. Inicie o desenvolvimento: +```bash +npm run dev +``` + +Isso iniciara: +- Frontend: http://localhost:3000 +- API: http://localhost:4000 + +## Funcionalidades + +### Para Usuarios + +- **Dashboard de Projeto** - Visao geral do seu projeto +- **Chat com Agentes IA** - Interacao em tempo real com 21+ agentes especializados +- **Workflows Guiados** - 50+ workflows para todas as fases de desenvolvimento +- **Editor de Artefatos** - Edicao e versionamento de documentos +- **Tracks Adaptativos** - Quick Flow (~5min), BMAD Method (~15min), Enterprise (~30min) + +### Agentes Disponiveis + +| Agente | Funcao | +|--------|--------| +| John (PM) | Product Manager - PRDs e requisitos | +| Alex (Architect) | Arquiteto - Design de sistemas | +| Dev | Desenvolvedor Senior - Implementacao | +| Barry | Quick-Flow Solo Dev - Desenvolvimento rapido | +| UX Designer | Design de experiencia do usuario | +| Tech Writer | Documentacao tecnica | +| TEA | Test Architecture Agent - Estrategia de testes | +| Scrum Master | Coordenacao de equipe | +| Analyst | Analise de dados e requisitos | + +### Workflows Principais + +**Analise** +- Project Discovery +- Requirement Elicitation +- Team Composition + +**Planejamento** +- PRD Creation +- Workflow Initialization + +**Solutioning** +- Architecture Design +- Epic/Story Creation +- Excalidraw Diagrams + +**Implementacao** +- Sprint Planning +- Sprint Status +- Retrospectives + +**Quick Flow** +- Quick-Spec (~5 min) +- Quick-Dev (~5 min) + +## Scripts Disponiveis + +```bash +# Desenvolvimento +npm run dev # Inicia frontend e backend + +# Build +npm run build # Build de producao + +# Lint +npm run lint # Verifica codigo + +# Banco de dados (producao) +npm run db:generate # Gera cliente Prisma +npm run db:push # Sincroniza schema +npm run db:studio # Interface do banco + +# Limpeza +npm run clean # Remove node_modules e builds +``` + +## Arquitetura + +### Frontend (Next.js) + +``` +apps/web/src/ +├── app/ # Rotas (App Router) +│ ├── (auth)/ # Login, Register +│ ├── (dashboard)/ # Area logada +│ ├── projects/ # Gestao de projetos +│ ├── agents/ # Interface de agentes +│ └── workflows/ # Visualizacao de workflows +├── components/ # Componentes React +├── hooks/ # Hooks customizados +├── stores/ # Estado (Zustand) +└── types/ # TypeScript types +``` + +### Backend (Express) + +``` +apps/api/src/ +├── routes/ # Endpoints REST +├── services/ # Logica de negocio +├── middleware/ # Auth, errors +├── websocket/ # Handlers real-time +├── bmad/ # Integracao BMAD Core +└── database/ # Models (Prisma) +``` + +### Packages + +- **@bmad/core** - Tipos, loaders de agentes e workflows +- **@bmad/ui** - Componentes compartilhados (Button, Card, Input, etc.) +- **@bmad/config** - Configuracoes de TypeScript, ESLint + +## API Endpoints + +### Autenticacao +- `POST /api/auth/register` - Registro +- `POST /api/auth/login` - Login +- `POST /api/auth/logout` - Logout +- `GET /api/auth/me` - Usuario atual + +### Projetos +- `GET /api/projects` - Listar projetos +- `POST /api/projects` - Criar projeto +- `GET /api/projects/:id` - Detalhes do projeto +- `PUT /api/projects/:id` - Atualizar projeto +- `DELETE /api/projects/:id` - Deletar projeto + +### Agentes +- `GET /api/agents` - Listar agentes +- `GET /api/agents/:id` - Detalhes do agente +- `POST /api/agents/:id/chat` - Chat com agente + +### Workflows +- `GET /api/workflows` - Listar workflows +- `POST /api/workflows/:id/start` - Iniciar workflow +- `GET /api/workflows/instance/:id/status` - Status +- `POST /api/workflows/instance/:id/step/complete` - Completar passo + +### WebSocket Events +- `agent:message` - Mensagem do agente +- `agent:typing` - Indicador de digitacao +- `workflow:progress` - Progresso do workflow +- `artifact:updated` - Artefato atualizado + +## Proximos Passos + +### Fase 1 - MVP +- [x] Estrutura do monorepo +- [x] Frontend basico com Next.js +- [x] API com autenticacao +- [x] Chat com agentes +- [ ] Integracao com LLM (OpenAI/Anthropic) +- [ ] Persistencia em banco de dados + +### Fase 2 - Features +- [ ] Todos os agentes integrados +- [ ] Workflow engine completo +- [ ] Editor de artefatos rico +- [ ] Versionamento de artefatos + +### Fase 3 - Polish +- [ ] Onboarding wizard +- [ ] Templates de projeto +- [ ] Temas e personalizacao +- [ ] Mobile responsivo + +### Fase 4 - Scale +- [ ] Colaboracao em tempo real +- [ ] Integracoes (GitHub, Jira) +- [ ] API publica +- [ ] Analytics + +## Contribuindo + +1. Fork o repositorio +2. Crie uma branch (`git checkout -b feature/nova-funcionalidade`) +3. Commit suas mudancas (`git commit -am 'Adiciona nova funcionalidade'`) +4. Push para a branch (`git push origin feature/nova-funcionalidade`) +5. Abra um Pull Request + +## Licenca + +MIT - Veja [LICENSE](../LICENSE) para detalhes. + +--- + +**BMAD - Build More, Architect Dreams** + +Framework de desenvolvimento agil impulsionado por IA com 21 agentes especializados e 50+ workflows guiados. diff --git a/bmad-web/apps/api/.env.example b/bmad-web/apps/api/.env.example new file mode 100644 index 00000000..1ce377a9 --- /dev/null +++ b/bmad-web/apps/api/.env.example @@ -0,0 +1,21 @@ +# BMAD API Environment Variables + +PORT=4000 +NODE_ENV=development +FRONTEND_URL=http://localhost:3000 + +# Authentication +JWT_SECRET=your-super-secret-jwt-key-change-in-production + +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/bmad_web + +# Redis +REDIS_URL=redis://localhost:6379 + +# BMAD Core Path +BMAD_ROOT=../../.. + +# AI Provider Keys +OPENAI_API_KEY= +ANTHROPIC_API_KEY= diff --git a/bmad-web/apps/api/package.json b/bmad-web/apps/api/package.json new file mode 100644 index 00000000..00a22303 --- /dev/null +++ b/bmad-web/apps/api/package.json @@ -0,0 +1,46 @@ +{ + "name": "@bmad/api", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc && tsc-alias", + "start": "node dist/index.js", + "lint": "eslint src --ext .ts", + "clean": "rm -rf dist node_modules", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:studio": "prisma studio" + }, + "dependencies": { + "@bmad/core": "workspace:*", + "@prisma/client": "^5.6.0", + "bcryptjs": "^2.4.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.4", + "helmet": "^7.1.0", + "ioredis": "^5.3.2", + "jsonwebtoken": "^9.0.2", + "socket.io": "^4.7.2", + "uuid": "^9.0.1", + "yaml": "^2.3.4", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.16", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.0", + "@types/uuid": "^9.0.7", + "prisma": "^5.6.0", + "tsc-alias": "^1.8.8", + "tsx": "^4.6.0", + "typescript": "^5.3.0" + } +} diff --git a/bmad-web/apps/api/src/bmad/adapter.ts b/bmad-web/apps/api/src/bmad/adapter.ts new file mode 100644 index 00000000..adc1d060 --- /dev/null +++ b/bmad-web/apps/api/src/bmad/adapter.ts @@ -0,0 +1,401 @@ +import { readFile, readdir } from 'fs/promises'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { parse as parseYaml } from 'yaml'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// Path to BMAD core - adjust based on deployment +const BMAD_ROOT = process.env.BMAD_ROOT || join(__dirname, '../../../../..'); + +interface AgentDefinition { + id: string; + name: string; + title: string; + icon?: string; + module: string; + persona: { + role: string; + identity?: string; + communicationStyle: string; + principles?: string[]; + }; + menu: AgentMenuItem[]; + prompts: Record; +} + +interface AgentMenuItem { + trigger: string; + exec?: string; + description: string; + requiresContext?: string[]; +} + +interface WorkflowDefinition { + id: string; + name: string; + description: string; + module: string; + phase: string; + steps: WorkflowStep[]; + estimatedTime?: string; +} + +interface WorkflowStep { + id: string; + name: string; + description: string; + instruction: string; + expectedOutput?: string; +} + +interface MessageContext { + conversationId?: string; + projectId?: string; + userId?: string; +} + +interface MessageResponse { + content: string; + metadata?: { + suggestedActions?: { label: string; action: string; primary?: boolean }[]; + artifacts?: string[]; + workflowStep?: string; + }; +} + +export class BMADAdapter { + private agents: Map = new Map(); + private workflows: Map = new Map(); + private loaded: boolean = false; + + constructor() { + // Load on first use + this.loadAgents().catch(console.error); + this.loadWorkflows().catch(console.error); + } + + private async ensureLoaded(): Promise { + if (!this.loaded) { + await Promise.all([this.loadAgents(), this.loadWorkflows()]); + this.loaded = true; + } + } + + async loadAgents(): Promise { + const modulesPath = join(BMAD_ROOT, 'src/modules'); + + try { + const modules = await readdir(modulesPath); + + for (const moduleName of modules) { + const agentsPath = join(modulesPath, moduleName, 'agents'); + + try { + const agentFiles = await readdir(agentsPath); + + for (const file of agentFiles) { + if (file.endsWith('.agent.yaml') || file.endsWith('.agent.md')) { + try { + const content = await readFile(join(agentsPath, file), 'utf-8'); + const agent = this.parseAgentFile(content, moduleName, file); + if (agent) { + this.agents.set(agent.id, agent); + } + } catch (e) { + console.warn(`Failed to parse agent file: ${file}`, e); + } + } + } + } catch { + // agents directory doesn't exist for this module + } + } + } catch (e) { + console.warn('Failed to load agents from BMAD root:', e); + // Load sample agents for development + this.loadSampleAgents(); + } + } + + private parseAgentFile(content: string, moduleName: string, fileName: string): AgentDefinition | null { + try { + // Try YAML first + const data = parseYaml(content); + const agent = data.agent || data; + + return { + id: agent.metadata?.id || `${moduleName}/${fileName.replace('.agent.yaml', '').replace('.agent.md', '')}`, + name: agent.metadata?.name || 'Agent', + title: agent.metadata?.title || 'AI Agent', + icon: agent.metadata?.icon, + module: moduleName, + persona: { + role: agent.persona?.role || 'AI Assistant', + identity: agent.persona?.identity, + communicationStyle: agent.persona?.communication_style || 'professional', + principles: agent.persona?.principles, + }, + menu: (agent.menu || []).map((item: any) => ({ + trigger: item.trigger, + exec: item.exec, + description: item.description || item.trigger, + requiresContext: item.requires_context, + })), + prompts: agent.prompts || {}, + }; + } catch { + return null; + } + } + + private loadSampleAgents(): void { + // Sample agents for development/demo + const sampleAgents: AgentDefinition[] = [ + { + id: 'bmm/pm', + name: 'John', + title: 'Product Manager', + icon: 'J', + module: 'bmm', + persona: { + role: 'Product Manager especializado em criacao colaborativa de PRDs e gestao de requisitos', + communicationStyle: 'Pergunta "POR QUE?" incessantemente para extrair os requisitos reais', + }, + menu: [ + { trigger: 'PR', description: '[PR] Criar Product Requirements Document' }, + { trigger: 'analyze', description: '[AN] Analisar requisitos do projeto' }, + { trigger: 'stakeholders', description: '[ST] Mapear stakeholders' }, + ], + prompts: {}, + }, + { + id: 'bmm/architect', + name: 'Alex', + title: 'Software Architect', + icon: 'A', + module: 'bmm', + persona: { + role: 'Arquiteto de Software com foco em design de sistemas escalaveis', + communicationStyle: 'Tecnico mas acessivel, usa diagramas e exemplos', + }, + menu: [ + { trigger: 'arch', description: '[AR] Criar documento de arquitetura' }, + { trigger: 'review', description: '[RV] Revisar decisoes arquiteturais' }, + { trigger: 'diagram', description: '[DG] Gerar diagramas de sistema' }, + ], + prompts: {}, + }, + { + id: 'bmm/developer', + name: 'Dev', + title: 'Senior Developer', + icon: 'D', + module: 'bmm', + persona: { + role: 'Desenvolvedor Senior especializado em implementacao de alta qualidade', + communicationStyle: 'Pratico e direto, foca em codigo limpo e testavel', + }, + menu: [ + { trigger: 'impl', description: '[IM] Implementar feature' }, + { trigger: 'test', description: '[TE] Criar testes' }, + { trigger: 'refactor', description: '[RF] Refatorar codigo' }, + ], + prompts: {}, + }, + { + id: 'bmm/barry', + name: 'Barry', + title: 'Quick-Flow Solo Dev', + icon: 'B', + module: 'bmm', + persona: { + role: 'Desenvolvedor agil para fluxos rapidos de implementacao', + communicationStyle: 'Eficiente e focado, ideal para tarefas menores', + }, + menu: [ + { trigger: 'quick-spec', description: '[QS] Quick Spec - Especificacao rapida' }, + { trigger: 'quick-dev', description: '[QD] Quick Dev - Desenvolvimento rapido' }, + { trigger: 'fix', description: '[FX] Corrigir bug rapidamente' }, + ], + prompts: {}, + }, + ]; + + for (const agent of sampleAgents) { + this.agents.set(agent.id, agent); + } + } + + async loadWorkflows(): Promise { + // Load sample workflows for now + const sampleWorkflows: WorkflowDefinition[] = [ + { + id: 'bmm/quick-spec', + name: 'Quick Spec', + description: 'Especificacao tecnica rapida para pequenas mudancas', + module: 'bmm', + phase: 'planning', + estimatedTime: '~5 min', + steps: [ + { + id: 'step1', + name: 'Analisar Delta', + description: 'Investigacao superficial da mudanca', + instruction: 'Analise o que precisa mudar entre o estado atual e o desejado.', + }, + { + id: 'step2', + name: 'Investigacao Profunda', + description: 'Analise detalhada do codigo e consequencias', + instruction: 'Examine o codigo existente e identifique impactos da mudanca.', + }, + { + id: 'step3', + name: 'Gerar Especificacao', + description: 'Produzir tech-spec', + instruction: 'Crie a especificacao tecnica detalhada.', + }, + { + id: 'step4', + name: 'Revisar e Refinar', + description: 'Validacao e refinamento', + instruction: 'Revise a especificacao e faca ajustes finais.', + }, + ], + }, + { + id: 'bmm/prd', + name: 'Product Requirements Document', + description: 'Criacao completa de PRD para novos produtos/features', + module: 'bmm', + phase: 'planning', + estimatedTime: '~15 min', + steps: [ + { + id: 'step1', + name: 'Descoberta', + description: 'Entender o problema e contexto', + instruction: 'Pergunte sobre o problema que estamos resolvendo, quem sao os usuarios e qual o impacto esperado.', + }, + { + id: 'step2', + name: 'Requisitos', + description: 'Definir requisitos funcionais e nao-funcionais', + instruction: 'Liste todos os requisitos do produto, separando funcionais de nao-funcionais.', + }, + { + id: 'step3', + name: 'User Stories', + description: 'Criar historias de usuario', + instruction: 'Transforme requisitos em user stories no formato "Como [persona], quero [acao], para [beneficio]".', + }, + { + id: 'step4', + name: 'Criterios de Aceite', + description: 'Definir criterios de aceitacao', + instruction: 'Para cada user story, defina criterios claros de aceitacao.', + }, + { + id: 'step5', + name: 'Revisao Final', + description: 'Revisar e aprovar PRD', + instruction: 'Revise o documento completo, verifique consistencia e obtenha aprovacao.', + }, + ], + }, + ]; + + for (const workflow of sampleWorkflows) { + this.workflows.set(workflow.id, workflow); + } + } + + async getAllAgents(): Promise { + await this.ensureLoaded(); + return Array.from(this.agents.values()); + } + + async getAgent(id: string): Promise { + await this.ensureLoaded(); + return this.agents.get(id); + } + + async getAgentsByModule(moduleId: string): Promise { + await this.ensureLoaded(); + return Array.from(this.agents.values()).filter(a => a.module === moduleId); + } + + async getAgentActions(agentId: string): Promise { + await this.ensureLoaded(); + const agent = this.agents.get(agentId); + return agent?.menu || []; + } + + async getAllWorkflows(): Promise { + await this.ensureLoaded(); + return Array.from(this.workflows.values()); + } + + async getWorkflow(id: string): Promise { + await this.ensureLoaded(); + return this.workflows.get(id); + } + + async processMessage( + agentId: string, + message: string, + context: MessageContext + ): Promise { + await this.ensureLoaded(); + + const agent = this.agents.get(agentId); + if (!agent) { + return { + content: 'Desculpe, nao consegui encontrar o agente solicitado.', + }; + } + + // Check if message matches any menu trigger + const matchedAction = agent.menu.find(item => + message.toLowerCase().includes(item.trigger.toLowerCase()) + ); + + // Simulate AI response based on agent persona + const response = this.generateAgentResponse(agent, message, matchedAction); + + return response; + } + + private generateAgentResponse( + agent: AgentDefinition, + userMessage: string, + matchedAction?: AgentMenuItem + ): MessageResponse { + // This is a placeholder - in production, this would call an LLM + // with the agent's persona and prompts + + if (matchedAction) { + return { + content: `Otimo! Vou iniciar o workflow **${matchedAction.description}** para voce.\n\nComo ${agent.title}, vou guia-lo atraves deste processo. Vamos comecar?\n\n**Proximo passo:** Me conte mais sobre o que voce quer construir.`, + metadata: { + suggestedActions: [ + { label: 'Sim, vamos comecar!', action: 'start_workflow', primary: true }, + { label: 'Tenho duvidas', action: 'ask_questions' }, + ], + }, + }; + } + + return { + content: `Ola! Sou ${agent.name}, ${agent.persona.role}.\n\nVoce disse: "${userMessage}"\n\nComo posso ajudar? Aqui estao algumas coisas que posso fazer:\n\n${agent.menu.map(item => `- **${item.trigger}**: ${item.description}`).join('\n')}\n\nBasta digitar um dos comandos acima ou me contar mais sobre seu projeto!`, + metadata: { + suggestedActions: agent.menu.slice(0, 3).map((item, index) => ({ + label: item.description.replace(/^\[[\w-]+\]\s*/, ''), + action: item.trigger, + primary: index === 0, + })), + }, + }; + } +} diff --git a/bmad-web/apps/api/src/index.ts b/bmad-web/apps/api/src/index.ts new file mode 100644 index 00000000..dde9589c --- /dev/null +++ b/bmad-web/apps/api/src/index.ts @@ -0,0 +1,74 @@ +import 'dotenv/config'; +import express from 'express'; +import { createServer } from 'http'; +import { Server } from 'socket.io'; +import cors from 'cors'; +import helmet from 'helmet'; +import cookieParser from 'cookie-parser'; +import rateLimit from 'express-rate-limit'; + +import { authRouter } from './routes/auth.js'; +import { projectsRouter } from './routes/projects.js'; +import { agentsRouter } from './routes/agents.js'; +import { workflowsRouter } from './routes/workflows.js'; +import { artifactsRouter } from './routes/artifacts.js'; +import { setupWebSocket } from './websocket/index.js'; +import { errorHandler } from './middleware/error-handler.js'; +import { authenticate } from './middleware/auth.js'; + +const app = express(); +const httpServer = createServer(app); + +// Socket.io setup +const io = new Server(httpServer, { + cors: { + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true, + }, +}); + +// Middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true, +})); +app.use(express.json()); +app.use(cookieParser()); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs + message: 'Too many requests, please try again later.', +}); +app.use('/api', limiter); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// Public routes +app.use('/api/auth', authRouter); + +// Protected routes +app.use('/api/projects', authenticate, projectsRouter); +app.use('/api/agents', authenticate, agentsRouter); +app.use('/api/workflows', authenticate, workflowsRouter); +app.use('/api/artifacts', authenticate, artifactsRouter); + +// WebSocket setup +setupWebSocket(io); + +// Error handler +app.use(errorHandler); + +// Start server +const PORT = process.env.PORT || 4000; +httpServer.listen(PORT, () => { + console.log(`BMAD API Server running on port ${PORT}`); + console.log(`WebSocket server ready`); +}); + +export { app, io }; diff --git a/bmad-web/apps/api/src/middleware/auth.ts b/bmad-web/apps/api/src/middleware/auth.ts new file mode 100644 index 00000000..6b7a75f8 --- /dev/null +++ b/bmad-web/apps/api/src/middleware/auth.ts @@ -0,0 +1,53 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'bmad-secret-key-change-in-production'; + +// Extend Express Request type +declare global { + namespace Express { + interface Request { + user?: { + id: string; + email?: string; + }; + } + } +} + +export function authenticate(req: Request, res: Response, next: NextFunction) { + const token = req.cookies.token || req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + success: false, + error: { code: 'UNAUTHORIZED', message: 'Token de autenticacao necessario' }, + }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + req.user = { id: decoded.userId }; + next(); + } catch { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_TOKEN', message: 'Token invalido ou expirado' }, + }); + } +} + +export function optionalAuth(req: Request, res: Response, next: NextFunction) { + const token = req.cookies.token || req.headers.authorization?.replace('Bearer ', ''); + + if (token) { + try { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + req.user = { id: decoded.userId }; + } catch { + // Token is invalid, but we continue without user + } + } + + next(); +} diff --git a/bmad-web/apps/api/src/middleware/error-handler.ts b/bmad-web/apps/api/src/middleware/error-handler.ts new file mode 100644 index 00000000..6593e75d --- /dev/null +++ b/bmad-web/apps/api/src/middleware/error-handler.ts @@ -0,0 +1,49 @@ +import { Request, Response, NextFunction } from 'express'; + +export interface AppError extends Error { + statusCode?: number; + code?: string; +} + +export function errorHandler( + err: AppError, + req: Request, + res: Response, + _next: NextFunction +) { + console.error('Error:', err); + + const statusCode = err.statusCode || 500; + const code = err.code || 'INTERNAL_ERROR'; + const message = err.message || 'Ocorreu um erro interno'; + + res.status(statusCode).json({ + success: false, + error: { + code, + message, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }), + }, + }); +} + +export function notFoundHandler(req: Request, res: Response) { + res.status(404).json({ + success: false, + error: { + code: 'NOT_FOUND', + message: `Rota ${req.method} ${req.path} nao encontrada`, + }, + }); +} + +export function createError( + message: string, + statusCode: number = 500, + code: string = 'ERROR' +): AppError { + const error = new Error(message) as AppError; + error.statusCode = statusCode; + error.code = code; + return error; +} diff --git a/bmad-web/apps/api/src/routes/agents.ts b/bmad-web/apps/api/src/routes/agents.ts new file mode 100644 index 00000000..c82e5f01 --- /dev/null +++ b/bmad-web/apps/api/src/routes/agents.ts @@ -0,0 +1,109 @@ +import { Router } from 'express'; +import { BMADAdapter } from '../bmad/adapter.js'; + +const router = Router(); +const bmadAdapter = new BMADAdapter(); + +// Get all available agents +router.get('/', async (req, res, next) => { + try { + const agents = await bmadAdapter.getAllAgents(); + + res.json({ + success: true, + data: agents.map(agent => ({ + id: agent.id, + name: agent.name, + title: agent.title, + icon: agent.icon, + module: agent.module, + persona: { + role: agent.persona.role, + communicationStyle: agent.persona.communicationStyle, + }, + menuCount: agent.menu.length, + })), + }); + } catch (error) { + next(error); + } +}); + +// Get agent by ID +router.get('/:id', async (req, res, next) => { + try { + const agent = await bmadAdapter.getAgent(req.params.id); + + if (!agent) { + return res.status(404).json({ + success: false, + error: { code: 'AGENT_NOT_FOUND', message: 'Agente nao encontrado' }, + }); + } + + res.json({ + success: true, + data: agent, + }); + } catch (error) { + next(error); + } +}); + +// Get agent actions (menu) +router.get('/:id/actions', async (req, res, next) => { + try { + const actions = await bmadAdapter.getAgentActions(req.params.id); + + res.json({ + success: true, + data: actions, + }); + } catch (error) { + next(error); + } +}); + +// Start chat with agent +router.post('/:id/chat', async (req, res, next) => { + try { + const { message, conversationId, projectId } = req.body; + const agentId = req.params.id; + + if (!message) { + return res.status(400).json({ + success: false, + error: { code: 'MISSING_MESSAGE', message: 'Mensagem e obrigatoria' }, + }); + } + + const response = await bmadAdapter.processMessage(agentId, message, { + conversationId, + projectId, + userId: req.user?.id, + }); + + res.json({ + success: true, + data: response, + }); + } catch (error) { + next(error); + } +}); + +// Get agents by module +router.get('/module/:moduleId', async (req, res, next) => { + try { + const agents = await bmadAdapter.getAgentsByModule(req.params.moduleId); + + res.json({ + success: true, + data: agents, + }); + } catch (error) { + next(error); + } +}); + +export { router as agentsRouter }; diff --git a/bmad-web/apps/api/src/routes/artifacts.ts b/bmad-web/apps/api/src/routes/artifacts.ts new file mode 100644 index 00000000..ae33e102 --- /dev/null +++ b/bmad-web/apps/api/src/routes/artifacts.ts @@ -0,0 +1,197 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { v4 as uuid } from 'uuid'; + +const router = Router(); + +// In-memory artifact store (replace with database in production) +const artifacts: Map = new Map(); + +// Validation schemas +const createArtifactSchema = z.object({ + projectId: z.string().uuid(), + type: z.enum(['prd', 'architecture', 'epic', 'story', 'spec', 'diagram', 'tech-spec', 'test-plan']), + name: z.string().min(1), + content: z.string(), +}); + +const updateArtifactSchema = z.object({ + name: z.string().min(1).optional(), + content: z.string().optional(), +}); + +// Get artifact by ID +router.get('/:id', (req, res) => { + const artifact = artifacts.get(req.params.id); + + if (!artifact) { + return res.status(404).json({ + success: false, + error: { code: 'ARTIFACT_NOT_FOUND', message: 'Artefato nao encontrado' }, + }); + } + + res.json({ + success: true, + data: artifact, + }); +}); + +// Create artifact +router.post('/', (req, res, next) => { + try { + const data = createArtifactSchema.parse(req.body); + + const artifact = { + id: uuid(), + ...data, + version: 1, + history: [], + createdAt: new Date(), + updatedAt: new Date(), + }; + + artifacts.set(artifact.id, artifact); + + res.status(201).json({ + success: true, + data: artifact, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Update artifact +router.put('/:id', (req, res, next) => { + try { + const artifact = artifacts.get(req.params.id); + + if (!artifact) { + return res.status(404).json({ + success: false, + error: { code: 'ARTIFACT_NOT_FOUND', message: 'Artefato nao encontrado' }, + }); + } + + const data = updateArtifactSchema.parse(req.body); + + // Save current version to history + artifact.history.push({ + version: artifact.version, + content: artifact.content, + updatedAt: artifact.updatedAt, + }); + + // Update artifact + const updatedArtifact = { + ...artifact, + ...data, + version: artifact.version + 1, + updatedAt: new Date(), + }; + + artifacts.set(artifact.id, updatedArtifact); + + res.json({ + success: true, + data: updatedArtifact, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Get artifact versions +router.get('/:id/versions', (req, res) => { + const artifact = artifacts.get(req.params.id); + + if (!artifact) { + return res.status(404).json({ + success: false, + error: { code: 'ARTIFACT_NOT_FOUND', message: 'Artefato nao encontrado' }, + }); + } + + const versions = [ + ...artifact.history, + { + version: artifact.version, + content: artifact.content, + updatedAt: artifact.updatedAt, + }, + ].sort((a, b) => b.version - a.version); + + res.json({ + success: true, + data: versions, + }); +}); + +// Export artifact +router.post('/:id/export', (req, res) => { + const artifact = artifacts.get(req.params.id); + + if (!artifact) { + return res.status(404).json({ + success: false, + error: { code: 'ARTIFACT_NOT_FOUND', message: 'Artefato nao encontrado' }, + }); + } + + const { format = 'md' } = req.body; + + // TODO: Implement different export formats (pdf, docx, etc.) + if (format === 'md') { + res.setHeader('Content-Type', 'text/markdown'); + res.setHeader('Content-Disposition', `attachment; filename="${artifact.name}.md"`); + res.send(artifact.content); + } else { + res.json({ + success: false, + error: { code: 'UNSUPPORTED_FORMAT', message: `Formato ${format} nao suportado ainda` }, + }); + } +}); + +// Delete artifact +router.delete('/:id', (req, res) => { + const artifact = artifacts.get(req.params.id); + + if (!artifact) { + return res.status(404).json({ + success: false, + error: { code: 'ARTIFACT_NOT_FOUND', message: 'Artefato nao encontrado' }, + }); + } + + artifacts.delete(req.params.id); + + res.json({ + success: true, + }); +}); + +export { router as artifactsRouter }; diff --git a/bmad-web/apps/api/src/routes/auth.ts b/bmad-web/apps/api/src/routes/auth.ts new file mode 100644 index 00000000..465dc32c --- /dev/null +++ b/bmad-web/apps/api/src/routes/auth.ts @@ -0,0 +1,196 @@ +import { Router } from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { z } from 'zod'; +import { v4 as uuid } from 'uuid'; + +const router = Router(); + +// In-memory user store (replace with database in production) +const users: Map = new Map(); + +const JWT_SECRET = process.env.JWT_SECRET || 'bmad-secret-key-change-in-production'; +const JWT_EXPIRES_IN = '7d'; + +// Validation schemas +const registerSchema = z.object({ + email: z.string().email('Email invalido'), + password: z.string().min(8, 'Senha deve ter no minimo 8 caracteres'), + name: z.string().min(2, 'Nome deve ter no minimo 2 caracteres'), +}); + +const loginSchema = z.object({ + email: z.string().email('Email invalido'), + password: z.string().min(1, 'Senha e obrigatoria'), +}); + +// Register +router.post('/register', async (req, res, next) => { + try { + const { email, password, name } = registerSchema.parse(req.body); + + // Check if user exists + const existingUser = Array.from(users.values()).find(u => u.email === email); + if (existingUser) { + return res.status(400).json({ + success: false, + error: { code: 'EMAIL_EXISTS', message: 'Este email ja esta em uso' }, + }); + } + + // Hash password + const passwordHash = await bcrypt.hash(password, 12); + + // Create user + const user = { + id: uuid(), + email, + name, + passwordHash, + createdAt: new Date(), + }; + users.set(user.id, user); + + // Generate token + const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + + // Set cookie + res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days + }); + + res.status(201).json({ + success: true, + data: { + user: { + id: user.id, + email: user.email, + name: user.name, + createdAt: user.createdAt, + }, + token, + }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Login +router.post('/login', async (req, res, next) => { + try { + const { email, password } = loginSchema.parse(req.body); + + // Find user + const user = Array.from(users.values()).find(u => u.email === email); + if (!user) { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_CREDENTIALS', message: 'Email ou senha incorretos' }, + }); + } + + // Verify password + const isValid = await bcrypt.compare(password, user.passwordHash); + if (!isValid) { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_CREDENTIALS', message: 'Email ou senha incorretos' }, + }); + } + + // Generate token + const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + + // Set cookie + res.cookie('token', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.json({ + success: true, + data: { + user: { + id: user.id, + email: user.email, + name: user.name, + createdAt: user.createdAt, + }, + token, + }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Logout +router.post('/logout', (req, res) => { + res.clearCookie('token'); + res.json({ success: true }); +}); + +// Get current user +router.get('/me', (req, res) => { + const token = req.cookies.token || req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + success: false, + error: { code: 'UNAUTHORIZED', message: 'Nao autenticado' }, + }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + const user = users.get(decoded.userId); + + if (!user) { + return res.status(401).json({ + success: false, + error: { code: 'USER_NOT_FOUND', message: 'Usuario nao encontrado' }, + }); + } + + res.json({ + success: true, + data: { + id: user.id, + email: user.email, + name: user.name, + createdAt: user.createdAt, + }, + }); + } catch { + return res.status(401).json({ + success: false, + error: { code: 'INVALID_TOKEN', message: 'Token invalido' }, + }); + } +}); + +export { router as authRouter }; diff --git a/bmad-web/apps/api/src/routes/projects.ts b/bmad-web/apps/api/src/routes/projects.ts new file mode 100644 index 00000000..794ac6b5 --- /dev/null +++ b/bmad-web/apps/api/src/routes/projects.ts @@ -0,0 +1,215 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { v4 as uuid } from 'uuid'; + +const router = Router(); + +// In-memory project store (replace with database in production) +const projects: Map = new Map(); + +// Validation schemas +const createProjectSchema = z.object({ + name: z.string().min(2, 'Nome deve ter no minimo 2 caracteres'), + description: z.string().optional(), + complexityLevel: z.number().min(0).max(4).default(2), + selectedModules: z.array(z.string()).default(['bmm']), +}); + +const updateProjectSchema = createProjectSchema.partial(); + +// Get all projects for user +router.get('/', (req, res) => { + const userProjects = Array.from(projects.values()) + .filter(p => p.userId === req.user?.id) + .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + + res.json({ + success: true, + data: userProjects, + }); +}); + +// Get project by ID +router.get('/:id', (req, res) => { + const project = projects.get(req.params.id); + + if (!project) { + return res.status(404).json({ + success: false, + error: { code: 'PROJECT_NOT_FOUND', message: 'Projeto nao encontrado' }, + }); + } + + if (project.userId !== req.user?.id) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Acesso negado' }, + }); + } + + res.json({ + success: true, + data: project, + }); +}); + +// Create project +router.post('/', (req, res, next) => { + try { + const data = createProjectSchema.parse(req.body); + + const project = { + id: uuid(), + ...data, + status: 'draft', + userId: req.user!.id, + createdAt: new Date(), + updatedAt: new Date(), + }; + + projects.set(project.id, project); + + res.status(201).json({ + success: true, + data: project, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Update project +router.put('/:id', (req, res, next) => { + try { + const project = projects.get(req.params.id); + + if (!project) { + return res.status(404).json({ + success: false, + error: { code: 'PROJECT_NOT_FOUND', message: 'Projeto nao encontrado' }, + }); + } + + if (project.userId !== req.user?.id) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Acesso negado' }, + }); + } + + const data = updateProjectSchema.parse(req.body); + const updatedProject = { + ...project, + ...data, + updatedAt: new Date(), + }; + + projects.set(project.id, updatedProject); + + res.json({ + success: true, + data: updatedProject, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { code: 'VALIDATION_ERROR', message: error.errors[0].message }, + }); + } + next(error); + } +}); + +// Delete project +router.delete('/:id', (req, res) => { + const project = projects.get(req.params.id); + + if (!project) { + return res.status(404).json({ + success: false, + error: { code: 'PROJECT_NOT_FOUND', message: 'Projeto nao encontrado' }, + }); + } + + if (project.userId !== req.user?.id) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Acesso negado' }, + }); + } + + projects.delete(req.params.id); + + res.json({ + success: true, + }); +}); + +// Get project artifacts +router.get('/:id/artifacts', (req, res) => { + const project = projects.get(req.params.id); + + if (!project) { + return res.status(404).json({ + success: false, + error: { code: 'PROJECT_NOT_FOUND', message: 'Projeto nao encontrado' }, + }); + } + + if (project.userId !== req.user?.id) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Acesso negado' }, + }); + } + + // TODO: Implement artifact retrieval + res.json({ + success: true, + data: [], + }); +}); + +// Get project workflows +router.get('/:id/workflows', (req, res) => { + const project = projects.get(req.params.id); + + if (!project) { + return res.status(404).json({ + success: false, + error: { code: 'PROJECT_NOT_FOUND', message: 'Projeto nao encontrado' }, + }); + } + + if (project.userId !== req.user?.id) { + return res.status(403).json({ + success: false, + error: { code: 'FORBIDDEN', message: 'Acesso negado' }, + }); + } + + // TODO: Implement workflow retrieval + res.json({ + success: true, + data: [], + }); +}); + +export { router as projectsRouter }; diff --git a/bmad-web/apps/api/src/routes/workflows.ts b/bmad-web/apps/api/src/routes/workflows.ts new file mode 100644 index 00000000..609af4ad --- /dev/null +++ b/bmad-web/apps/api/src/routes/workflows.ts @@ -0,0 +1,216 @@ +import { Router } from 'express'; +import { BMADAdapter } from '../bmad/adapter.js'; + +const router = Router(); +const bmadAdapter = new BMADAdapter(); + +// In-memory workflow instances (replace with database in production) +const workflowInstances: Map; + startedAt: Date; + completedAt?: Date; +}> = new Map(); + +// Get all available workflows +router.get('/', async (req, res, next) => { + try { + const workflows = await bmadAdapter.getAllWorkflows(); + + res.json({ + success: true, + data: workflows, + }); + } catch (error) { + next(error); + } +}); + +// Get workflow by ID +router.get('/:id', async (req, res, next) => { + try { + const workflow = await bmadAdapter.getWorkflow(req.params.id); + + if (!workflow) { + return res.status(404).json({ + success: false, + error: { code: 'WORKFLOW_NOT_FOUND', message: 'Workflow nao encontrado' }, + }); + } + + res.json({ + success: true, + data: workflow, + }); + } catch (error) { + next(error); + } +}); + +// Start a workflow +router.post('/:id/start', async (req, res, next) => { + try { + const { projectId } = req.body; + const workflowId = req.params.id; + + if (!projectId) { + return res.status(400).json({ + success: false, + error: { code: 'MISSING_PROJECT', message: 'Projeto e obrigatorio' }, + }); + } + + const workflow = await bmadAdapter.getWorkflow(workflowId); + if (!workflow) { + return res.status(404).json({ + success: false, + error: { code: 'WORKFLOW_NOT_FOUND', message: 'Workflow nao encontrado' }, + }); + } + + const instance = { + id: crypto.randomUUID(), + projectId, + workflowId, + currentStep: 1, + totalSteps: workflow.steps.length, + status: 'active', + stepOutputs: {}, + startedAt: new Date(), + }; + + workflowInstances.set(instance.id, instance); + + res.status(201).json({ + success: true, + data: { + ...instance, + currentStepDetails: workflow.steps[0], + }, + }); + } catch (error) { + next(error); + } +}); + +// Get workflow instance status +router.get('/instance/:instanceId/status', (req, res) => { + const instance = workflowInstances.get(req.params.instanceId); + + if (!instance) { + return res.status(404).json({ + success: false, + error: { code: 'INSTANCE_NOT_FOUND', message: 'Instancia nao encontrada' }, + }); + } + + res.json({ + success: true, + data: instance, + }); +}); + +// Complete current step and advance +router.post('/instance/:instanceId/step/complete', async (req, res, next) => { + try { + const { output } = req.body; + const instance = workflowInstances.get(req.params.instanceId); + + if (!instance) { + return res.status(404).json({ + success: false, + error: { code: 'INSTANCE_NOT_FOUND', message: 'Instancia nao encontrada' }, + }); + } + + if (instance.status !== 'active') { + return res.status(400).json({ + success: false, + error: { code: 'WORKFLOW_NOT_ACTIVE', message: 'Workflow nao esta ativo' }, + }); + } + + // Save step output + instance.stepOutputs[`step_${instance.currentStep}`] = output; + + // Advance to next step + if (instance.currentStep >= instance.totalSteps) { + instance.status = 'completed'; + instance.completedAt = new Date(); + } else { + instance.currentStep += 1; + } + + workflowInstances.set(instance.id, instance); + + const workflow = await bmadAdapter.getWorkflow(instance.workflowId); + const nextStep = instance.status === 'completed' + ? null + : workflow?.steps[instance.currentStep - 1]; + + res.json({ + success: true, + data: { + ...instance, + currentStepDetails: nextStep, + completed: instance.status === 'completed', + }, + }); + } catch (error) { + next(error); + } +}); + +// Pause workflow +router.post('/instance/:instanceId/pause', (req, res) => { + const instance = workflowInstances.get(req.params.instanceId); + + if (!instance) { + return res.status(404).json({ + success: false, + error: { code: 'INSTANCE_NOT_FOUND', message: 'Instancia nao encontrada' }, + }); + } + + instance.status = 'paused'; + workflowInstances.set(instance.id, instance); + + res.json({ + success: true, + data: instance, + }); +}); + +// Resume workflow +router.post('/instance/:instanceId/resume', (req, res) => { + const instance = workflowInstances.get(req.params.instanceId); + + if (!instance) { + return res.status(404).json({ + success: false, + error: { code: 'INSTANCE_NOT_FOUND', message: 'Instancia nao encontrada' }, + }); + } + + if (instance.status !== 'paused') { + return res.status(400).json({ + success: false, + error: { code: 'WORKFLOW_NOT_PAUSED', message: 'Workflow nao esta pausado' }, + }); + } + + instance.status = 'active'; + workflowInstances.set(instance.id, instance); + + res.json({ + success: true, + data: instance, + }); +}); + +export { router as workflowsRouter }; diff --git a/bmad-web/apps/api/src/websocket/index.ts b/bmad-web/apps/api/src/websocket/index.ts new file mode 100644 index 00000000..e44db533 --- /dev/null +++ b/bmad-web/apps/api/src/websocket/index.ts @@ -0,0 +1,146 @@ +import { Server, Socket } from 'socket.io'; +import jwt from 'jsonwebtoken'; +import { BMADAdapter } from '../bmad/adapter.js'; + +const JWT_SECRET = process.env.JWT_SECRET || 'bmad-secret-key-change-in-production'; +const bmadAdapter = new BMADAdapter(); + +interface AuthenticatedSocket extends Socket { + userId?: string; +} + +export function setupWebSocket(io: Server) { + // Authentication middleware + io.use((socket: AuthenticatedSocket, next) => { + const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return next(new Error('Authentication required')); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + socket.userId = decoded.userId; + next(); + } catch { + next(new Error('Invalid token')); + } + }); + + io.on('connection', (socket: AuthenticatedSocket) => { + console.log(`Client connected: ${socket.id} (User: ${socket.userId})`); + + // Join user-specific room + if (socket.userId) { + socket.join(`user:${socket.userId}`); + } + + // Join project room + socket.on('project:join', (projectId: string) => { + socket.join(`project:${projectId}`); + console.log(`Socket ${socket.id} joined project:${projectId}`); + }); + + // Leave project room + socket.on('project:leave', (projectId: string) => { + socket.leave(`project:${projectId}`); + console.log(`Socket ${socket.id} left project:${projectId}`); + }); + + // Handle chat message to agent + socket.on('agent:message', async (data: { + agentId: string; + message: string; + conversationId?: string; + projectId?: string; + }) => { + const { agentId, message, conversationId, projectId } = data; + + // Emit typing indicator + socket.emit('agent:typing', { agentId, isTyping: true }); + + try { + const response = await bmadAdapter.processMessage(agentId, message, { + conversationId, + projectId, + userId: socket.userId, + }); + + // Emit response + socket.emit('agent:message', { + agentId, + message: response.content, + metadata: response.metadata, + timestamp: new Date().toISOString(), + }); + + // Check for artifacts created + if (response.metadata?.artifacts) { + for (const artifactId of response.metadata.artifacts) { + socket.emit('artifact:created', { artifactId, projectId }); + } + } + } catch (error) { + socket.emit('error', { + code: 'AGENT_ERROR', + message: 'Erro ao processar mensagem do agente', + }); + } finally { + socket.emit('agent:typing', { agentId, isTyping: false }); + } + }); + + // Handle workflow advancement + socket.on('workflow:advance', async (data: { + instanceId: string; + stepOutput?: unknown; + }) => { + const { instanceId, stepOutput } = data; + + try { + // TODO: Implement actual workflow advancement + socket.emit('workflow:progress', { + instanceId, + currentStep: 2, // Example + completed: false, + timestamp: new Date().toISOString(), + }); + } catch (error) { + socket.emit('error', { + code: 'WORKFLOW_ERROR', + message: 'Erro ao avancar workflow', + }); + } + }); + + // Handle artifact updates + socket.on('artifact:update', async (data: { + artifactId: string; + content: string; + projectId?: string; + }) => { + const { artifactId, content, projectId } = data; + + // Broadcast update to other clients in the project + if (projectId) { + socket.to(`project:${projectId}`).emit('artifact:updated', { + artifactId, + updatedBy: socket.userId, + timestamp: new Date().toISOString(), + }); + } + }); + + // Handle disconnect + socket.on('disconnect', (reason) => { + console.log(`Client disconnected: ${socket.id} (Reason: ${reason})`); + }); + + // Handle errors + socket.on('error', (error) => { + console.error(`Socket error for ${socket.id}:`, error); + }); + }); + + console.log('WebSocket handlers configured'); +} diff --git a/bmad-web/apps/api/tsconfig.json b/bmad-web/apps/api/tsconfig.json new file mode 100644 index 00000000..cf8371cc --- /dev/null +++ b/bmad-web/apps/api/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "noEmit": false, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@bmad/core": ["../../packages/bmad-core/src"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/bmad-web/apps/web/.env.local.example b/bmad-web/apps/web/.env.local.example new file mode 100644 index 00000000..350c56f7 --- /dev/null +++ b/bmad-web/apps/web/.env.local.example @@ -0,0 +1,11 @@ +# BMAD Web Frontend Environment Variables + +# API URL +NEXT_PUBLIC_API_URL=http://localhost:4000 + +# WebSocket URL +NEXT_PUBLIC_WS_URL=ws://localhost:4000 + +# App Configuration +NEXT_PUBLIC_APP_NAME=BMAD +NEXT_PUBLIC_APP_URL=http://localhost:3000 diff --git a/bmad-web/apps/web/next.config.js b/bmad-web/apps/web/next.config.js new file mode 100644 index 00000000..9d168b88 --- /dev/null +++ b/bmad-web/apps/web/next.config.js @@ -0,0 +1,13 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ['@bmad/core', '@bmad/ui'], + experimental: { + serverComponentsExternalPackages: ['@bmad/core'], + }, + images: { + domains: ['avatars.githubusercontent.com'], + }, +}; + +module.exports = nextConfig; diff --git a/bmad-web/apps/web/package.json b/bmad-web/apps/web/package.json new file mode 100644 index 00000000..f6b890c6 --- /dev/null +++ b/bmad-web/apps/web/package.json @@ -0,0 +1,58 @@ +{ + "name": "@bmad/web", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "next start", + "lint": "next lint", + "clean": "rm -rf .next node_modules" + }, + "dependencies": { + "@bmad/core": "workspace:*", + "@bmad/ui": "workspace:*", + "@hookform/resolvers": "^3.3.2", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-query": "^5.8.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "framer-motion": "^10.16.5", + "lucide-react": "^0.294.0", + "next": "14.0.3", + "next-themes": "^0.2.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.48.2", + "react-markdown": "^9.0.1", + "remark-gfm": "^4.0.0", + "socket.io-client": "^4.7.2", + "tailwind-merge": "^2.1.0", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4", + "zustand": "^4.4.7" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@types/react": "^18.2.38", + "@types/react-dom": "^18.2.17", + "autoprefixer": "^10.4.16", + "eslint": "^8.54.0", + "eslint-config-next": "14.0.3", + "postcss": "^8.4.31", + "tailwindcss": "^3.3.5", + "typescript": "^5.3.0" + } +} diff --git a/bmad-web/apps/web/postcss.config.js b/bmad-web/apps/web/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/bmad-web/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/bmad-web/apps/web/src/app/globals.css b/bmad-web/apps/web/src/app/globals.css new file mode 100644 index 00000000..c80a7afa --- /dev/null +++ b/bmad-web/apps/web/src/app/globals.css @@ -0,0 +1,120 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-muted/50; +} + +::-webkit-scrollbar-thumb { + @apply bg-muted-foreground/30 rounded-full; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-muted-foreground/50; +} + +/* Chat message animations */ +.message-enter { + opacity: 0; + transform: translateY(10px); +} + +.message-enter-active { + opacity: 1; + transform: translateY(0); + transition: opacity 300ms, transform 300ms; +} + +/* Typing indicator */ +.typing-dot { + animation: typing-bounce 1.4s infinite ease-in-out both; +} + +.typing-dot:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-dot:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes typing-bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } +} + +/* Gradient backgrounds */ +.bg-gradient-bmad { + background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 50%, #EC4899 100%); +} + +.bg-gradient-bmad-subtle { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(139, 92, 246, 0.1) 50%, rgba(236, 72, 153, 0.1) 100%); +} diff --git a/bmad-web/apps/web/src/app/layout.tsx b/bmad-web/apps/web/src/app/layout.tsx new file mode 100644 index 00000000..b8d7b241 --- /dev/null +++ b/bmad-web/apps/web/src/app/layout.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from 'next'; +import { Inter } from 'next/font/google'; +import './globals.css'; +import { Providers } from './providers'; + +const inter = Inter({ subsets: ['latin'] }); + +export const metadata: Metadata = { + title: 'BMAD - Build More, Architect Dreams', + description: + 'AI-driven agile development framework with 21 specialized agents and 50+ guided workflows', + keywords: [ + 'BMAD', + 'AI', + 'agile', + 'development', + 'agents', + 'workflows', + 'product management', + ], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/bmad-web/apps/web/src/app/page.tsx b/bmad-web/apps/web/src/app/page.tsx new file mode 100644 index 00000000..97bbf38f --- /dev/null +++ b/bmad-web/apps/web/src/app/page.tsx @@ -0,0 +1,212 @@ +import Link from 'next/link'; +import { ArrowRight, Bot, Workflow, FileText, Zap } from 'lucide-react'; + +export default function HomePage() { + return ( +
+ {/* Header */} +
+
+
+
+ BMAD +
+ +
+
+ + {/* Hero Section */} +
+
+

+ Build More,{' '} + + Architect Dreams + +

+

+ Framework de desenvolvimento agil impulsionado por IA com 21 agentes + especializados e mais de 50 workflows guiados para transformar suas + ideias em software de qualidade. +

+
+ + Iniciar Projeto + + + + Ver Demo + +
+
+
+ + {/* Features Grid */} +
+
+

+ Tudo que voce precisa para desenvolver com excelencia +

+
+ } + title="21+ Agentes IA" + description="PM, Arquiteto, Dev, UX Designer, Tech Writer e muito mais" + /> + } + title="50+ Workflows" + description="Da analise a implementacao, guias passo-a-passo" + /> + } + title="Artefatos Automaticos" + description="PRD, arquitetura, epics, stories gerados automaticamente" + /> + } + title="Quick Flow" + description="De ideia a especificacao em menos de 5 minutos" + /> +
+
+
+ + {/* Tracks Section */} +
+
+

+ Escolha seu ritmo de desenvolvimento +

+
+ + + +
+
+
+ + {/* CTA Section */} +
+
+

+ Pronto para transformar suas ideias? +

+

+ Junte-se a milhares de desenvolvedores que ja usam o BMAD para criar + software de qualidade. +

+ + Comece Agora - E Gratis + + +
+
+ + {/* Footer */} +
+
+

BMAD - Build More, Architect Dreams

+

+ Framework de desenvolvimento agil impulsionado por IA +

+
+
+
+ ); +} + +function FeatureCard({ + icon, + title, + description, +}: { + icon: React.ReactNode; + title: string; + description: string; +}) { + return ( +
+
+ {icon} +
+

{title}

+

{description}

+
+ ); +} + +function TrackCard({ + title, + time, + description, + color, + featured, +}: { + title: string; + time: string; + description: string; + color: 'green' | 'blue' | 'purple'; + featured?: boolean; +}) { + const colorClasses = { + green: 'border-green-500 bg-green-500/10', + blue: 'border-blue-500 bg-blue-500/10', + purple: 'border-purple-500 bg-purple-500/10', + }; + + return ( +
+
+ {time} para primeira story +
+

{title}

+

{description}

+ {featured && ( +
+ Mais Popular +
+ )} +
+ ); +} diff --git a/bmad-web/apps/web/src/app/providers.tsx b/bmad-web/apps/web/src/app/providers.tsx new file mode 100644 index 00000000..bb82953e --- /dev/null +++ b/bmad-web/apps/web/src/app/providers.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from 'next-themes'; +import { useState } from 'react'; + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + refetchOnWindowFocus: false, + }, + }, + }) + ); + + return ( + + + {children} + + + ); +} diff --git a/bmad-web/apps/web/src/components/chat/agent-chat.tsx b/bmad-web/apps/web/src/components/chat/agent-chat.tsx new file mode 100644 index 00000000..53373d73 --- /dev/null +++ b/bmad-web/apps/web/src/components/chat/agent-chat.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Send, Paperclip, MoreVertical } from 'lucide-react'; +import { useAppStore } from '@/stores/app-store'; +import type { Message, Agent } from '@/types'; +import { ChatMessage } from './chat-message'; +import { TypingIndicator } from './typing-indicator'; +import { QuickActions } from './quick-actions'; + +interface AgentChatProps { + agent: Agent; + projectId: string; + onArtifactCreated?: (artifactId: string) => void; +} + +export function AgentChat({ agent, projectId, onArtifactCreated }: AgentChatProps) { + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + const { currentConversation, addMessage, agentTyping, setAgentTyping } = useAppStore(); + + const messages = currentConversation?.messages || []; + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages, agentTyping]); + + // Auto-resize textarea + const handleInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value); + e.target.style.height = 'auto'; + e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || isLoading) return; + + const userMessage: Message = { + id: crypto.randomUUID(), + role: 'user', + content: input.trim(), + timestamp: new Date(), + }; + + addMessage(userMessage); + setInput(''); + setIsLoading(true); + setAgentTyping(true); + + // Reset textarea height + if (inputRef.current) { + inputRef.current.style.height = 'auto'; + } + + try { + // TODO: Replace with actual API call + const response = await simulateAgentResponse(agent, input); + + const agentMessage: Message = { + id: crypto.randomUUID(), + role: 'agent', + content: response.content, + metadata: response.metadata, + timestamp: new Date(), + }; + + addMessage(agentMessage); + + // Check if an artifact was created + if (response.metadata?.artifacts?.length) { + onArtifactCreated?.(response.metadata.artifacts[0]); + } + } catch (error) { + console.error('Error sending message:', error); + addMessage({ + id: crypto.randomUUID(), + role: 'system', + content: 'Desculpe, ocorreu um erro ao processar sua mensagem. Por favor, tente novamente.', + timestamp: new Date(), + }); + } finally { + setIsLoading(false); + setAgentTyping(false); + } + }; + + const handleQuickAction = (action: string) => { + setInput(action); + inputRef.current?.focus(); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + return ( +
+ {/* Chat Header */} +
+
+
+ {agent.icon || agent.name[0]} +
+
+

{agent.name}

+

{agent.title}

+
+
+ +
+ + {/* Messages Area */} +
+ {messages.length === 0 ? ( +
+
+ {agent.icon || agent.name[0]} +
+

+ Ola! Eu sou {agent.name} +

+

+ {agent.persona.role} +

+ +
+ ) : ( +
+ + {messages.map((message) => ( + + + + ))} + + + {agentTyping && ( + + + + )} + +
+
+ )} +
+ + {/* Input Area */} +
+
+ +
+