rook-cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/package.json +7 -3
- package/src/app.js +11 -4
- package/src/commands/AddCommand.js +49 -11
- package/src/commands/GenerateCommand.js +34 -5
- package/src/mcp/server.js +387 -0
- package/src/utils/logger.js +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,53 @@ node bin/rook.js config
|
|
|
40
40
|
node bin/rook.js add
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
## Modo Headless (Automação / CI)
|
|
44
|
+
|
|
45
|
+
Para rodar os comandos sem interação (útil para scripts ou MCP), utilize as flags adicionais:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Adicionar componente/kit diretamente e sobrescrever conflitos
|
|
49
|
+
rook add --type componente --name whatsapp-float --force
|
|
50
|
+
|
|
51
|
+
# Gerar scaffold de arquivos Liquid diretamente
|
|
52
|
+
rook generate hero-banner --type section --yes
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Integração com IA (MCP Server)
|
|
56
|
+
|
|
57
|
+
O Rook CLI expõe todas as suas funcionalidades como ferramentas (*tools*) através do protocolo **Model Context Protocol (MCP)**. Isso permite que Inteligências Artificiais como Claude Desktop e Cursor instalem componentes e gerem arquivos no seu projeto automaticamente.
|
|
58
|
+
|
|
59
|
+
Existem duas formas de configurar o servidor MCP dependendo de como você instalou o pacote:
|
|
60
|
+
|
|
61
|
+
### Opção 1: Usando `npx` (Recomendada)
|
|
62
|
+
Ideal se o pacote for instalado remotamente via NPM. O npx garante que a IA sempre fará o bypass do PATH sem problemas:
|
|
63
|
+
|
|
64
|
+
**Em `claude_desktop_config.json` ou `.cursor/mcp.json`:**
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"rook-cli": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "--package", "rook-cli", "rook-mcp"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Opção 2: Instalando globalmente (`npm install -g rook-cli`)
|
|
77
|
+
Se você instalou o CLI como pacote global no seu sistema, o comando `rook-mcp` estará disponível nativamente.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"rook-cli": {
|
|
83
|
+
"command": "rook-mcp",
|
|
84
|
+
"args": []
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
43
90
|
## Estrutura do Projeto
|
|
44
91
|
|
|
45
92
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rook-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "CLI para instalar componentes Shopify",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"bin": {
|
|
11
|
-
"rook": "bin/rook.js"
|
|
11
|
+
"rook": "bin/rook.js",
|
|
12
|
+
"rook-mcp": "src/mcp/server.js"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"bin",
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"start": "node bin/rook.js",
|
|
20
|
+
"mcp": "node src/mcp/server.js",
|
|
19
21
|
"test": "echo \"No tests yet\" && exit 0"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
@@ -28,10 +30,12 @@
|
|
|
28
30
|
"license": "ISC",
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"@inquirer/prompts": "^8.3.0",
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
31
34
|
"commander": "^14.0.3",
|
|
32
35
|
"dotenv": "^17.3.1",
|
|
33
36
|
"fs-extra": "^11.3.3",
|
|
34
37
|
"picocolors": "^1.1.1",
|
|
35
|
-
"tiged": "^2.12.7"
|
|
38
|
+
"tiged": "^2.12.7",
|
|
39
|
+
"zod": "^4.3.6"
|
|
36
40
|
}
|
|
37
41
|
}
|
package/src/app.js
CHANGED
|
@@ -74,12 +74,16 @@ export class App {
|
|
|
74
74
|
.version('1.0.1');
|
|
75
75
|
|
|
76
76
|
// Comando: rook add
|
|
77
|
+
// Flags de headless mode: --type, --name, --force
|
|
77
78
|
this.programa
|
|
78
79
|
.command('add')
|
|
79
80
|
.description('Adiciona kits ou componentes ao tema Shopify atual')
|
|
80
|
-
.
|
|
81
|
+
.option('--type <tipo>', 'Tipo de instalação: "kit" ou "componente"')
|
|
82
|
+
.option('--name <nome>', 'Nome do kit ou componente a instalar')
|
|
83
|
+
.option('--force', 'Sobrescreve arquivos existentes sem perguntar')
|
|
84
|
+
.action(async (opcoes) => {
|
|
81
85
|
this.logger.banner();
|
|
82
|
-
await this.addCommand.executar();
|
|
86
|
+
await this.addCommand.executar(opcoes);
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
// Comando: rook config
|
|
@@ -92,13 +96,16 @@ export class App {
|
|
|
92
96
|
});
|
|
93
97
|
|
|
94
98
|
// Comando: rook generate [nome-do-componente]
|
|
99
|
+
// Flags de headless mode: --type, --yes
|
|
95
100
|
this.programa
|
|
96
101
|
.command('generate [nome]')
|
|
97
102
|
.alias('g')
|
|
98
103
|
.description('Gera scaffold de componentes Shopify (sections, blocks, snippets, assets)')
|
|
99
|
-
.
|
|
104
|
+
.option('--type <tipo>', 'Tipo de componente: section, block, snippet, controller')
|
|
105
|
+
.option('--yes', 'Pula confirmações e sobrescreve arquivos existentes')
|
|
106
|
+
.action(async (nome, opcoes) => {
|
|
100
107
|
this.logger.banner();
|
|
101
|
-
await this.generateCommand.executar(nome);
|
|
108
|
+
await this.generateCommand.executar(nome, opcoes);
|
|
102
109
|
});
|
|
103
110
|
|
|
104
111
|
// Comando padrão (sem argumentos) — abre o menu interativo
|
|
@@ -34,18 +34,32 @@ export class AddCommand {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Executa o fluxo principal do comando "add".
|
|
37
|
+
*
|
|
38
|
+
* Suporta dois modos:
|
|
39
|
+
* - Interativo (padrão): exibe menus para o usuário
|
|
40
|
+
* - Headless (com flags): executa direto, sem prompts (para uso com MCP/IA)
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} [opcoes={}] - Opções passadas via flags do Commander
|
|
43
|
+
* @param {string} [opcoes.type] - Tipo de instalação ("kit" ou "componente")
|
|
44
|
+
* @param {string} [opcoes.name] - Nome do kit ou componente
|
|
45
|
+
* @param {boolean} [opcoes.force] - Sobrescreve sem perguntar
|
|
37
46
|
* @returns {Promise<void>}
|
|
38
47
|
*/
|
|
39
|
-
async executar() {
|
|
48
|
+
async executar(opcoes = {}) {
|
|
40
49
|
try {
|
|
41
|
-
//
|
|
42
|
-
|
|
50
|
+
// Modo headless: se --force foi passado, configura o ConflictResolver
|
|
51
|
+
if (opcoes.force) {
|
|
52
|
+
this.fileMapper.conflictResolver?.definirDecisaoGlobal?.(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 1. Determina o tipo (flag ou menu interativo)
|
|
56
|
+
const tipoInstalacao = opcoes.type || await this.promptUI.menuPrincipal();
|
|
43
57
|
|
|
44
58
|
// 2. Delega para o fluxo correto
|
|
45
59
|
if (tipoInstalacao === 'kit') {
|
|
46
|
-
await this._instalarKit();
|
|
60
|
+
await this._instalarKit(opcoes.name);
|
|
47
61
|
} else {
|
|
48
|
-
await this._instalarComponentes();
|
|
62
|
+
await this._instalarComponentes(opcoes.name);
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
} catch (erro) {
|
|
@@ -63,13 +77,14 @@ export class AddCommand {
|
|
|
63
77
|
*
|
|
64
78
|
* Segue o padrão descrito no PRD (Seção 5.3):
|
|
65
79
|
* 1. Lista kits disponíveis (com dados do kit.json)
|
|
66
|
-
* 2. Usuário seleciona o kit
|
|
80
|
+
* 2. Usuário seleciona o kit (ou recebe via flag headless)
|
|
67
81
|
* 3. Instala cada componente referenciado no manifesto
|
|
68
82
|
* 4. Instala os arquivos locais exclusivos do kit
|
|
69
83
|
*
|
|
84
|
+
* @param {string} [nomeKit] - Nome do kit (modo headless). Se omitido, exibe menu.
|
|
70
85
|
* @private
|
|
71
86
|
*/
|
|
72
|
-
async _instalarKit() {
|
|
87
|
+
async _instalarKit(nomeKit) {
|
|
73
88
|
// 1. Lista kits disponíveis (já enriquecidos com dados do kit.json)
|
|
74
89
|
const kits = await this.githubService.listarKits();
|
|
75
90
|
|
|
@@ -78,8 +93,17 @@ export class AddCommand {
|
|
|
78
93
|
return;
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
// 2.
|
|
82
|
-
|
|
96
|
+
// 2. Seleciona o kit (headless ou interativo)
|
|
97
|
+
let kitSelecionado;
|
|
98
|
+
if (nomeKit) {
|
|
99
|
+
kitSelecionado = kits.find(k => k.slug === nomeKit || k.nome === nomeKit);
|
|
100
|
+
if (!kitSelecionado) {
|
|
101
|
+
this.logger.erro(`Kit "${nomeKit}" não encontrado.`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
kitSelecionado = await this.promptUI.selecionarKit(kits);
|
|
106
|
+
}
|
|
83
107
|
|
|
84
108
|
this.logger.destaque(`\n📥 Instalando kit: ${kitSelecionado.nome}\n`);
|
|
85
109
|
|
|
@@ -132,12 +156,26 @@ export class AddCommand {
|
|
|
132
156
|
|
|
133
157
|
/**
|
|
134
158
|
* Fluxo de instalação de componentes individuais.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} [nomeComponente] - Nome do componente (modo headless). Se omitido, exibe menu.
|
|
135
161
|
* @private
|
|
136
162
|
*/
|
|
137
|
-
async _instalarComponentes() {
|
|
163
|
+
async _instalarComponentes(nomeComponente) {
|
|
138
164
|
// Lista componentes disponíveis
|
|
139
165
|
const componentes = await this.githubService.listarComponentes();
|
|
140
|
-
|
|
166
|
+
|
|
167
|
+
let selecionados;
|
|
168
|
+
if (nomeComponente) {
|
|
169
|
+
// Modo headless: busca pelo nome diretamente
|
|
170
|
+
const encontrado = componentes.find(c => c.nome === nomeComponente);
|
|
171
|
+
if (!encontrado) {
|
|
172
|
+
this.logger.erro(`Componente "${nomeComponente}" não encontrado no repositório.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
selecionados = [encontrado];
|
|
176
|
+
} else {
|
|
177
|
+
selecionados = await this.promptUI.selecionarComponentes(componentes);
|
|
178
|
+
}
|
|
141
179
|
|
|
142
180
|
this.logger.destaque(`\n📥 Instalando ${selecionados.length} componente(s)...\n`);
|
|
143
181
|
|
|
@@ -35,11 +35,23 @@ export class GenerateCommand {
|
|
|
35
35
|
/**
|
|
36
36
|
* Executa o fluxo principal do comando "generate".
|
|
37
37
|
*
|
|
38
|
+
* Suporta dois modos:
|
|
39
|
+
* - Interativo (padrão): pergunta nome e tipos ao usuário
|
|
40
|
+
* - Headless (com flags): executa direto, sem prompts (para uso com MCP/IA)
|
|
41
|
+
*
|
|
38
42
|
* @param {string} [nomeComponente] - Nome passado como argumento CLI (opcional)
|
|
43
|
+
* @param {Object} [opcoes={}] - Opções passadas via flags do Commander
|
|
44
|
+
* @param {string} [opcoes.type] - Tipo de componente (pula multi-select)
|
|
45
|
+
* @param {boolean} [opcoes.yes] - Sobrescreve arquivos sem perguntar
|
|
39
46
|
* @returns {Promise<void>}
|
|
40
47
|
*/
|
|
41
|
-
async executar(nomeComponente) {
|
|
48
|
+
async executar(nomeComponente, opcoes = {}) {
|
|
42
49
|
try {
|
|
50
|
+
// Modo headless: se --yes foi passado, sobrescreve tudo
|
|
51
|
+
if (opcoes.yes) {
|
|
52
|
+
this.conflictResolver.definirDecisaoGlobal(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
// 1. Se o nome não veio como argumento, pergunta ao usuário
|
|
44
56
|
const nome = nomeComponente || await this.promptUI.perguntarNomeComponente();
|
|
45
57
|
|
|
@@ -53,8 +65,23 @@ export class GenerateCommand {
|
|
|
53
65
|
|
|
54
66
|
this.logger.destaque(`\n🔧 Gerando componente: ${names.PascalName} (${names.kebabName})\n`);
|
|
55
67
|
|
|
56
|
-
// 3.
|
|
57
|
-
|
|
68
|
+
// 3. Determina os tipos (flag headless ou multi-select interativo)
|
|
69
|
+
let tiposSelecionados;
|
|
70
|
+
if (opcoes.type) {
|
|
71
|
+
// Headless: aceita tipo único ou múltiplos separados por vírgula
|
|
72
|
+
tiposSelecionados = opcoes.type.split(',').map(t => t.trim());
|
|
73
|
+
|
|
74
|
+
// Valida tipos
|
|
75
|
+
const tiposValidos = this.scaffoldService.getAvailableTypes();
|
|
76
|
+
for (const tipo of tiposSelecionados) {
|
|
77
|
+
if (!tiposValidos.includes(tipo)) {
|
|
78
|
+
this.logger.erro(`Tipo inválido: "${tipo}". Tipos válidos: ${tiposValidos.join(', ')}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
tiposSelecionados = await this.promptUI.selecionarTiposGerador();
|
|
84
|
+
}
|
|
58
85
|
|
|
59
86
|
if (tiposSelecionados.length === 0) {
|
|
60
87
|
this.logger.aviso('Nenhum tipo selecionado. Operação cancelada.');
|
|
@@ -68,8 +95,10 @@ export class GenerateCommand {
|
|
|
68
95
|
const diretorioAtual = process.cwd();
|
|
69
96
|
let totalGravados = 0;
|
|
70
97
|
|
|
71
|
-
// Reseta decisões anteriores do ConflictResolver
|
|
72
|
-
|
|
98
|
+
// Reseta decisões anteriores do ConflictResolver (se não é headless)
|
|
99
|
+
if (!opcoes.yes) {
|
|
100
|
+
this.conflictResolver.resetar();
|
|
101
|
+
}
|
|
73
102
|
|
|
74
103
|
for (const arquivo of arquivosGerados) {
|
|
75
104
|
const destDir = path.join(diretorioAtual, arquivo.outputDir);
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rook MCP Server — Ponte entre IA e o CLI Rook.
|
|
5
|
+
*
|
|
6
|
+
* Expõe as funcionalidades do CLI como Tools que clientes MCP
|
|
7
|
+
* (Claude Desktop, Cursor, etc.) podem descobrir e executar.
|
|
8
|
+
*
|
|
9
|
+
* Tools disponíveis:
|
|
10
|
+
* - list_components: Lista kits e componentes do repositório remoto
|
|
11
|
+
* - install_component: Instala um componente ou kit no tema local
|
|
12
|
+
* - generate_scaffold: Gera boilerplate de componentes Liquid
|
|
13
|
+
*
|
|
14
|
+
* Princípio: Composição — reutiliza os serviços existentes do CLI
|
|
15
|
+
* Princípio: Responsabilidade Única (SRP) — apenas expõe tools via protocolo MCP
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
import {
|
|
21
|
+
CallToolRequestSchema,
|
|
22
|
+
ListToolsRequestSchema,
|
|
23
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
|
|
26
|
+
// --- Dependências internas do CLI (reutilizadas) ---
|
|
27
|
+
import { Logger } from '../utils/logger.js';
|
|
28
|
+
import { TokenManager } from '../auth/TokenManager.js';
|
|
29
|
+
import { GitHubService } from '../services/GitHubService.js';
|
|
30
|
+
import { DownloadService } from '../services/DownloadService.js';
|
|
31
|
+
import { ScaffoldService } from '../services/ScaffoldService.js';
|
|
32
|
+
import { FileMapper } from '../filesystem/FileMapper.js';
|
|
33
|
+
import { ConflictResolver } from '../filesystem/ConflictResolver.js';
|
|
34
|
+
import { generateNames } from '../utils/stringUtils.js';
|
|
35
|
+
|
|
36
|
+
import path from 'path';
|
|
37
|
+
import os from 'os';
|
|
38
|
+
import fs from 'fs-extra';
|
|
39
|
+
import { fileURLToPath } from 'url';
|
|
40
|
+
|
|
41
|
+
// -- Resolve .env relativo ao diretório do CLI --
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const __dirname = path.dirname(__filename);
|
|
44
|
+
const cliRoot = path.resolve(__dirname, '..', '..');
|
|
45
|
+
|
|
46
|
+
import dotenv from 'dotenv';
|
|
47
|
+
dotenv.config({ path: path.join(cliRoot, '.env') });
|
|
48
|
+
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════
|
|
50
|
+
// Composition Root — instanciação de dependências (mesmo padrão do app.js)
|
|
51
|
+
// ═══════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
const logger = new Logger();
|
|
54
|
+
const tokenManager = new TokenManager(logger);
|
|
55
|
+
const githubService = new GitHubService(logger, tokenManager);
|
|
56
|
+
const downloadService = new DownloadService(logger, tokenManager);
|
|
57
|
+
const conflictResolver = new ConflictResolver(logger);
|
|
58
|
+
const fileMapper = new FileMapper(logger, conflictResolver);
|
|
59
|
+
const scaffoldService = new ScaffoldService(logger);
|
|
60
|
+
|
|
61
|
+
// No modo MCP, o ConflictResolver opera em modo headless (sobrescreve tudo)
|
|
62
|
+
conflictResolver.definirDecisaoGlobal(true);
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════
|
|
65
|
+
// Schemas de validação (Zod)
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
const ListComponentsSchema = z.object({
|
|
69
|
+
category: z
|
|
70
|
+
.enum(['kits', 'components', 'all'])
|
|
71
|
+
.optional()
|
|
72
|
+
.default('all')
|
|
73
|
+
.describe('Filtrar por categoria: "kits", "components" ou "all" (padrão).'),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const InstallComponentSchema = z.object({
|
|
77
|
+
name: z
|
|
78
|
+
.string()
|
|
79
|
+
.min(1)
|
|
80
|
+
.describe('Nome do componente ou kit a ser instalado (ex: "whatsapp-float").'),
|
|
81
|
+
type: z
|
|
82
|
+
.enum(['component', 'kit'])
|
|
83
|
+
.describe('Tipo do pacote: "component" para componente individual, "kit" para kit completo.'),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const GenerateScaffoldSchema = z.object({
|
|
87
|
+
name: z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.describe('Nome do novo recurso (ex: "hero-banner"). Será convertido para kebab-case automaticamente.'),
|
|
91
|
+
elementType: z
|
|
92
|
+
.enum(['section', 'block', 'snippet', 'controller'])
|
|
93
|
+
.describe('Tipo de recurso Shopify a ser gerado.'),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════
|
|
97
|
+
// Funções Handler das Tools
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handler: list_components
|
|
102
|
+
* Lista kits e/ou componentes disponíveis no repositório remoto.
|
|
103
|
+
*/
|
|
104
|
+
async function handleListComponents(args) {
|
|
105
|
+
const { category } = ListComponentsSchema.parse(args);
|
|
106
|
+
|
|
107
|
+
const resultado = {};
|
|
108
|
+
|
|
109
|
+
if (category === 'kits' || category === 'all') {
|
|
110
|
+
const kits = await githubService.listarKits();
|
|
111
|
+
resultado.kits = kits.map(kit => ({
|
|
112
|
+
name: kit.nome,
|
|
113
|
+
slug: kit.slug,
|
|
114
|
+
description: kit.descricao,
|
|
115
|
+
components: kit.componentes,
|
|
116
|
+
path: kit.caminho,
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (category === 'components' || category === 'all') {
|
|
121
|
+
const componentes = await githubService.listarComponentes();
|
|
122
|
+
resultado.components = componentes.map(comp => ({
|
|
123
|
+
name: comp.nome,
|
|
124
|
+
path: comp.caminho,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const totalKits = resultado.kits?.length || 0;
|
|
129
|
+
const totalComponents = resultado.components?.length || 0;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
content: [{
|
|
133
|
+
type: 'text',
|
|
134
|
+
text: JSON.stringify({
|
|
135
|
+
summary: `Encontrados ${totalKits} kit(s) e ${totalComponents} componente(s).`,
|
|
136
|
+
...resultado,
|
|
137
|
+
}, null, 2),
|
|
138
|
+
}],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handler: install_component
|
|
144
|
+
* Instala um componente individual ou kit completo no tema do usuário.
|
|
145
|
+
*/
|
|
146
|
+
async function handleInstallComponent(args) {
|
|
147
|
+
const { name, type } = InstallComponentSchema.parse(args);
|
|
148
|
+
const diretorioAtual = process.cwd();
|
|
149
|
+
|
|
150
|
+
if (type === 'kit') {
|
|
151
|
+
// --- Instalação de Kit ---
|
|
152
|
+
const kits = await githubService.listarKits();
|
|
153
|
+
const kit = kits.find(k => k.slug === name || k.nome === name);
|
|
154
|
+
|
|
155
|
+
if (!kit) {
|
|
156
|
+
const disponiveis = kits.map(k => k.slug).join(', ');
|
|
157
|
+
throw new Error(`Kit "${name}" não encontrado. Kits disponíveis: ${disponiveis}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let totalComponentes = 0;
|
|
161
|
+
let totalArquivos = 0;
|
|
162
|
+
|
|
163
|
+
// Instala cada componente do manifesto
|
|
164
|
+
for (const nomeComponente of kit.componentes) {
|
|
165
|
+
const copiados = await baixarEDistribuir(`components/${nomeComponente}`, diretorioAtual);
|
|
166
|
+
totalArquivos += copiados;
|
|
167
|
+
totalComponentes++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Instala arquivos base do kit
|
|
171
|
+
const copiadosKit = await baixarEDistribuir(kit.caminho, diretorioAtual);
|
|
172
|
+
totalArquivos += copiadosKit;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [{
|
|
176
|
+
type: 'text',
|
|
177
|
+
text: JSON.stringify({
|
|
178
|
+
success: true,
|
|
179
|
+
message: `Kit "${kit.nome}" instalado com sucesso.`,
|
|
180
|
+
components_installed: totalComponentes,
|
|
181
|
+
files_copied: totalArquivos,
|
|
182
|
+
}, null, 2),
|
|
183
|
+
}],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
} else {
|
|
187
|
+
// --- Instalação de Componente Individual ---
|
|
188
|
+
const caminhoRemoto = `components/${name}`;
|
|
189
|
+
const copiados = await baixarEDistribuir(caminhoRemoto, diretorioAtual);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
content: [{
|
|
193
|
+
type: 'text',
|
|
194
|
+
text: JSON.stringify({
|
|
195
|
+
success: true,
|
|
196
|
+
message: `Componente "${name}" instalado com sucesso.`,
|
|
197
|
+
files_copied: copiados,
|
|
198
|
+
}, null, 2),
|
|
199
|
+
}],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handler: generate_scaffold
|
|
206
|
+
* Gera boilerplate de um componente Liquid (section, block, snippet, controller).
|
|
207
|
+
*/
|
|
208
|
+
async function handleGenerateScaffold(args) {
|
|
209
|
+
const { name, elementType } = GenerateScaffoldSchema.parse(args);
|
|
210
|
+
|
|
211
|
+
// Normaliza nomes (kebab, Pascal, snake)
|
|
212
|
+
const names = generateNames(name);
|
|
213
|
+
|
|
214
|
+
// Gera o conteúdo via ScaffoldService
|
|
215
|
+
const arquivo = await scaffoldService.generate(elementType, names);
|
|
216
|
+
|
|
217
|
+
// Grava no disco
|
|
218
|
+
const diretorioAtual = process.cwd();
|
|
219
|
+
const destDir = path.join(diretorioAtual, arquivo.outputDir);
|
|
220
|
+
const destPath = path.join(destDir, arquivo.fileName);
|
|
221
|
+
const caminhoRelativo = path.join(arquivo.outputDir, arquivo.fileName);
|
|
222
|
+
|
|
223
|
+
await fs.ensureDir(destDir);
|
|
224
|
+
await fs.writeFile(destPath, arquivo.content, 'utf-8');
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
content: [{
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: JSON.stringify({
|
|
230
|
+
success: true,
|
|
231
|
+
message: `Scaffold gerado com sucesso: ${caminhoRelativo}`,
|
|
232
|
+
file: caminhoRelativo,
|
|
233
|
+
names: {
|
|
234
|
+
kebab: names.kebabName,
|
|
235
|
+
pascal: names.PascalName,
|
|
236
|
+
snake: names.snake_name,
|
|
237
|
+
},
|
|
238
|
+
}, null, 2),
|
|
239
|
+
}],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ═══════════════════════════════════════════════════════════════
|
|
244
|
+
// Função auxiliar: Download + Distribuição
|
|
245
|
+
// ═══════════════════════════════════════════════════════════════
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Processo genérico: baixa do repositório e distribui nas pastas locais.
|
|
249
|
+
* Reutiliza o mesmo padrão do AddCommand._baixarEDistribuir().
|
|
250
|
+
*
|
|
251
|
+
* @param {string} caminhoRemoto - Caminho no repositório remoto
|
|
252
|
+
* @param {string} diretorioDestino - Diretório raiz do tema Shopify local
|
|
253
|
+
* @returns {Promise<number>} Número de arquivos copiados
|
|
254
|
+
*/
|
|
255
|
+
async function baixarEDistribuir(caminhoRemoto, diretorioDestino) {
|
|
256
|
+
const pastaTmp = path.join(os.tmpdir(), `rook-mcp-${Date.now()}`);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await fs.ensureDir(pastaTmp);
|
|
260
|
+
await downloadService.baixar(caminhoRemoto, pastaTmp);
|
|
261
|
+
const totalCopiados = await fileMapper.distribuir(pastaTmp, diretorioDestino);
|
|
262
|
+
return totalCopiados;
|
|
263
|
+
} finally {
|
|
264
|
+
await fs.remove(pastaTmp);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ═══════════════════════════════════════════════════════════════
|
|
269
|
+
// Inicialização do Servidor MCP
|
|
270
|
+
// ═══════════════════════════════════════════════════════════════
|
|
271
|
+
|
|
272
|
+
const server = new Server(
|
|
273
|
+
{
|
|
274
|
+
name: 'rook-cli-server',
|
|
275
|
+
version: '1.0.0',
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
capabilities: {
|
|
279
|
+
tools: {},
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// --- Registrar lista de Tools disponíveis ---
|
|
285
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
286
|
+
return {
|
|
287
|
+
tools: [
|
|
288
|
+
{
|
|
289
|
+
name: 'list_components',
|
|
290
|
+
description:
|
|
291
|
+
'Lista componentes e kits disponíveis no repositório Rook. ' +
|
|
292
|
+
'Use para descobrir o que pode ser instalado no tema Shopify do usuário.',
|
|
293
|
+
inputSchema: {
|
|
294
|
+
type: 'object',
|
|
295
|
+
properties: {
|
|
296
|
+
category: {
|
|
297
|
+
type: 'string',
|
|
298
|
+
enum: ['kits', 'components', 'all'],
|
|
299
|
+
description: 'Filtrar por categoria: "kits", "components" ou "all" (padrão).',
|
|
300
|
+
default: 'all',
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'install_component',
|
|
307
|
+
description:
|
|
308
|
+
'Instala um componente individual ou kit completo no tema Shopify atual. ' +
|
|
309
|
+
'Os arquivos são automaticamente distribuídos nas pastas corretas (sections/, snippets/, blocks/, assets/).',
|
|
310
|
+
inputSchema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
name: {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'Nome/slug do componente ou kit (ex: "whatsapp-float", "starter-base").',
|
|
316
|
+
},
|
|
317
|
+
type: {
|
|
318
|
+
type: 'string',
|
|
319
|
+
enum: ['component', 'kit'],
|
|
320
|
+
description: 'Tipo do pacote: "component" ou "kit".',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ['name', 'type'],
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'generate_scaffold',
|
|
328
|
+
description:
|
|
329
|
+
'Gera a estrutura inicial (boilerplate) de um novo componente Liquid para Shopify. ' +
|
|
330
|
+
'Cria o arquivo com template padronizado já preenchido com LiquidDoc e Web Components.',
|
|
331
|
+
inputSchema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
name: {
|
|
335
|
+
type: 'string',
|
|
336
|
+
description: 'Nome do novo recurso em qualquer formato (ex: "hero-banner", "HeroBanner"). Será normalizado automaticamente.',
|
|
337
|
+
},
|
|
338
|
+
elementType: {
|
|
339
|
+
type: 'string',
|
|
340
|
+
enum: ['section', 'block', 'snippet', 'controller'],
|
|
341
|
+
description: 'Tipo de recurso Shopify: section, block, snippet ou controller (JS asset).',
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
required: ['name', 'elementType'],
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// --- Executar Tool solicitada pela IA ---
|
|
352
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
353
|
+
const { name, arguments: args } = request.params;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
switch (name) {
|
|
357
|
+
case 'list_components':
|
|
358
|
+
return await handleListComponents(args || {});
|
|
359
|
+
|
|
360
|
+
case 'install_component':
|
|
361
|
+
return await handleInstallComponent(args);
|
|
362
|
+
|
|
363
|
+
case 'generate_scaffold':
|
|
364
|
+
return await handleGenerateScaffold(args);
|
|
365
|
+
|
|
366
|
+
default:
|
|
367
|
+
throw new Error(`Tool desconhecida: "${name}". Tools disponíveis: list_components, install_component, generate_scaffold`);
|
|
368
|
+
}
|
|
369
|
+
} catch (erro) {
|
|
370
|
+
// Retorna erro estruturado para a IA
|
|
371
|
+
return {
|
|
372
|
+
isError: true,
|
|
373
|
+
content: [{
|
|
374
|
+
type: 'text',
|
|
375
|
+
text: JSON.stringify({
|
|
376
|
+
error: true,
|
|
377
|
+
tool: name,
|
|
378
|
+
message: erro.message,
|
|
379
|
+
}, null, 2),
|
|
380
|
+
}],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// --- Iniciar transporte Stdio ---
|
|
386
|
+
const transport = new StdioServerTransport();
|
|
387
|
+
await server.connect(transport);
|
package/src/utils/logger.js
CHANGED
|
@@ -49,7 +49,7 @@ export class Logger {
|
|
|
49
49
|
banner() {
|
|
50
50
|
console.log('');
|
|
51
51
|
console.log(pc.bold(pc.white(' ╔══════════════════════════════╗')));
|
|
52
|
-
console.log(pc.bold(pc.white(' ║ ♟️ ROOK CLI v1.
|
|
52
|
+
console.log(pc.bold(pc.white(' ║ ♟️ ROOK CLI v1.1.1 ║')));
|
|
53
53
|
console.log(pc.bold(pc.white(' ║ Shopify Component Tool ║')));
|
|
54
54
|
console.log(pc.bold(pc.white(' ╚══════════════════════════════╝')));
|
|
55
55
|
console.log('');
|