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:
parent
c18904d674
commit
1ac7a69a46
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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=
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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%);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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*/, '');
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -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 } };
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"noEmit": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
Loading…
Reference in New Issue