feat: add consumer frontend web application structure

Adds complete monorepo structure for BMAD web application:

- Frontend (Next.js 14):
  - Landing page with tracks overview
  - Agent chat interface with real-time messaging
  - Zustand state management
  - TanStack Query for data fetching
  - Tailwind CSS with shadcn/ui components

- Backend (Express + Socket.io):
  - REST API for auth, projects, agents, workflows, artifacts
  - WebSocket for real-time agent communication
  - JWT authentication
  - BMAD adapter for loading agents and workflows

- Shared packages:
  - @bmad/core: Types, agent/workflow loaders
  - @bmad/ui: Reusable UI components

- Documentation:
  - Architecture document with system design
  - Complete README with setup instructions

This enables end-users to interact with BMAD agents through
a web interface instead of CLI.
This commit is contained in:
Claude 2026-01-10 23:59:42 +00:00
parent c18904d674
commit 1ac7a69a46
No known key found for this signature in database
56 changed files with 5294 additions and 0 deletions

22
bmad-web/.env.example Normal file
View File

@ -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

257
bmad-web/README.md Normal file
View File

@ -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.

View File

@ -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=

View File

@ -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"
}
}

View File

@ -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<string, string>;
}
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<string, AgentDefinition> = new Map();
private workflows: Map<string, WorkflowDefinition> = 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<void> {
if (!this.loaded) {
await Promise.all([this.loadAgents(), this.loadWorkflows()]);
this.loaded = true;
}
}
async loadAgents(): Promise<void> {
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<void> {
// 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<AgentDefinition[]> {
await this.ensureLoaded();
return Array.from(this.agents.values());
}
async getAgent(id: string): Promise<AgentDefinition | undefined> {
await this.ensureLoaded();
return this.agents.get(id);
}
async getAgentsByModule(moduleId: string): Promise<AgentDefinition[]> {
await this.ensureLoaded();
return Array.from(this.agents.values()).filter(a => a.module === moduleId);
}
async getAgentActions(agentId: string): Promise<AgentMenuItem[]> {
await this.ensureLoaded();
const agent = this.agents.get(agentId);
return agent?.menu || [];
}
async getAllWorkflows(): Promise<WorkflowDefinition[]> {
await this.ensureLoaded();
return Array.from(this.workflows.values());
}
async getWorkflow(id: string): Promise<WorkflowDefinition | undefined> {
await this.ensureLoaded();
return this.workflows.get(id);
}
async processMessage(
agentId: string,
message: string,
context: MessageContext
): Promise<MessageResponse> {
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,
})),
},
};
}
}

View File

@ -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 };

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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 };

View File

@ -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<string, {
id: string;
projectId: string;
type: string;
name: string;
content: string;
version: number;
history: { version: number; content: string; updatedAt: Date }[];
createdAt: Date;
updatedAt: Date;
}> = 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 };

View File

@ -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<string, {
id: string;
email: string;
name: string;
passwordHash: string;
createdAt: Date;
}> = 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 };

View File

@ -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<string, {
id: string;
name: string;
description?: string;
complexityLevel: number;
selectedModules: string[];
status: string;
userId: string;
createdAt: Date;
updatedAt: Date;
}> = 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 };

View File

@ -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<string, {
id: string;
projectId: string;
workflowId: string;
currentStep: number;
totalSteps: number;
status: string;
stepOutputs: Record<string, unknown>;
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 };

View File

@ -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');
}

View File

@ -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"]
}

View File

@ -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

View File

@ -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;

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -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%);
}

View File

@ -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 (
<html lang="pt-BR" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@ -0,0 +1,212 @@
import Link from 'next/link';
import { ArrowRight, Bot, Workflow, FileText, Zap } from 'lucide-react';
export default function HomePage() {
return (
<div className="min-h-screen bg-gradient-to-b from-background to-muted">
{/* Header */}
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-gradient-bmad" />
<span className="text-xl font-bold">BMAD</span>
</div>
<nav className="flex items-center gap-4">
<Link
href="/login"
className="text-sm font-medium text-muted-foreground hover:text-foreground"
>
Entrar
</Link>
<Link
href="/register"
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90"
>
Comece Gratis
</Link>
</nav>
</div>
</header>
{/* Hero Section */}
<section className="container py-24 text-center">
<div className="mx-auto max-w-3xl space-y-6">
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
Build More,{' '}
<span className="bg-gradient-bmad bg-clip-text text-transparent">
Architect Dreams
</span>
</h1>
<p className="text-xl text-muted-foreground">
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.
</p>
<div className="flex items-center justify-center gap-4">
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground shadow-lg hover:bg-primary/90"
>
Iniciar Projeto
<ArrowRight className="h-4 w-4" />
</Link>
<Link
href="/demo"
className="inline-flex items-center justify-center gap-2 rounded-md border border-input bg-background px-6 py-3 text-sm font-medium shadow-sm hover:bg-accent"
>
Ver Demo
</Link>
</div>
</div>
</section>
{/* Features Grid */}
<section className="container py-16">
<div className="mx-auto max-w-5xl">
<h2 className="mb-12 text-center text-3xl font-bold">
Tudo que voce precisa para desenvolver com excelencia
</h2>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
<FeatureCard
icon={<Bot className="h-10 w-10" />}
title="21+ Agentes IA"
description="PM, Arquiteto, Dev, UX Designer, Tech Writer e muito mais"
/>
<FeatureCard
icon={<Workflow className="h-10 w-10" />}
title="50+ Workflows"
description="Da analise a implementacao, guias passo-a-passo"
/>
<FeatureCard
icon={<FileText className="h-10 w-10" />}
title="Artefatos Automaticos"
description="PRD, arquitetura, epics, stories gerados automaticamente"
/>
<FeatureCard
icon={<Zap className="h-10 w-10" />}
title="Quick Flow"
description="De ideia a especificacao em menos de 5 minutos"
/>
</div>
</div>
</section>
{/* Tracks Section */}
<section className="container py-16">
<div className="mx-auto max-w-5xl">
<h2 className="mb-12 text-center text-3xl font-bold">
Escolha seu ritmo de desenvolvimento
</h2>
<div className="grid gap-6 md:grid-cols-3">
<TrackCard
title="Quick Flow"
time="~5 min"
description="Para bug fixes, pequenas features e ajustes rapidos"
color="green"
/>
<TrackCard
title="BMAD Method"
time="~15 min"
description="Para produtos e plataformas com escopo moderado"
color="blue"
featured
/>
<TrackCard
title="Enterprise"
time="~30 min"
description="Para sistemas complexos com requisitos de compliance"
color="purple"
/>
</div>
</div>
</section>
{/* CTA Section */}
<section className="container py-24">
<div className="mx-auto max-w-2xl rounded-2xl bg-gradient-bmad p-8 text-center text-white">
<h2 className="mb-4 text-3xl font-bold">
Pronto para transformar suas ideias?
</h2>
<p className="mb-6 text-white/80">
Junte-se a milhares de desenvolvedores que ja usam o BMAD para criar
software de qualidade.
</p>
<Link
href="/register"
className="inline-flex items-center justify-center gap-2 rounded-md bg-white px-6 py-3 text-sm font-medium text-primary shadow-lg hover:bg-white/90"
>
Comece Agora - E Gratis
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</section>
{/* Footer */}
<footer className="border-t py-12">
<div className="container text-center text-sm text-muted-foreground">
<p>BMAD - Build More, Architect Dreams</p>
<p className="mt-2">
Framework de desenvolvimento agil impulsionado por IA
</p>
</div>
</footer>
</div>
);
}
function FeatureCard({
icon,
title,
description,
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="rounded-xl border bg-card p-6 text-center shadow-sm">
<div className="mb-4 inline-flex items-center justify-center text-primary">
{icon}
</div>
<h3 className="mb-2 font-semibold">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
);
}
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 (
<div
className={`rounded-xl border-2 p-6 ${colorClasses[color]} ${featured ? 'ring-2 ring-primary ring-offset-2' : ''}`}
>
<div className="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
{time} para primeira story
</div>
<h3 className="mb-2 text-xl font-bold">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
{featured && (
<div className="mt-4 inline-block rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
Mais Popular
</div>
)}
</div>
);
}

View File

@ -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 (
<QueryClientProvider client={queryClient}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</QueryClientProvider>
);
}

View File

@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>) => {
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 (
<div className="flex h-full flex-col">
{/* Chat Header */}
<div className="flex items-center justify-between border-b px-4 py-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-lg text-primary-foreground">
{agent.icon || agent.name[0]}
</div>
<div>
<h2 className="font-semibold">{agent.name}</h2>
<p className="text-sm text-muted-foreground">{agent.title}</p>
</div>
</div>
<button className="rounded-lg p-2 hover:bg-muted">
<MoreVertical className="h-5 w-5" />
</button>
</div>
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4">
{messages.length === 0 ? (
<div className="flex h-full flex-col items-center justify-center text-center">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 text-3xl">
{agent.icon || agent.name[0]}
</div>
<h3 className="mb-2 text-lg font-semibold">
Ola! Eu sou {agent.name}
</h3>
<p className="mb-6 max-w-md text-muted-foreground">
{agent.persona.role}
</p>
<QuickActions actions={agent.menu} onSelect={handleQuickAction} />
</div>
) : (
<div className="space-y-4">
<AnimatePresence initial={false}>
{messages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<ChatMessage
message={message}
agentName={agent.name}
agentIcon={agent.icon}
/>
</motion.div>
))}
</AnimatePresence>
{agentTyping && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
<TypingIndicator agentName={agent.name} />
</motion.div>
)}
<div ref={messagesEndRef} />
</div>
)}
</div>
{/* Input Area */}
<div className="border-t p-4">
<form onSubmit={handleSubmit} className="flex items-end gap-2">
<button
type="button"
className="flex h-10 w-10 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground"
>
<Paperclip className="h-5 w-5" />
</button>
<div className="flex-1">
<textarea
ref={inputRef}
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={`Mensagem para ${agent.name}...`}
className="max-h-[200px] min-h-[40px] w-full resize-none rounded-lg border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
rows={1}
/>
</div>
<button
type="submit"
disabled={!input.trim() || isLoading}
className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
>
<Send className="h-5 w-5" />
</button>
</form>
</div>
</div>
);
}
// Simulate agent response (replace with actual API call)
async function simulateAgentResponse(
agent: Agent,
userMessage: string
): Promise<{ content: string; metadata?: Message['metadata'] }> {
await new Promise((resolve) => setTimeout(resolve, 1500));
return {
content: `Obrigado pela sua mensagem! Como ${agent.title}, estou aqui para ajudar. Voce mencionou: "${userMessage}". O que gostaria de fazer a seguir?\n\n**Sugestoes:**\n- Iniciar um novo workflow\n- Revisar artefatos existentes\n- Continuar de onde paramos`,
metadata: {
suggestedActions: [
{ label: 'Iniciar Workflow', action: 'start_workflow', primary: true },
{ label: 'Ver Artefatos', action: 'view_artifacts' },
],
},
};
}

View File

@ -0,0 +1,108 @@
'use client';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { Message } from '@/types';
interface ChatMessageProps {
message: Message;
agentName: string;
agentIcon?: string;
}
export function ChatMessage({ message, agentName, agentIcon }: ChatMessageProps) {
const isUser = message.role === 'user';
const isSystem = message.role === 'system';
if (isSystem) {
return (
<div className="flex justify-center">
<div className="max-w-md rounded-lg bg-muted px-4 py-2 text-center text-sm text-muted-foreground">
{message.content}
</div>
</div>
);
}
return (
<div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
{/* Avatar */}
<div
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm ${
isUser
? 'bg-secondary text-secondary-foreground'
: 'bg-primary text-primary-foreground'
}`}
>
{isUser ? 'Eu' : agentIcon || agentName[0]}
</div>
{/* Message Content */}
<div
className={`max-w-[70%] rounded-2xl px-4 py-2 ${
isUser
? 'bg-primary text-primary-foreground'
: 'bg-muted text-foreground'
}`}
>
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{message.content}
</ReactMarkdown>
</div>
{/* Suggested Actions */}
{message.metadata?.suggestedActions && (
<div className="mt-3 flex flex-wrap gap-2 border-t border-border/50 pt-3">
{message.metadata.suggestedActions.map((action, index) => (
<button
key={index}
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
action.primary
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'bg-background text-foreground hover:bg-accent'
}`}
>
{action.label}
</button>
))}
</div>
)}
{/* Artifacts */}
{message.metadata?.artifacts && message.metadata.artifacts.length > 0 && (
<div className="mt-3 border-t border-border/50 pt-3">
<p className="mb-2 text-xs font-medium text-muted-foreground">
Artefatos criados:
</p>
{message.metadata.artifacts.map((artifactId) => (
<button
key={artifactId}
className="flex w-full items-center gap-2 rounded-lg bg-background p-2 text-left text-sm hover:bg-accent"
>
<div className="h-8 w-8 rounded bg-primary/10" />
<span className="truncate">{artifactId}</span>
</button>
))}
</div>
)}
{/* Timestamp */}
<div
className={`mt-1 text-xs ${
isUser ? 'text-primary-foreground/70' : 'text-muted-foreground'
}`}
>
{formatTime(message.timestamp)}
</div>
</div>
</div>
);
}
function formatTime(date: Date): string {
return new Intl.DateTimeFormat('pt-BR', {
hour: '2-digit',
minute: '2-digit',
}).format(new Date(date));
}

View File

@ -0,0 +1,35 @@
'use client';
import type { AgentMenuItem } from '@/types';
interface QuickActionsProps {
actions: AgentMenuItem[];
onSelect: (action: string) => void;
}
export function QuickActions({ actions, onSelect }: QuickActionsProps) {
// Show max 6 actions
const displayActions = actions.slice(0, 6);
return (
<div className="grid max-w-lg grid-cols-2 gap-2">
{displayActions.map((action, index) => (
<button
key={index}
onClick={() => onSelect(action.trigger)}
className="rounded-lg border bg-card p-3 text-left transition-colors hover:bg-accent"
>
<p className="text-sm font-medium">{extractLabel(action.description)}</p>
<p className="mt-1 text-xs text-muted-foreground">
Digite: {action.trigger}
</p>
</button>
))}
</div>
);
}
function extractLabel(description: string): string {
// Remove [XX] prefix from description
return description.replace(/^\[[\w-]+\]\s*/, '');
}

View File

@ -0,0 +1,22 @@
'use client';
interface TypingIndicatorProps {
agentName: string;
}
export function TypingIndicator({ agentName }: TypingIndicatorProps) {
return (
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-sm text-primary-foreground">
{agentName[0]}
</div>
<div className="rounded-2xl bg-muted px-4 py-3">
<div className="flex gap-1">
<span className="typing-dot h-2 w-2 rounded-full bg-muted-foreground" />
<span className="typing-dot h-2 w-2 rounded-full bg-muted-foreground" />
<span className="typing-dot h-2 w-2 rounded-full bg-muted-foreground" />
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,146 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { Project, Agent, Conversation, Message, WorkflowInstance, User } from '@/types';
interface AppState {
// User
user: User | null;
setUser: (user: User | null) => void;
// Current Project
currentProject: Project | null;
setCurrentProject: (project: Project | null) => void;
// Projects List
projects: Project[];
setProjects: (projects: Project[]) => void;
addProject: (project: Project) => void;
updateProject: (id: string, updates: Partial<Project>) => void;
removeProject: (id: string) => void;
// Current Agent
currentAgent: Agent | null;
setCurrentAgent: (agent: Agent | null) => void;
// Available Agents
agents: Agent[];
setAgents: (agents: Agent[]) => void;
// Current Conversation
currentConversation: Conversation | null;
setCurrentConversation: (conversation: Conversation | null) => void;
addMessage: (message: Message) => void;
// Active Workflow
activeWorkflow: WorkflowInstance | null;
setActiveWorkflow: (workflow: WorkflowInstance | null) => void;
updateWorkflowStep: (step: number) => void;
// UI State
sidebarOpen: boolean;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
// Agent Typing State
agentTyping: boolean;
setAgentTyping: (typing: boolean) => void;
// Reset
reset: () => void;
}
const initialState = {
user: null,
currentProject: null,
projects: [],
currentAgent: null,
agents: [],
currentConversation: null,
activeWorkflow: null,
sidebarOpen: true,
agentTyping: false,
};
export const useAppStore = create<AppState>()(
persist(
(set, get) => ({
...initialState,
// User
setUser: (user) => set({ user }),
// Current Project
setCurrentProject: (project) => set({ currentProject: project }),
// Projects
setProjects: (projects) => set({ projects }),
addProject: (project) =>
set((state) => ({ projects: [...state.projects, project] })),
updateProject: (id, updates) =>
set((state) => ({
projects: state.projects.map((p) =>
p.id === id ? { ...p, ...updates } : p
),
currentProject:
state.currentProject?.id === id
? { ...state.currentProject, ...updates }
: state.currentProject,
})),
removeProject: (id) =>
set((state) => ({
projects: state.projects.filter((p) => p.id !== id),
currentProject:
state.currentProject?.id === id ? null : state.currentProject,
})),
// Agent
setCurrentAgent: (agent) => set({ currentAgent: agent }),
setAgents: (agents) => set({ agents }),
// Conversation
setCurrentConversation: (conversation) =>
set({ currentConversation: conversation }),
addMessage: (message) =>
set((state) => {
if (!state.currentConversation) return state;
return {
currentConversation: {
...state.currentConversation,
messages: [...state.currentConversation.messages, message],
},
};
}),
// Workflow
setActiveWorkflow: (workflow) => set({ activeWorkflow: workflow }),
updateWorkflowStep: (step) =>
set((state) => {
if (!state.activeWorkflow) return state;
return {
activeWorkflow: {
...state.activeWorkflow,
currentStep: step,
status: step >= state.activeWorkflow.totalSteps ? 'completed' : 'active',
},
};
}),
// UI
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
// Agent Typing
setAgentTyping: (typing) => set({ agentTyping: typing }),
// Reset
reset: () => set(initialState),
}),
{
name: 'bmad-app-storage',
partialize: (state) => ({
user: state.user,
sidebarOpen: state.sidebarOpen,
}),
}
)
);

View File

@ -0,0 +1,204 @@
// User Types
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
preferences: UserPreferences;
createdAt: Date;
}
export interface UserPreferences {
theme: 'light' | 'dark' | 'system';
language: string;
communicationStyle: 'technical' | 'simple' | 'balanced';
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'expert';
}
// Project Types
export interface Project {
id: string;
name: string;
description?: string;
complexityLevel: ComplexityLevel;
selectedModules: string[];
activeWorkflow?: string;
artifacts: Artifact[];
status: ProjectStatus;
createdAt: Date;
updatedAt: Date;
userId: string;
}
export type ComplexityLevel = 0 | 1 | 2 | 3 | 4;
export type ProjectStatus =
| 'draft'
| 'analysis'
| 'planning'
| 'solutioning'
| 'implementation'
| 'completed';
// Agent Types
export interface Agent {
id: string;
name: string;
title: string;
icon: string;
module: string;
persona: AgentPersona;
menu: AgentMenuItem[];
prompts: Record<string, string>;
}
export interface AgentPersona {
role: string;
identity?: string;
communicationStyle: string;
principles?: string[];
}
export interface AgentMenuItem {
trigger: string;
exec?: string;
description: string;
requiresContext?: string[];
}
// Conversation Types
export interface Conversation {
id: string;
projectId: string;
agentId: string;
messages: Message[];
context: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
export interface Message {
id: string;
role: 'user' | 'agent' | 'system';
content: string;
metadata?: MessageMetadata;
timestamp: Date;
}
export interface MessageMetadata {
workflowStep?: string;
action?: string;
artifacts?: string[];
suggestedActions?: SuggestedAction[];
}
export interface SuggestedAction {
label: string;
action: string;
primary?: boolean;
}
// Workflow Types
export interface Workflow {
id: string;
name: string;
description: string;
module: string;
phase: WorkflowPhase;
steps: WorkflowStep[];
estimatedTime?: string;
}
export type WorkflowPhase =
| 'analysis'
| 'planning'
| 'solutioning'
| 'implementation';
export interface WorkflowStep {
id: string;
name: string;
description: string;
instruction: string;
expectedOutput?: string;
}
export interface WorkflowInstance {
id: string;
projectId: string;
workflowId: string;
currentStep: number;
totalSteps: number;
status: WorkflowInstanceStatus;
stepOutputs: Record<string, unknown>;
startedAt: Date;
completedAt?: Date;
}
export type WorkflowInstanceStatus =
| 'active'
| 'paused'
| 'completed'
| 'failed';
// Artifact Types
export interface Artifact {
id: string;
projectId: string;
type: ArtifactType;
name: string;
content: string;
version: number;
createdAt: Date;
updatedAt: Date;
}
export type ArtifactType =
| 'prd'
| 'architecture'
| 'epic'
| 'story'
| 'spec'
| 'diagram'
| 'tech-spec'
| 'test-plan';
// Module Types
export interface Module {
id: string;
name: string;
description: string;
agents: string[];
workflows: string[];
icon?: string;
}
// API Response Types
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
// Pagination
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
// WebSocket Event Types
export type WebSocketEvent =
| { type: 'agent:message'; payload: { agentId: string; message: string; metadata?: MessageMetadata } }
| { type: 'agent:typing'; payload: { agentId: string; isTyping: boolean } }
| { type: 'workflow:progress'; payload: { instanceId: string; currentStep: number; completed: boolean } }
| { type: 'artifact:updated'; payload: { artifactId: string; version: number } }
| { type: 'error'; payload: { code: string; message: string } };

View File

@ -0,0 +1,100 @@
import type { Config } from 'tailwindcss';
const config: Config = {
darkMode: ['class'],
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/ui/src/**/*.{js,ts,jsx,tsx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
// BMAD Brand Colors
bmad: {
blue: '#3B82F6',
purple: '#8B5CF6',
green: '#10B981',
orange: '#F59E0B',
pink: '#EC4899',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
'fade-in': {
from: { opacity: '0' },
to: { opacity: '1' },
},
'slide-in-from-bottom': {
from: { transform: 'translateY(10px)', opacity: '0' },
to: { transform: 'translateY(0)', opacity: '1' },
},
pulse: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0.5' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'fade-in': 'fade-in 0.3s ease-out',
'slide-in': 'slide-in-from-bottom 0.3s ease-out',
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
},
},
plugins: [require('tailwindcss-animate')],
};
export default config;

View File

@ -0,0 +1,30 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
"@bmad/core": ["../../packages/bmad-core/src"],
"@bmad/ui": ["../../packages/ui/src"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

31
bmad-web/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "bmad-web",
"version": "1.0.0",
"private": true,
"description": "BMAD-METHOD Web Application - Consumer Frontend",
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"db:generate": "turbo run db:generate --filter=@bmad/api",
"db:push": "turbo run db:push --filter=@bmad/api",
"db:studio": "turbo run db:studio --filter=@bmad/api"
},
"devDependencies": {
"@types/node": "^20.10.0",
"prettier": "^3.1.0",
"turbo": "^2.0.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "npm@10.0.0"
}

View File

@ -0,0 +1,27 @@
{
"name": "@bmad/core",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint src --ext .ts",
"clean": "rm -rf dist node_modules"
},
"dependencies": {
"yaml": "^2.3.4",
"glob": "^10.3.10"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
}
}

View File

@ -0,0 +1,58 @@
/**
* BMAD Core Package
*
* This package provides the core functionality for BMAD Method:
* - Agent definitions and parsing
* - Workflow definitions and execution
* - Module system
* - Shared types
*/
// Export types
export * from './types/index.js';
// Export utilities
export * from './utils/yaml-parser.js';
export * from './utils/agent-loader.js';
export * from './utils/workflow-loader.js';
// Export constants
export const BMAD_VERSION = '6.0.0';
export const COMPLEXITY_LEVELS = {
0: { name: 'Bug Fix', description: 'Simple bug fixes' },
1: { name: 'Small Feature', description: 'Small features and enhancements' },
2: { name: 'Standard Product', description: 'Standard products and platforms' },
3: { name: 'Complex Platform', description: 'Complex platforms with multiple integrations' },
4: { name: 'Enterprise System', description: 'Enterprise systems with compliance requirements' },
} as const;
export const MODULES = {
bmm: {
id: 'bmm',
name: 'BMAD Method Module',
description: 'Core agile development framework',
},
cis: {
id: 'cis',
name: 'Creative Intelligence Suite',
description: 'Innovation and brainstorming tools',
},
bmb: {
id: 'bmb',
name: 'BMAD Builder',
description: 'Custom agent and module creation',
},
bmgd: {
id: 'bmgd',
name: 'BMAD Game Dev',
description: 'Game development specialization',
},
} as const;
export const WORKFLOW_PHASES = [
'analysis',
'planning',
'solutioning',
'implementation',
] as const;

View File

@ -0,0 +1,277 @@
/**
* BMAD Core Types
*/
// Agent Types
export interface AgentMetadata {
id: string;
name: string;
title: string;
icon?: string;
module: string;
version?: string;
}
export interface AgentPersona {
role: string;
identity?: string;
communicationStyle: string;
principles?: string[];
expertise?: string[];
}
export interface AgentMenuItem {
trigger: string;
exec?: string;
description: string;
requiresContext?: string[];
fuzzyMatch?: boolean;
}
export interface AgentDefinition {
metadata: AgentMetadata;
persona: AgentPersona;
menu: AgentMenuItem[];
prompts: Record<string, string>;
sidecars?: string[];
}
// Workflow Types
export interface WorkflowMetadata {
id: string;
name: string;
description: string;
module: string;
phase: WorkflowPhase;
estimatedTime?: string;
complexity?: number[];
}
export type WorkflowPhase =
| 'analysis'
| 'planning'
| 'solutioning'
| 'implementation';
export interface WorkflowStep {
id: string;
number: number;
name: string;
description: string;
instruction: string;
expectedOutput?: string;
validation?: WorkflowStepValidation;
}
export interface WorkflowStepValidation {
required?: string[];
format?: string;
minLength?: number;
}
export interface WorkflowDefinition {
metadata: WorkflowMetadata;
steps: WorkflowStep[];
outputs?: WorkflowOutput[];
}
export interface WorkflowOutput {
name: string;
type: ArtifactType;
template?: string;
}
// Module Types
export interface ModuleDefinition {
id: string;
name: string;
description: string;
version?: string;
agents: string[];
workflows: string[];
dependencies?: string[];
}
// Artifact Types
export type ArtifactType =
| 'prd'
| 'architecture'
| 'epic'
| 'story'
| 'spec'
| 'diagram'
| 'tech-spec'
| 'test-plan'
| 'retrospective'
| 'custom';
export interface Artifact {
id: string;
projectId: string;
type: ArtifactType;
name: string;
content: string;
version: number;
metadata?: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
// Project Types
export type ComplexityLevel = 0 | 1 | 2 | 3 | 4;
export type ProjectStatus =
| 'draft'
| 'analysis'
| 'planning'
| 'solutioning'
| 'implementation'
| 'completed'
| 'archived';
export interface Project {
id: string;
name: string;
description?: string;
complexityLevel: ComplexityLevel;
selectedModules: string[];
status: ProjectStatus;
config?: ProjectConfig;
createdAt: Date;
updatedAt: Date;
}
export interface ProjectConfig {
communicationLanguage?: string;
outputFolder?: string;
userSkillLevel?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
customSettings?: Record<string, unknown>;
}
// Conversation Types
export interface Message {
id: string;
role: 'user' | 'agent' | 'system';
content: string;
agentId?: string;
metadata?: MessageMetadata;
timestamp: Date;
}
export interface MessageMetadata {
workflowStep?: string;
action?: string;
artifacts?: string[];
suggestedActions?: SuggestedAction[];
context?: Record<string, unknown>;
}
export interface SuggestedAction {
label: string;
action: string;
primary?: boolean;
disabled?: boolean;
}
export interface Conversation {
id: string;
projectId: string;
agentId: string;
messages: Message[];
context: ConversationContext;
createdAt: Date;
updatedAt: Date;
}
export interface ConversationContext {
workflowId?: string;
workflowStep?: number;
artifacts?: string[];
variables?: Record<string, unknown>;
}
// Workflow Instance Types
export type WorkflowInstanceStatus =
| 'pending'
| 'active'
| 'paused'
| 'completed'
| 'failed'
| 'cancelled';
export interface WorkflowInstance {
id: string;
projectId: string;
workflowId: string;
currentStep: number;
totalSteps: number;
status: WorkflowInstanceStatus;
stepOutputs: Record<string, unknown>;
conversationId?: string;
startedAt: Date;
completedAt?: Date;
error?: string;
}
// User Types
export interface User {
id: string;
email: string;
name: string;
avatar?: string;
preferences: UserPreferences;
createdAt: Date;
}
export interface UserPreferences {
theme: 'light' | 'dark' | 'system';
language: string;
communicationStyle: 'technical' | 'simple' | 'balanced';
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'expert';
notifications?: NotificationPreferences;
}
export interface NotificationPreferences {
email: boolean;
push: boolean;
workflowUpdates: boolean;
agentMessages: boolean;
}
// API Types
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
meta?: ApiMeta;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
export interface ApiMeta {
page?: number;
pageSize?: number;
total?: number;
hasMore?: boolean;
}
// Event Types
export type WebSocketEventType =
| 'agent:message'
| 'agent:typing'
| 'workflow:progress'
| 'workflow:completed'
| 'artifact:created'
| 'artifact:updated'
| 'project:updated'
| 'error';
export interface WebSocketEvent<T = unknown> {
type: WebSocketEventType;
payload: T;
timestamp: string;
}

View File

@ -0,0 +1,158 @@
import { readFile, readdir } from 'fs/promises';
import { join } from 'path';
import { parseYaml, extractFrontmatter } from './yaml-parser.js';
import type { AgentDefinition, AgentMetadata, AgentPersona, AgentMenuItem } from '../types/index.js';
interface RawAgentData {
agent?: {
metadata?: Partial<AgentMetadata>;
persona?: Partial<AgentPersona> & { communication_style?: string };
menu?: Array<{
trigger: string;
exec?: string;
description?: string;
requires_context?: string[];
}>;
prompts?: Record<string, string>;
};
metadata?: Partial<AgentMetadata>;
persona?: Partial<AgentPersona> & { communication_style?: string };
menu?: Array<{
trigger: string;
exec?: string;
description?: string;
requires_context?: string[];
}>;
prompts?: Record<string, string>;
}
/**
* Load agent definition from file
*/
export async function loadAgent(
filePath: string,
moduleName: string
): Promise<AgentDefinition | null> {
try {
const content = await readFile(filePath, 'utf-8');
return parseAgentContent(content, moduleName, filePath);
} catch (error) {
console.warn(`Failed to load agent from ${filePath}:`, error);
return null;
}
}
/**
* Parse agent content (YAML or Markdown with frontmatter)
*/
export function parseAgentContent(
content: string,
moduleName: string,
filePath: string
): AgentDefinition | null {
try {
let data: RawAgentData;
// Check if it's markdown with frontmatter
if (content.startsWith('---')) {
const { frontmatter } = extractFrontmatter<RawAgentData>(content);
if (!frontmatter) return null;
data = frontmatter;
} else {
data = parseYaml<RawAgentData>(content);
}
// Extract agent data (could be nested under 'agent' key)
const agentData = data.agent || data;
// Build agent ID from file path
const fileName = filePath.split('/').pop() || '';
const agentId = agentData.metadata?.id ||
`${moduleName}/${fileName.replace('.agent.yaml', '').replace('.agent.md', '')}`;
const definition: AgentDefinition = {
metadata: {
id: agentId,
name: agentData.metadata?.name || 'Agent',
title: agentData.metadata?.title || 'AI Agent',
icon: agentData.metadata?.icon,
module: moduleName,
version: agentData.metadata?.version,
},
persona: {
role: agentData.persona?.role || 'AI Assistant',
identity: agentData.persona?.identity,
communicationStyle:
agentData.persona?.communicationStyle ||
agentData.persona?.communication_style ||
'professional',
principles: agentData.persona?.principles,
expertise: agentData.persona?.expertise,
},
menu: (agentData.menu || []).map((item): AgentMenuItem => ({
trigger: item.trigger,
exec: item.exec,
description: item.description || item.trigger,
requiresContext: item.requires_context,
})),
prompts: agentData.prompts || {},
};
return definition;
} catch (error) {
console.warn('Failed to parse agent content:', error);
return null;
}
}
/**
* Load all agents from a module directory
*/
export async function loadAgentsFromModule(
modulePath: string,
moduleName: string
): Promise<AgentDefinition[]> {
const agents: AgentDefinition[] = [];
const agentsPath = join(modulePath, 'agents');
try {
const files = await readdir(agentsPath);
for (const file of files) {
if (file.endsWith('.agent.yaml') || file.endsWith('.agent.md')) {
const agent = await loadAgent(join(agentsPath, file), moduleName);
if (agent) {
agents.push(agent);
}
}
}
} catch {
// agents directory doesn't exist
}
return agents;
}
/**
* Load all agents from BMAD root
*/
export async function loadAllAgents(bmadRoot: string): Promise<AgentDefinition[]> {
const agents: AgentDefinition[] = [];
const modulesPath = join(bmadRoot, 'src/modules');
try {
const modules = await readdir(modulesPath);
for (const moduleName of modules) {
const moduleAgents = await loadAgentsFromModule(
join(modulesPath, moduleName),
moduleName
);
agents.push(...moduleAgents);
}
} catch (error) {
console.warn('Failed to load agents:', error);
}
return agents;
}

View File

@ -0,0 +1,216 @@
import { readFile, readdir, stat } from 'fs/promises';
import { join } from 'path';
import { parseYaml, extractFrontmatter } from './yaml-parser.js';
import type { WorkflowDefinition, WorkflowMetadata, WorkflowStep, WorkflowPhase } from '../types/index.js';
interface RawWorkflowData {
name?: string;
description?: string;
phase?: string;
estimatedTime?: string;
complexity?: number[];
}
/**
* Load workflow definition from directory
*/
export async function loadWorkflow(
workflowPath: string,
moduleName: string
): Promise<WorkflowDefinition | null> {
try {
// Try to load workflow.yaml or workflow.md
let workflowConfig: RawWorkflowData | null = null;
const workflowYamlPath = join(workflowPath, 'workflow.yaml');
const workflowMdPath = join(workflowPath, 'workflow.md');
try {
const yamlContent = await readFile(workflowYamlPath, 'utf-8');
workflowConfig = parseYaml<RawWorkflowData>(yamlContent);
} catch {
try {
const mdContent = await readFile(workflowMdPath, 'utf-8');
const { frontmatter } = extractFrontmatter<RawWorkflowData>(mdContent);
workflowConfig = frontmatter;
} catch {
// No workflow config found
}
}
// Load steps
const steps = await loadWorkflowSteps(workflowPath);
if (steps.length === 0 && !workflowConfig) {
return null;
}
// Build workflow ID from path
const workflowName = workflowPath.split('/').pop() || 'workflow';
const workflowId = `${moduleName}/${workflowName}`;
const definition: WorkflowDefinition = {
metadata: {
id: workflowId,
name: workflowConfig?.name || workflowName,
description: workflowConfig?.description || '',
module: moduleName,
phase: (workflowConfig?.phase || 'planning') as WorkflowPhase,
estimatedTime: workflowConfig?.estimatedTime,
complexity: workflowConfig?.complexity,
},
steps,
};
return definition;
} catch (error) {
console.warn(`Failed to load workflow from ${workflowPath}:`, error);
return null;
}
}
/**
* Load workflow steps from directory
*/
async function loadWorkflowSteps(workflowPath: string): Promise<WorkflowStep[]> {
const steps: WorkflowStep[] = [];
try {
const files = await readdir(workflowPath);
// Find step files (step-1.md, step-2.md, etc.)
const stepFiles = files
.filter(f => /^step-\d+\.md$/.test(f))
.sort((a, b) => {
const numA = parseInt(a.match(/\d+/)?.[0] || '0');
const numB = parseInt(b.match(/\d+/)?.[0] || '0');
return numA - numB;
});
for (let i = 0; i < stepFiles.length; i++) {
const stepFile = stepFiles[i];
const stepPath = join(workflowPath, stepFile);
const content = await readFile(stepPath, 'utf-8');
const step = parseStepContent(content, i + 1, stepFile);
if (step) {
steps.push(step);
}
}
} catch {
// No step files found
}
return steps;
}
/**
* Parse step content from markdown
*/
function parseStepContent(
content: string,
stepNumber: number,
fileName: string
): WorkflowStep | null {
try {
const { frontmatter, content: markdownContent } = extractFrontmatter<{
name?: string;
description?: string;
expectedOutput?: string;
validation?: {
required?: string[];
format?: string;
minLength?: number;
};
}>(content);
// Extract title from first heading if no frontmatter name
let name = frontmatter?.name;
if (!name) {
const titleMatch = markdownContent.match(/^#\s+(.+)$/m);
name = titleMatch?.[1] || `Step ${stepNumber}`;
}
return {
id: fileName.replace('.md', ''),
number: stepNumber,
name,
description: frontmatter?.description || '',
instruction: markdownContent.trim(),
expectedOutput: frontmatter?.expectedOutput,
validation: frontmatter?.validation,
};
} catch {
return null;
}
}
/**
* Load all workflows from a module directory
*/
export async function loadWorkflowsFromModule(
modulePath: string,
moduleName: string
): Promise<WorkflowDefinition[]> {
const workflows: WorkflowDefinition[] = [];
const workflowsPath = join(modulePath, 'workflows');
try {
const items = await readdir(workflowsPath);
for (const item of items) {
const itemPath = join(workflowsPath, item);
const itemStat = await stat(itemPath);
if (itemStat.isDirectory()) {
// Check subdirectories for workflows
const subItems = await readdir(itemPath);
for (const subItem of subItems) {
const subItemPath = join(itemPath, subItem);
const subItemStat = await stat(subItemPath);
if (subItemStat.isDirectory()) {
const workflow = await loadWorkflow(subItemPath, moduleName);
if (workflow) {
workflows.push(workflow);
}
}
}
// Also check if current directory is a workflow
const workflow = await loadWorkflow(itemPath, moduleName);
if (workflow) {
workflows.push(workflow);
}
}
}
} catch {
// workflows directory doesn't exist
}
return workflows;
}
/**
* Load all workflows from BMAD root
*/
export async function loadAllWorkflows(bmadRoot: string): Promise<WorkflowDefinition[]> {
const workflows: WorkflowDefinition[] = [];
const modulesPath = join(bmadRoot, 'src/modules');
try {
const modules = await readdir(modulesPath);
for (const moduleName of modules) {
const moduleWorkflows = await loadWorkflowsFromModule(
join(modulesPath, moduleName),
moduleName
);
workflows.push(...moduleWorkflows);
}
} catch (error) {
console.warn('Failed to load workflows:', error);
}
return workflows;
}

View File

@ -0,0 +1,51 @@
import { parse, stringify } from 'yaml';
/**
* Parse YAML content to JavaScript object
*/
export function parseYaml<T = unknown>(content: string): T {
return parse(content) as T;
}
/**
* Stringify JavaScript object to YAML
*/
export function toYaml(data: unknown): string {
return stringify(data, {
indent: 2,
lineWidth: 120,
});
}
/**
* Extract frontmatter from markdown content
*/
export function extractFrontmatter<T = Record<string, unknown>>(
content: string
): { frontmatter: T | null; content: string } {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
const match = content.match(frontmatterRegex);
if (match) {
try {
const frontmatter = parseYaml<T>(match[1]);
const contentWithoutFrontmatter = content.slice(match[0].length);
return { frontmatter, content: contentWithoutFrontmatter };
} catch {
return { frontmatter: null, content };
}
}
return { frontmatter: null, content };
}
/**
* Add frontmatter to markdown content
*/
export function addFrontmatter(
content: string,
frontmatter: Record<string, unknown>
): string {
const yamlFrontmatter = toYaml(frontmatter);
return `---\n${yamlFrontmatter}---\n\n${content}`;
}

View File

@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@ -0,0 +1,40 @@
{
"name": "@bmad/ui",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
},
"./components/*": {
"import": "./src/components/*.tsx",
"types": "./src/components/*.tsx"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint src --ext .ts,.tsx",
"clean": "rm -rf dist node_modules"
},
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"tailwind-merge": "^2.1.0"
},
"devDependencies": {
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"react": "^18.2.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@ -0,0 +1,46 @@
import * as React from 'react';
import { cn } from '../utils.js';
const Avatar = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
));
Avatar.displayName = 'Avatar';
const AvatarImage = React.forwardRef<
HTMLImageElement,
React.ImgHTMLAttributes<HTMLImageElement>
>(({ className, ...props }, ref) => (
<img
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = 'AvatarImage';
const AvatarFallback = React.forwardRef<
HTMLSpanElement,
React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
<span
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
));
AvatarFallback.displayName = 'AvatarFallback';
export { Avatar, AvatarImage, AvatarFallback };

View File

@ -0,0 +1,39 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../utils.js';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default:
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
success:
'border-transparent bg-green-500 text-white hover:bg-green-500/80',
warning:
'border-transparent bg-yellow-500 text-white hover:bg-yellow-500/80',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
);
}
export { Badge, badgeVariants };

View File

@ -0,0 +1,55 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../utils.js';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
export { Button, buttonVariants };

View File

@ -0,0 +1,78 @@
import * as React from 'react';
import { cn } from '../utils.js';
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
));
Card.displayName = 'Card';
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
));
CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
));
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
));
CardContent.displayName = 'CardContent';
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@ -0,0 +1,24 @@
import * as React from 'react';
import { cn } from '../utils.js';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Input.displayName = 'Input';
export { Input };

View File

@ -0,0 +1,20 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../utils.js';
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
);
export interface LabelProps
extends React.LabelHTMLAttributes<HTMLLabelElement>,
VariantProps<typeof labelVariants> {}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
({ className, ...props }, ref) => (
<label ref={ref} className={cn(labelVariants(), className)} {...props} />
)
);
Label.displayName = 'Label';
export { Label };

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import { cn } from '../utils.js';
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
max?: number;
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
({ className, value = 0, max = 100, ...props }, ref) => {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
return (
<div
ref={ref}
className={cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
className
)}
{...props}
>
<div
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - percentage}%)` }}
/>
</div>
);
}
);
Progress.displayName = 'Progress';
export { Progress };

View File

@ -0,0 +1,15 @@
import { cn } from '../utils.js';
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn('animate-pulse rounded-md bg-muted', className)}
{...props}
/>
);
}
export { Skeleton };

View File

@ -0,0 +1,22 @@
import { cn } from '../utils.js';
import { Loader2 } from 'lucide-react';
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
size?: 'sm' | 'md' | 'lg';
}
const sizeClasses = {
sm: 'h-4 w-4',
md: 'h-6 w-6',
lg: 'h-8 w-8',
};
function Spinner({ className, size = 'md', ...props }: SpinnerProps) {
return (
<div className={cn('flex items-center justify-center', className)} {...props}>
<Loader2 className={cn('animate-spin', sizeClasses[size])} />
</div>
);
}
export { Spinner };

View File

@ -0,0 +1,23 @@
import * as React from 'react';
import { cn } from '../utils.js';
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';
export { Textarea };

View File

@ -0,0 +1,20 @@
/**
* BMAD UI Package
*
* Shared UI components for BMAD applications
*/
// Export utilities
export { cn } from './utils.js';
// Export components
export { Button, buttonVariants } from './components/button.js';
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './components/card.js';
export { Input } from './components/input.js';
export { Label } from './components/label.js';
export { Textarea } from './components/textarea.js';
export { Badge, badgeVariants } from './components/badge.js';
export { Avatar, AvatarImage, AvatarFallback } from './components/avatar.js';
export { Progress } from './components/progress.js';
export { Skeleton } from './components/skeleton.js';
export { Spinner } from './components/spinner.js';

View File

@ -0,0 +1,9 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Merge Tailwind CSS classes with clsx
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

18
bmad-web/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"noEmit": true
},
"exclude": ["node_modules"]
}

33
bmad-web/turbo.json Normal file
View File

@ -0,0 +1,33 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
},
"db:generate": {
"cache": false
},
"db:push": {
"cache": false
},
"db:studio": {
"cache": false,
"persistent": true
}
}
}

View File

@ -0,0 +1,600 @@
# BMAD-METHOD: Arquitetura para Aplicação Frontend Consumer
## Visão Geral
Este documento descreve a arquitetura para transformar o BMAD-METHOD (atualmente uma CLI) em uma aplicação web completa para consumidores finais.
---
## 1. Arquitetura Proposta
```
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND (React/Next.js) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Dashboard │ │ Agent Chat │ │ Workflow │ │
│ │ Project │ │ Interface │ │ Viewer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Artifact │ │ Team │ │ Settings & │ │
│ │ Editor │ │ Management │ │ Profile │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ REST API + WebSocket
┌─────────────────────────────────────────────────────────────────────┐
│ BACKEND (Node.js/Express) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ API REST │ │ WebSocket │ │ Auth │ │
│ │ Gateway │ │ Server │ │ Service │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Agent │ │ Workflow │ │ Project │ │
│ │ Manager │ │ Engine │ │ Service │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ BMAD-METHOD CORE │
├─────────────────────────────────────────────────────────────────────┤
│ Agents (21+) │ Workflows (50+) │ Modules │ Templates │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ DATA LAYER │
├─────────────────────────────────────────────────────────────────────┤
│ PostgreSQL (Projects, Users) │ Redis (Sessions, Cache) │
│ S3/MinIO (Artifacts, Files) │ MongoDB (Conversations, Logs) │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. Estrutura de Diretórios Proposta
```
bmad-web/
├── apps/
│ ├── web/ # Frontend Next.js
│ │ ├── src/
│ │ │ ├── app/ # App Router (Next.js 14+)
│ │ │ │ ├── (auth)/ # Rotas de autenticação
│ │ │ │ ├── (dashboard)/ # Dashboard principal
│ │ │ │ ├── projects/ # Gestão de projetos
│ │ │ │ ├── agents/ # Interface dos agentes
│ │ │ │ └── workflows/ # Visualização de workflows
│ │ │ ├── components/
│ │ │ │ ├── ui/ # Componentes base (shadcn/ui)
│ │ │ │ ├── agents/ # Componentes de agentes
│ │ │ │ ├── chat/ # Interface de chat
│ │ │ │ ├── workflow/ # Visualizadores de workflow
│ │ │ │ └── editor/ # Editor de artefatos
│ │ │ ├── hooks/ # React hooks customizados
│ │ │ ├── lib/ # Utilitários e helpers
│ │ │ ├── stores/ # Estado global (Zustand)
│ │ │ └── types/ # TypeScript types
│ │ └── package.json
│ │
│ └── api/ # Backend API
│ ├── src/
│ │ ├── routes/ # Express routes
│ │ ├── services/ # Business logic
│ │ ├── middleware/ # Auth, logging, etc.
│ │ ├── websocket/ # WebSocket handlers
│ │ ├── bmad/ # BMAD Core integration
│ │ └── database/ # Database models
│ └── package.json
├── packages/
│ ├── bmad-core/ # Core BMAD refatorado como lib
│ │ ├── agents/ # Agent definitions
│ │ ├── workflows/ # Workflow engine
│ │ ├── modules/ # Module system
│ │ └── types/ # Shared types
│ │
│ ├── ui/ # Componentes compartilhados
│ └── config/ # Configurações compartilhadas
├── docker/ # Docker configs
├── turbo.json # Turborepo config
└── package.json # Root package
```
---
## 3. Stack Tecnológico Recomendado
### Frontend
| Tecnologia | Propósito | Justificativa |
|------------|-----------|---------------|
| **Next.js 14** | Framework React | SSR, App Router, excelente DX |
| **TypeScript** | Linguagem | Type safety, melhor manutenção |
| **Tailwind CSS** | Estilos | Rápido, consistente, customizável |
| **shadcn/ui** | Componentes | Acessível, bonito, não-bloated |
| **Zustand** | Estado global | Simples, performático |
| **TanStack Query** | Data fetching | Cache, refetch, mutations |
| **Socket.io Client** | Real-time | Comunicação com agentes |
| **Monaco Editor** | Code editor | Para edição de artefatos |
| **Framer Motion** | Animações | UX fluida nas transições |
### Backend
| Tecnologia | Propósito | Justificativa |
|------------|-----------|---------------|
| **Node.js 20+** | Runtime | Já usado pelo BMAD |
| **Express/Fastify** | HTTP Server | Maduro, flexível |
| **Socket.io** | WebSocket | Real-time bi-direcional |
| **Prisma** | ORM | Type-safe, migrations |
| **Redis** | Cache/Sessions | Performance |
| **PostgreSQL** | Database | Dados estruturados |
| **MongoDB** | Document store | Conversas, logs |
| **Bull/BullMQ** | Job Queue | Workflows assíncronos |
### Infraestrutura
| Tecnologia | Propósito |
|------------|-----------|
| **Docker** | Containerização |
| **Turborepo** | Monorepo management |
| **Vercel/Railway** | Deploy frontend |
| **Fly.io/Render** | Deploy backend |
---
## 4. Funcionalidades Principais
### 4.1 Dashboard do Projeto
- Visão geral do projeto atual
- Status dos workflows ativos
- Métricas de progresso
- Artefatos gerados
- Atividade recente
### 4.2 Interface de Chat com Agentes
- Chat em tempo real com agentes BMAD
- Seleção de agente por contexto
- Histórico de conversas
- Sugestões de ações
- Execução de comandos via chat
### 4.3 Visualizador de Workflows
- Visualização gráfica do workflow atual
- Progresso step-by-step
- Navegação entre passos
- Outputs de cada passo
- Possibilidade de retroceder/avançar
### 4.4 Editor de Artefatos
- Editor markdown/código rico
- Preview em tempo real
- Versionamento de artefatos
- Exportação (PDF, MD, DOCX)
- Templates pré-definidos
### 4.5 Gestão de Projetos
- Criar novos projetos
- Configurar parâmetros BMAD
- Selecionar módulos e agentes
- Definir nível de complexidade
- Gestão de equipe/colaboradores
---
## 5. Fluxo de Usuário Principal
```
┌─────────────────────────────────────────────────────────────────────┐
│ JORNADA DO USUÁRIO │
└─────────────────────────────────────────────────────────────────────┘
1. ONBOARDING
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Signup │───▶│ Setup │───▶│ Create │───▶│ Choose │
│ │ │ Profile│ │ Project│ │ Track │
└────────┘ └────────┘ └────────┘ └────────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Quick │ │ BMAD │ │Enterprise│
│ Flow │ │ Method │ │ │
└──────────┘ └──────────┘ └──────────┘
2. WORKFLOW EXECUTION
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Select │───▶│ Chat │───▶│Complete│───▶│ Save │
│ Agent │ │ Work │ │ Step │ │Artifact│
└────────┘ └────────┘ └────────┘ └────────┘
│ ▲ │
│ └────────────────────────────┘
│ (repeat)
┌────────────────────────────────────────────────────┐
│ WORKFLOW COMPLETION │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Review │ │ Export │ │ Next │ │
│ │ Outputs │ │ Artifacts│ │ Phase │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────────┘
```
---
## 6. API Design
### 6.1 Endpoints Principais
```typescript
// Autenticação
POST /api/auth/register
POST /api/auth/login
POST /api/auth/logout
GET /api/auth/me
// Projetos
GET /api/projects
POST /api/projects
GET /api/projects/:id
PUT /api/projects/:id
DELETE /api/projects/:id
GET /api/projects/:id/artifacts
GET /api/projects/:id/workflows
// Agentes
GET /api/agents # Lista todos os agentes disponíveis
GET /api/agents/:id # Detalhes de um agente
POST /api/agents/:id/chat # Iniciar conversa com agente
// Workflows
GET /api/workflows # Lista workflows disponíveis
GET /api/workflows/:id # Detalhes de workflow
POST /api/workflows/:id/start # Iniciar workflow
GET /api/workflows/:id/status # Status atual
POST /api/workflows/:id/step/next # Avançar para próximo passo
POST /api/workflows/:id/step/complete # Completar passo atual
// Artefatos
GET /api/artifacts/:id
PUT /api/artifacts/:id
POST /api/artifacts/:id/export
GET /api/artifacts/:id/versions
// WebSocket Events
ws://api/socket
- agent:message # Mensagem do agente
- agent:typing # Agente está digitando
- workflow:progress # Progresso do workflow
- artifact:updated # Artefato atualizado
```
### 6.2 Schema de Dados
```typescript
// User
interface User {
id: string;
email: string;
name: string;
avatar?: string;
preferences: UserPreferences;
createdAt: Date;
}
// Project
interface Project {
id: string;
name: string;
description?: string;
complexityLevel: 0 | 1 | 2 | 3 | 4;
selectedModules: string[];
activeWorkflow?: string;
artifacts: Artifact[];
createdAt: Date;
updatedAt: Date;
userId: string;
}
// Conversation
interface Conversation {
id: string;
projectId: string;
agentId: string;
messages: Message[];
context: Record<string, any>;
createdAt: Date;
}
// Message
interface Message {
id: string;
role: 'user' | 'agent' | 'system';
content: string;
metadata?: {
workflowStep?: string;
action?: string;
artifacts?: string[];
};
timestamp: Date;
}
// Workflow Instance
interface WorkflowInstance {
id: string;
projectId: string;
workflowId: string;
currentStep: number;
totalSteps: number;
status: 'active' | 'paused' | 'completed' | 'failed';
stepOutputs: Record<string, any>;
startedAt: Date;
completedAt?: Date;
}
// Artifact
interface Artifact {
id: string;
projectId: string;
type: 'prd' | 'architecture' | 'epic' | 'story' | 'spec' | 'diagram';
name: string;
content: string;
version: number;
createdAt: Date;
updatedAt: Date;
}
```
---
## 7. Componentes de Interface Chave
### 7.1 Agent Chat Interface
```tsx
// components/chat/AgentChat.tsx
interface AgentChatProps {
projectId: string;
agentId: string;
onArtifactCreated?: (artifact: Artifact) => void;
}
// Features:
// - Avatar e nome do agente
// - Input de mensagem com markdown support
// - Botões de ação rápida (baseados no menu do agente)
// - Exibição de outputs estruturados
// - Indicador de "typing"
// - Scroll automático para última mensagem
```
### 7.2 Workflow Stepper
```tsx
// components/workflow/WorkflowStepper.tsx
interface WorkflowStepperProps {
workflowInstance: WorkflowInstance;
onStepComplete: (stepId: string, output: any) => void;
}
// Features:
// - Visualização vertical/horizontal dos passos
// - Indicador de passo atual
// - Preview do conteúdo de cada passo
// - Botões de navegação
// - Exibição de outputs anteriores
```
### 7.3 Artifact Editor
```tsx
// components/editor/ArtifactEditor.tsx
interface ArtifactEditorProps {
artifact: Artifact;
onSave: (content: string) => void;
readonly?: boolean;
}
// Features:
// - Monaco editor para código/markdown
// - Preview lado a lado
// - Autosave
// - Toolbar com formatação
// - Export button
```
---
## 8. Integração com BMAD Core
### 8.1 Adapter Pattern
```typescript
// packages/bmad-core/src/adapter.ts
import { AgentDefinition, WorkflowDefinition } from './types';
export class BMADAdapter {
private agents: Map<string, AgentDefinition>;
private workflows: Map<string, WorkflowDefinition>;
constructor() {
this.loadAgents();
this.loadWorkflows();
}
// Carrega definições de agentes dos arquivos YAML
async loadAgents(): Promise<void> {
// Parse YAML files from src/modules/*/agents/
}
// Carrega definições de workflows
async loadWorkflows(): Promise<void> {
// Parse workflow directories from src/modules/*/workflows/
}
// Obtém menu de ações disponíveis para um agente
getAgentActions(agentId: string): AgentAction[] {
const agent = this.agents.get(agentId);
return agent?.menu || [];
}
// Processa mensagem do usuário com contexto do agente
async processMessage(
agentId: string,
message: string,
context: ConversationContext
): Promise<AgentResponse> {
// Usa persona, prompts e menu do agente
// para gerar resposta contextualizada
}
// Inicia um workflow
async startWorkflow(
workflowId: string,
projectContext: ProjectContext
): Promise<WorkflowInstance> {
// Carrega steps do workflow
// Inicializa estado
}
// Avança para próximo step
async advanceWorkflow(
instanceId: string,
stepOutput: any
): Promise<WorkflowStep | null> {
// Valida output do step atual
// Carrega próximo step
}
}
```
### 8.2 Real-time Communication
```typescript
// apps/api/src/websocket/handlers.ts
import { Server } from 'socket.io';
import { BMADAdapter } from '@bmad/core';
export function setupWebSocketHandlers(io: Server, bmad: BMADAdapter) {
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Usuário envia mensagem para agente
socket.on('agent:message', async (data) => {
const { agentId, message, conversationId } = data;
// Indica que agente está "pensando"
socket.emit('agent:typing', { agentId, isTyping: true });
// Processa mensagem
const response = await bmad.processMessage(
agentId,
message,
await getConversationContext(conversationId)
);
// Envia resposta
socket.emit('agent:message', {
agentId,
message: response.content,
metadata: response.metadata,
});
socket.emit('agent:typing', { agentId, isTyping: false });
});
// Workflow events
socket.on('workflow:advance', async (data) => {
const { instanceId, stepOutput } = data;
const nextStep = await bmad.advanceWorkflow(instanceId, stepOutput);
socket.emit('workflow:progress', {
instanceId,
currentStep: nextStep?.stepNumber,
completed: nextStep === null,
});
});
});
}
```
---
## 9. Considerações de UX para Consumidor Final
### 9.1 Simplificação da Complexidade
- **Wizard de Onboarding**: Guia passo-a-passo para criar primeiro projeto
- **Templates Pré-configurados**: "Start a SaaS", "Build a Mobile App", etc.
- **Auto-seleção de Agentes**: Sistema sugere agentes baseado no contexto
- **Progress Gamification**: Badges, progress bars, celebrações
### 9.2 Linguagem Acessível
- Substituir jargões técnicos por termos simples
- Tooltips explicativos em toda interface
- Modo "Explain Like I'm 5" opcional
- Suporte multi-idioma (pt-BR prioritário)
### 9.3 Fluxo Guiado
- Nunca deixar usuário "perdido"
- Sempre ter próxima ação sugerida
- Chat de ajuda sempre disponível
- Tutorials interativos integrados
### 9.4 Responsividade
- Mobile-first design
- PWA para acesso offline
- Push notifications para progresso
---
## 10. Roadmap de Implementação
### Fase 1: Foundation (MVP)
- [ ] Setup monorepo com Turborepo
- [ ] Refatorar BMAD core como package importável
- [ ] Backend básico com autenticação
- [ ] Frontend com dashboard e chat básico
- [ ] 1 workflow completo funcionando (Quick-Spec)
### Fase 2: Core Features
- [ ] Todos os agentes BMM integrados
- [ ] Workflow engine completo
- [ ] Editor de artefatos
- [ ] Sistema de projetos
- [ ] Export de artefatos
### Fase 3: Polish
- [ ] UX refinada com animações
- [ ] Onboarding wizard
- [ ] Templates de projeto
- [ ] Temas e personalização
- [ ] Mobile responsivo
### Fase 4: Scale
- [ ] Colaboração em tempo real
- [ ] Integrações (GitHub, Jira, etc.)
- [ ] API pública
- [ ] Módulos custom via interface
- [ ] Analytics e métricas
---
## 11. Considerações Finais
Transformar o BMAD-METHOD em uma aplicação web consumer-facing é um projeto significativo, mas muito viável. O core do BMAD já está bem estruturado com:
- Definições de agentes em YAML (fácil de parsear)
- Workflows modulares (fácil de expor como steps)
- Sistema de módulos (extensibilidade built-in)
As principais adaptações necessárias são:
1. **Camada de persistência** para projetos e conversas
2. **API HTTP/WebSocket** para expor funcionalidades
3. **Interface visual** para interagir com agentes
4. **Simplificação de UX** para usuários não-técnicos
O investimento vale a pena porque democratiza acesso a metodologias de desenvolvimento estruturadas, permitindo que pessoas sem background técnico profundo consigam planejar e executar projetos de software de forma guiada.