rook-cli 1.1.0 → 1.2.1

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 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", "-q", "--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.1.0",
3
+ "version": "1.2.1",
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
- .action(async () => {
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
- .action(async (nome) => {
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
- // 1. Menu principal
42
- const tipoInstalacao = await this.promptUI.menuPrincipal();
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. Usuário seleciona o kit
82
- const kitSelecionado = await this.promptUI.selecionarKit(kits);
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
- const selecionados = await this.promptUI.selecionarComponentes(componentes);
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. Pergunta quais tipos gerar
57
- const tiposSelecionados = await this.promptUI.selecionarTiposGerador();
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
- this.conflictResolver.resetar();
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,399 @@
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
+ // ═══════════════════════════════════════════════════════════════
19
+ // IMPORTANTE: Redireciona saídas puramente textuais para stderr
20
+ // MCP requer que o stdout seja estritamente usado para o protocolo JSON-RPC.
21
+ // Geração de logs da própria aplicação (como nosso logger interno ou o dotenv)
22
+ // quebrará o handshake do Cursor/Claude Desktop, portanto desviamos
23
+ // tudo que é visual para stderr.
24
+ // ═══════════════════════════════════════════════════════════════
25
+ console.log = console.error;
26
+ console.info = console.error;
27
+ console.warn = console.error;
28
+ console.debug = console.error;
29
+
30
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
31
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
32
+ import {
33
+ CallToolRequestSchema,
34
+ ListToolsRequestSchema,
35
+ } from '@modelcontextprotocol/sdk/types.js';
36
+ import { z } from 'zod';
37
+
38
+ // --- Dependências internas do CLI (reutilizadas) ---
39
+ import { Logger } from '../utils/logger.js';
40
+ import { TokenManager } from '../auth/TokenManager.js';
41
+ import { GitHubService } from '../services/GitHubService.js';
42
+ import { DownloadService } from '../services/DownloadService.js';
43
+ import { ScaffoldService } from '../services/ScaffoldService.js';
44
+ import { FileMapper } from '../filesystem/FileMapper.js';
45
+ import { ConflictResolver } from '../filesystem/ConflictResolver.js';
46
+ import { generateNames } from '../utils/stringUtils.js';
47
+
48
+ import path from 'path';
49
+ import os from 'os';
50
+ import fs from 'fs-extra';
51
+ import { fileURLToPath } from 'url';
52
+
53
+ // -- Resolve .env relativo ao diretório do CLI --
54
+ const __filename = fileURLToPath(import.meta.url);
55
+ const __dirname = path.dirname(__filename);
56
+ const cliRoot = path.resolve(__dirname, '..', '..');
57
+
58
+ import dotenv from 'dotenv';
59
+ dotenv.config({ path: path.join(cliRoot, '.env') });
60
+
61
+ // ═══════════════════════════════════════════════════════════════
62
+ // Composition Root — instanciação de dependências (mesmo padrão do app.js)
63
+ // ═══════════════════════════════════════════════════════════════
64
+
65
+ const logger = new Logger();
66
+ const tokenManager = new TokenManager(logger);
67
+ const githubService = new GitHubService(logger, tokenManager);
68
+ const downloadService = new DownloadService(logger, tokenManager);
69
+ const conflictResolver = new ConflictResolver(logger);
70
+ const fileMapper = new FileMapper(logger, conflictResolver);
71
+ const scaffoldService = new ScaffoldService(logger);
72
+
73
+ // No modo MCP, o ConflictResolver opera em modo headless (sobrescreve tudo)
74
+ conflictResolver.definirDecisaoGlobal(true);
75
+
76
+ // ═══════════════════════════════════════════════════════════════
77
+ // Schemas de validação (Zod)
78
+ // ═══════════════════════════════════════════════════════════════
79
+
80
+ const ListComponentsSchema = z.object({
81
+ category: z
82
+ .enum(['kits', 'components', 'all'])
83
+ .optional()
84
+ .default('all')
85
+ .describe('Filtrar por categoria: "kits", "components" ou "all" (padrão).'),
86
+ });
87
+
88
+ const InstallComponentSchema = z.object({
89
+ name: z
90
+ .string()
91
+ .min(1)
92
+ .describe('Nome do componente ou kit a ser instalado (ex: "whatsapp-float").'),
93
+ type: z
94
+ .enum(['component', 'kit'])
95
+ .describe('Tipo do pacote: "component" para componente individual, "kit" para kit completo.'),
96
+ });
97
+
98
+ const GenerateScaffoldSchema = z.object({
99
+ name: z
100
+ .string()
101
+ .min(1)
102
+ .describe('Nome do novo recurso (ex: "hero-banner"). Será convertido para kebab-case automaticamente.'),
103
+ elementType: z
104
+ .enum(['section', 'block', 'snippet', 'controller'])
105
+ .describe('Tipo de recurso Shopify a ser gerado.'),
106
+ });
107
+
108
+ // ═══════════════════════════════════════════════════════════════
109
+ // Funções Handler das Tools
110
+ // ═══════════════════════════════════════════════════════════════
111
+
112
+ /**
113
+ * Handler: list_components
114
+ * Lista kits e/ou componentes disponíveis no repositório remoto.
115
+ */
116
+ async function handleListComponents(args) {
117
+ const { category } = ListComponentsSchema.parse(args);
118
+
119
+ const resultado = {};
120
+
121
+ if (category === 'kits' || category === 'all') {
122
+ const kits = await githubService.listarKits();
123
+ resultado.kits = kits.map(kit => ({
124
+ name: kit.nome,
125
+ slug: kit.slug,
126
+ description: kit.descricao,
127
+ components: kit.componentes,
128
+ path: kit.caminho,
129
+ }));
130
+ }
131
+
132
+ if (category === 'components' || category === 'all') {
133
+ const componentes = await githubService.listarComponentes();
134
+ resultado.components = componentes.map(comp => ({
135
+ name: comp.nome,
136
+ path: comp.caminho,
137
+ }));
138
+ }
139
+
140
+ const totalKits = resultado.kits?.length || 0;
141
+ const totalComponents = resultado.components?.length || 0;
142
+
143
+ return {
144
+ content: [{
145
+ type: 'text',
146
+ text: JSON.stringify({
147
+ summary: `Encontrados ${totalKits} kit(s) e ${totalComponents} componente(s).`,
148
+ ...resultado,
149
+ }, null, 2),
150
+ }],
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Handler: install_component
156
+ * Instala um componente individual ou kit completo no tema do usuário.
157
+ */
158
+ async function handleInstallComponent(args) {
159
+ const { name, type } = InstallComponentSchema.parse(args);
160
+ const diretorioAtual = process.cwd();
161
+
162
+ if (type === 'kit') {
163
+ // --- Instalação de Kit ---
164
+ const kits = await githubService.listarKits();
165
+ const kit = kits.find(k => k.slug === name || k.nome === name);
166
+
167
+ if (!kit) {
168
+ const disponiveis = kits.map(k => k.slug).join(', ');
169
+ throw new Error(`Kit "${name}" não encontrado. Kits disponíveis: ${disponiveis}`);
170
+ }
171
+
172
+ let totalComponentes = 0;
173
+ let totalArquivos = 0;
174
+
175
+ // Instala cada componente do manifesto
176
+ for (const nomeComponente of kit.componentes) {
177
+ const copiados = await baixarEDistribuir(`components/${nomeComponente}`, diretorioAtual);
178
+ totalArquivos += copiados;
179
+ totalComponentes++;
180
+ }
181
+
182
+ // Instala arquivos base do kit
183
+ const copiadosKit = await baixarEDistribuir(kit.caminho, diretorioAtual);
184
+ totalArquivos += copiadosKit;
185
+
186
+ return {
187
+ content: [{
188
+ type: 'text',
189
+ text: JSON.stringify({
190
+ success: true,
191
+ message: `Kit "${kit.nome}" instalado com sucesso.`,
192
+ components_installed: totalComponentes,
193
+ files_copied: totalArquivos,
194
+ }, null, 2),
195
+ }],
196
+ };
197
+
198
+ } else {
199
+ // --- Instalação de Componente Individual ---
200
+ const caminhoRemoto = `components/${name}`;
201
+ const copiados = await baixarEDistribuir(caminhoRemoto, diretorioAtual);
202
+
203
+ return {
204
+ content: [{
205
+ type: 'text',
206
+ text: JSON.stringify({
207
+ success: true,
208
+ message: `Componente "${name}" instalado com sucesso.`,
209
+ files_copied: copiados,
210
+ }, null, 2),
211
+ }],
212
+ };
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Handler: generate_scaffold
218
+ * Gera boilerplate de um componente Liquid (section, block, snippet, controller).
219
+ */
220
+ async function handleGenerateScaffold(args) {
221
+ const { name, elementType } = GenerateScaffoldSchema.parse(args);
222
+
223
+ // Normaliza nomes (kebab, Pascal, snake)
224
+ const names = generateNames(name);
225
+
226
+ // Gera o conteúdo via ScaffoldService
227
+ const arquivo = await scaffoldService.generate(elementType, names);
228
+
229
+ // Grava no disco
230
+ const diretorioAtual = process.cwd();
231
+ const destDir = path.join(diretorioAtual, arquivo.outputDir);
232
+ const destPath = path.join(destDir, arquivo.fileName);
233
+ const caminhoRelativo = path.join(arquivo.outputDir, arquivo.fileName);
234
+
235
+ await fs.ensureDir(destDir);
236
+ await fs.writeFile(destPath, arquivo.content, 'utf-8');
237
+
238
+ return {
239
+ content: [{
240
+ type: 'text',
241
+ text: JSON.stringify({
242
+ success: true,
243
+ message: `Scaffold gerado com sucesso: ${caminhoRelativo}`,
244
+ file: caminhoRelativo,
245
+ names: {
246
+ kebab: names.kebabName,
247
+ pascal: names.PascalName,
248
+ snake: names.snake_name,
249
+ },
250
+ }, null, 2),
251
+ }],
252
+ };
253
+ }
254
+
255
+ // ═══════════════════════════════════════════════════════════════
256
+ // Função auxiliar: Download + Distribuição
257
+ // ═══════════════════════════════════════════════════════════════
258
+
259
+ /**
260
+ * Processo genérico: baixa do repositório e distribui nas pastas locais.
261
+ * Reutiliza o mesmo padrão do AddCommand._baixarEDistribuir().
262
+ *
263
+ * @param {string} caminhoRemoto - Caminho no repositório remoto
264
+ * @param {string} diretorioDestino - Diretório raiz do tema Shopify local
265
+ * @returns {Promise<number>} Número de arquivos copiados
266
+ */
267
+ async function baixarEDistribuir(caminhoRemoto, diretorioDestino) {
268
+ const pastaTmp = path.join(os.tmpdir(), `rook-mcp-${Date.now()}`);
269
+
270
+ try {
271
+ await fs.ensureDir(pastaTmp);
272
+ await downloadService.baixar(caminhoRemoto, pastaTmp);
273
+ const totalCopiados = await fileMapper.distribuir(pastaTmp, diretorioDestino);
274
+ return totalCopiados;
275
+ } finally {
276
+ await fs.remove(pastaTmp);
277
+ }
278
+ }
279
+
280
+ // ═══════════════════════════════════════════════════════════════
281
+ // Inicialização do Servidor MCP
282
+ // ═══════════════════════════════════════════════════════════════
283
+
284
+ const server = new Server(
285
+ {
286
+ name: 'rook-cli-server',
287
+ version: '1.0.0',
288
+ },
289
+ {
290
+ capabilities: {
291
+ tools: {},
292
+ },
293
+ }
294
+ );
295
+
296
+ // --- Registrar lista de Tools disponíveis ---
297
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
298
+ return {
299
+ tools: [
300
+ {
301
+ name: 'list_components',
302
+ description:
303
+ 'Lista componentes e kits disponíveis no repositório Rook. ' +
304
+ 'Use para descobrir o que pode ser instalado no tema Shopify do usuário.',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ category: {
309
+ type: 'string',
310
+ enum: ['kits', 'components', 'all'],
311
+ description: 'Filtrar por categoria: "kits", "components" ou "all" (padrão).',
312
+ default: 'all',
313
+ },
314
+ },
315
+ },
316
+ },
317
+ {
318
+ name: 'install_component',
319
+ description:
320
+ 'Instala um componente individual ou kit completo no tema Shopify atual. ' +
321
+ 'Os arquivos são automaticamente distribuídos nas pastas corretas (sections/, snippets/, blocks/, assets/).',
322
+ inputSchema: {
323
+ type: 'object',
324
+ properties: {
325
+ name: {
326
+ type: 'string',
327
+ description: 'Nome/slug do componente ou kit (ex: "whatsapp-float", "starter-base").',
328
+ },
329
+ type: {
330
+ type: 'string',
331
+ enum: ['component', 'kit'],
332
+ description: 'Tipo do pacote: "component" ou "kit".',
333
+ },
334
+ },
335
+ required: ['name', 'type'],
336
+ },
337
+ },
338
+ {
339
+ name: 'generate_scaffold',
340
+ description:
341
+ 'Gera a estrutura inicial (boilerplate) de um novo componente Liquid para Shopify. ' +
342
+ 'Cria o arquivo com template padronizado já preenchido com LiquidDoc e Web Components.',
343
+ inputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ name: {
347
+ type: 'string',
348
+ description: 'Nome do novo recurso em qualquer formato (ex: "hero-banner", "HeroBanner"). Será normalizado automaticamente.',
349
+ },
350
+ elementType: {
351
+ type: 'string',
352
+ enum: ['section', 'block', 'snippet', 'controller'],
353
+ description: 'Tipo de recurso Shopify: section, block, snippet ou controller (JS asset).',
354
+ },
355
+ },
356
+ required: ['name', 'elementType'],
357
+ },
358
+ },
359
+ ],
360
+ };
361
+ });
362
+
363
+ // --- Executar Tool solicitada pela IA ---
364
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
365
+ const { name, arguments: args } = request.params;
366
+
367
+ try {
368
+ switch (name) {
369
+ case 'list_components':
370
+ return await handleListComponents(args || {});
371
+
372
+ case 'install_component':
373
+ return await handleInstallComponent(args);
374
+
375
+ case 'generate_scaffold':
376
+ return await handleGenerateScaffold(args);
377
+
378
+ default:
379
+ throw new Error(`Tool desconhecida: "${name}". Tools disponíveis: list_components, install_component, generate_scaffold`);
380
+ }
381
+ } catch (erro) {
382
+ // Retorna erro estruturado para a IA
383
+ return {
384
+ isError: true,
385
+ content: [{
386
+ type: 'text',
387
+ text: JSON.stringify({
388
+ error: true,
389
+ tool: name,
390
+ message: erro.message,
391
+ }, null, 2),
392
+ }],
393
+ };
394
+ }
395
+ });
396
+
397
+ // --- Iniciar transporte Stdio ---
398
+ const transport = new StdioServerTransport();
399
+ await server.connect(transport);
@@ -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.0.3 ║')));
52
+ console.log(pc.bold(pc.white(' ║ ♟️ ROOK CLI v1.2.1 ║')));
53
53
  console.log(pc.bold(pc.white(' ║ Shopify Component Tool ║')));
54
54
  console.log(pc.bold(pc.white(' ╚══════════════════════════════╝')));
55
55
  console.log('');