rook-cli 1.0.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 +71 -0
- package/bin/rook.js +14 -0
- package/package.json +37 -0
- package/src/app.js +93 -0
- package/src/auth/TokenManager.js +118 -0
- package/src/commands/AddCommand.js +121 -0
- package/src/commands/ConfigCommand.js +128 -0
- package/src/config/constants.js +40 -0
- package/src/filesystem/ConflictResolver.js +70 -0
- package/src/filesystem/FileMapper.js +123 -0
- package/src/services/DownloadService.js +151 -0
- package/src/services/GitHubService.js +99 -0
- package/src/ui/PromptUI.js +84 -0
- package/src/utils/logger.js +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# 🚀 ROOK CLI — Shopify Component Tool
|
|
2
|
+
|
|
3
|
+
CLI para instalar componentes Shopify de um repositório centralizado no GitHub.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Instalar dependências
|
|
9
|
+
npm install
|
|
10
|
+
|
|
11
|
+
# Linkar globalmente (para usar o comando "rook" no terminal)
|
|
12
|
+
npm link
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Uso
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Abre o menu interativo
|
|
19
|
+
rook
|
|
20
|
+
|
|
21
|
+
# Comando direto
|
|
22
|
+
rook add
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Caso tenha algum repositorio privado, você pode configurar o token de acesso:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
rook config
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
# 1. Configurar o token (uma única vez)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node bin/rook.js config
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
# 2. Usar normalmente — autenticação é automática
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node bin/rook.js add
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Estrutura do Projeto
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
src/
|
|
47
|
+
├── app.js # Classe principal (Composition Root)
|
|
48
|
+
├── commands/
|
|
49
|
+
│ └── AddCommand.js # Comando "add" — orquestra o fluxo
|
|
50
|
+
├── ui/
|
|
51
|
+
│ └── PromptUI.js # Menus interativos (Inquirer)
|
|
52
|
+
├── services/
|
|
53
|
+
│ ├── GitHubService.js # Comunicação com GitHub API
|
|
54
|
+
│ └── DownloadService.js # Download via tiged
|
|
55
|
+
├── filesystem/
|
|
56
|
+
│ ├── FileMapper.js # Mapeamento de diretórios Shopify
|
|
57
|
+
│ └── ConflictResolver.js # Tratamento de conflitos
|
|
58
|
+
├── config/
|
|
59
|
+
│ └── constants.js # Constantes globais
|
|
60
|
+
└── utils/
|
|
61
|
+
└── logger.js # Logger com feedback visual
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Tecnologias
|
|
65
|
+
|
|
66
|
+
- **Node.js** (ESM) — Ambiente de execução
|
|
67
|
+
- **Commander** — Parser de comandos CLI
|
|
68
|
+
- **Inquirer** — Menus interativos
|
|
69
|
+
- **tiged** — Download eficiente sem `.git`
|
|
70
|
+
- **fs-extra** — Manipulação de arquivos
|
|
71
|
+
- **picocolors** — Estilo visual no terminal
|
package/bin/rook.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ponto de entrada do CLI — ROOK CLI
|
|
5
|
+
*
|
|
6
|
+
* Este arquivo é o entry point registrado no package.json.
|
|
7
|
+
* Sua única responsabilidade é inicializar a aplicação.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import 'dotenv/config';
|
|
11
|
+
import { App } from '../src/app.js';
|
|
12
|
+
|
|
13
|
+
const app = new App();
|
|
14
|
+
app.run();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rook-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI para instalar componentes Shopify de um repositório centralizado",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/chesslabdev/rook-cli.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"rook": "bin/rook.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/rook.js",
|
|
19
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"shopify",
|
|
23
|
+
"cli",
|
|
24
|
+
"components",
|
|
25
|
+
"themes"
|
|
26
|
+
],
|
|
27
|
+
"author": "ROOK",
|
|
28
|
+
"license": "ISC",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@inquirer/prompts": "^8.3.0",
|
|
31
|
+
"commander": "^14.0.3",
|
|
32
|
+
"dotenv": "^17.3.1",
|
|
33
|
+
"fs-extra": "^11.3.3",
|
|
34
|
+
"picocolors": "^1.1.1",
|
|
35
|
+
"tiged": "^2.12.7"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — Classe principal da aplicação.
|
|
3
|
+
*
|
|
4
|
+
* Responsável por:
|
|
5
|
+
* - Registrar comandos no Commander
|
|
6
|
+
* - Instanciar e injetar dependências (Composition Root)
|
|
7
|
+
* - Iniciar o CLI
|
|
8
|
+
*
|
|
9
|
+
* Princípio: Inversão de Dependência (DIP) — atua como Composition Root
|
|
10
|
+
* Princípio: Aberto/Fechado (OCP) — novos comandos são adicionados sem alterar o existente
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import { Logger } from './utils/logger.js';
|
|
15
|
+
import { PromptUI } from './ui/PromptUI.js';
|
|
16
|
+
import { TokenManager } from './auth/TokenManager.js';
|
|
17
|
+
import { GitHubService } from './services/GitHubService.js';
|
|
18
|
+
import { DownloadService } from './services/DownloadService.js';
|
|
19
|
+
import { FileMapper } from './filesystem/FileMapper.js';
|
|
20
|
+
import { ConflictResolver } from './filesystem/ConflictResolver.js';
|
|
21
|
+
import { AddCommand } from './commands/AddCommand.js';
|
|
22
|
+
import { ConfigCommand } from './commands/ConfigCommand.js';
|
|
23
|
+
import { CLI_NAME } from './config/constants.js';
|
|
24
|
+
|
|
25
|
+
export class App {
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
// -- Instância do Commander (parser de comandos) --
|
|
29
|
+
this.programa = new Command();
|
|
30
|
+
|
|
31
|
+
// -- Instanciação de dependências (Composition Root) --
|
|
32
|
+
this.logger = new Logger();
|
|
33
|
+
this.promptUI = new PromptUI();
|
|
34
|
+
this.tokenManager = new TokenManager(this.logger);
|
|
35
|
+
this.githubService = new GitHubService(this.logger, this.tokenManager);
|
|
36
|
+
this.downloadService = new DownloadService(this.logger, this.tokenManager);
|
|
37
|
+
this.conflictResolver = new ConflictResolver(this.logger);
|
|
38
|
+
this.fileMapper = new FileMapper(this.logger, this.conflictResolver);
|
|
39
|
+
|
|
40
|
+
// -- Comandos --
|
|
41
|
+
this.addCommand = new AddCommand(
|
|
42
|
+
this.logger,
|
|
43
|
+
this.promptUI,
|
|
44
|
+
this.githubService,
|
|
45
|
+
this.downloadService,
|
|
46
|
+
this.fileMapper
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
this.configCommand = new ConfigCommand(
|
|
50
|
+
this.logger,
|
|
51
|
+
this.tokenManager
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configura e executa o CLI.
|
|
57
|
+
* Registra todos os comandos e faz o parse dos argumentos.
|
|
58
|
+
*/
|
|
59
|
+
run() {
|
|
60
|
+
this.programa
|
|
61
|
+
.name(CLI_NAME)
|
|
62
|
+
.description('CLI para instalar componentes Shopify de um repositório centralizado')
|
|
63
|
+
.version('1.0.0');
|
|
64
|
+
|
|
65
|
+
// Comando: rook add
|
|
66
|
+
this.programa
|
|
67
|
+
.command('add')
|
|
68
|
+
.description('Adiciona kits ou componentes ao tema Shopify atual')
|
|
69
|
+
.action(async () => {
|
|
70
|
+
this.logger.banner();
|
|
71
|
+
await this.addCommand.executar();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Comando: rook config
|
|
75
|
+
this.programa
|
|
76
|
+
.command('config')
|
|
77
|
+
.description('Configura o token de acesso para repositórios privados do GitHub')
|
|
78
|
+
.action(async () => {
|
|
79
|
+
this.logger.banner();
|
|
80
|
+
await this.configCommand.executar();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Comando padrão (sem argumentos) — abre o menu interativo
|
|
84
|
+
this.programa
|
|
85
|
+
.action(async () => {
|
|
86
|
+
this.logger.banner();
|
|
87
|
+
await this.addCommand.executar();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Parse dos argumentos
|
|
91
|
+
this.programa.parse(process.argv);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenManager — Gerenciamento do token de acesso GitHub.
|
|
3
|
+
*
|
|
4
|
+
* Armazena e recupera o Personal Access Token (PAT) do GitHub
|
|
5
|
+
* em um arquivo de configuração na home do usuário (~/.rookrc).
|
|
6
|
+
* O token NUNCA é commitado no repositório do projeto.
|
|
7
|
+
*
|
|
8
|
+
* Princípio: Responsabilidade Única (SRP) — apenas gerencia credenciais
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
|
|
15
|
+
/** Caminho do arquivo de configuração (~/.rookrc) */
|
|
16
|
+
const CONFIG_PATH = path.join(os.homedir(), '.rookrc');
|
|
17
|
+
|
|
18
|
+
export class TokenManager {
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
22
|
+
*/
|
|
23
|
+
constructor(logger) {
|
|
24
|
+
this.logger = logger;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lê a configuração salva no disco.
|
|
29
|
+
* @returns {Promise<Object>} Objeto de configuração
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
async _lerConfig() {
|
|
33
|
+
try {
|
|
34
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
35
|
+
const conteudo = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
36
|
+
return JSON.parse(conteudo);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Se o arquivo estiver corrompido, retorna vazio
|
|
40
|
+
this.logger.aviso('Arquivo de configuração corrompido. Recriando...');
|
|
41
|
+
}
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Salva a configuração no disco.
|
|
47
|
+
* @param {Object} config - Objeto de configuração
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
async _salvarConfig(config) {
|
|
51
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Salva o token de acesso do GitHub.
|
|
56
|
+
* @param {string} token - Personal Access Token (PAT) do GitHub
|
|
57
|
+
*/
|
|
58
|
+
async salvarToken(token) {
|
|
59
|
+
const config = await this._lerConfig();
|
|
60
|
+
config.githubToken = token;
|
|
61
|
+
await this._salvarConfig(config);
|
|
62
|
+
this.logger.sucesso(`Token salvo em: ${CONFIG_PATH}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Recupera o token de acesso do GitHub.
|
|
67
|
+
* @returns {Promise<string|null>} Token salvo ou null se não existir
|
|
68
|
+
*/
|
|
69
|
+
async obterToken() {
|
|
70
|
+
// 1. Prioridade: variável de ambiente
|
|
71
|
+
if (process.env.GITHUB_TOKEN) {
|
|
72
|
+
return process.env.GITHUB_TOKEN;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Fallback: arquivo de configuração
|
|
76
|
+
const config = await this._lerConfig();
|
|
77
|
+
return config.githubToken || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Verifica se existe um token configurado.
|
|
82
|
+
* @returns {Promise<boolean>}
|
|
83
|
+
*/
|
|
84
|
+
async temToken() {
|
|
85
|
+
const token = await this.obterToken();
|
|
86
|
+
return token !== null && token.length > 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove o token salvo.
|
|
91
|
+
*/
|
|
92
|
+
async removerToken() {
|
|
93
|
+
const config = await this._lerConfig();
|
|
94
|
+
delete config.githubToken;
|
|
95
|
+
await this._salvarConfig(config);
|
|
96
|
+
this.logger.sucesso('Token removido com sucesso.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Retorna os headers de autenticação para uso nas requisições.
|
|
101
|
+
* Se não houver token, retorna headers sem autenticação.
|
|
102
|
+
*
|
|
103
|
+
* @returns {Promise<Object>} Headers HTTP com ou sem Authorization
|
|
104
|
+
*/
|
|
105
|
+
async obterHeaders() {
|
|
106
|
+
const headers = {
|
|
107
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const token = await this.obterToken();
|
|
111
|
+
|
|
112
|
+
if (token) {
|
|
113
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return headers;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddCommand — Comando "add" do CLI.
|
|
3
|
+
*
|
|
4
|
+
* Orquestra o fluxo de instalação:
|
|
5
|
+
* 1. Exibe menu de seleção (kit ou componente)
|
|
6
|
+
* 2. Lista itens disponíveis via GitHub
|
|
7
|
+
* 3. Realiza download via tiged
|
|
8
|
+
* 4. Distribui arquivos nas pastas corretas
|
|
9
|
+
*
|
|
10
|
+
* Princípio: Inversão de Dependência (DIP) — todas as dependências são injetadas
|
|
11
|
+
* Princípio: Responsabilidade Única (SRP) — orquestra apenas o fluxo do comando "add"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
|
|
18
|
+
export class AddCommand {
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
22
|
+
* @param {import('../ui/PromptUI.js').PromptUI} promptUI
|
|
23
|
+
* @param {import('../services/GitHubService.js').GitHubService} githubService
|
|
24
|
+
* @param {import('../services/DownloadService.js').DownloadService} downloadService
|
|
25
|
+
* @param {import('../filesystem/FileMapper.js').FileMapper} fileMapper
|
|
26
|
+
*/
|
|
27
|
+
constructor(logger, promptUI, githubService, downloadService, fileMapper) {
|
|
28
|
+
this.logger = logger;
|
|
29
|
+
this.promptUI = promptUI;
|
|
30
|
+
this.githubService = githubService;
|
|
31
|
+
this.downloadService = downloadService;
|
|
32
|
+
this.fileMapper = fileMapper;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executa o fluxo principal do comando "add".
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async executar() {
|
|
40
|
+
try {
|
|
41
|
+
// 1. Menu principal
|
|
42
|
+
const tipoInstalacao = await this.promptUI.menuPrincipal();
|
|
43
|
+
|
|
44
|
+
// 2. Delega para o fluxo correto
|
|
45
|
+
if (tipoInstalacao === 'kit') {
|
|
46
|
+
await this._instalarKit();
|
|
47
|
+
} else {
|
|
48
|
+
await this._instalarComponentes();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} catch (erro) {
|
|
52
|
+
// Trata cancelamento do usuário (Ctrl+C)
|
|
53
|
+
if (erro.name === 'ExitPromptError') {
|
|
54
|
+
this.logger.aviso('Operação cancelada pelo usuário.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.logger.erro(`Erro durante a instalação: ${erro.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fluxo de instalação de um kit completo.
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
async _instalarKit() {
|
|
66
|
+
// Lista kits disponíveis
|
|
67
|
+
const kits = await this.githubService.listarKits();
|
|
68
|
+
const kitSelecionado = await this.promptUI.selecionarKit(kits);
|
|
69
|
+
|
|
70
|
+
this.logger.destaque(`\n📥 Instalando kit: ${kitSelecionado.nome}\n`);
|
|
71
|
+
|
|
72
|
+
// Baixa e distribui
|
|
73
|
+
await this._baixarEDistribuir(kitSelecionado.caminho, kitSelecionado.nome);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Fluxo de instalação de componentes individuais.
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
async _instalarComponentes() {
|
|
81
|
+
// Lista componentes disponíveis
|
|
82
|
+
const componentes = await this.githubService.listarComponentes();
|
|
83
|
+
const selecionados = await this.promptUI.selecionarComponentes(componentes);
|
|
84
|
+
|
|
85
|
+
this.logger.destaque(`\n📥 Instalando ${selecionados.length} componente(s)...\n`);
|
|
86
|
+
|
|
87
|
+
// Baixa e distribui cada componente
|
|
88
|
+
for (const componente of selecionados) {
|
|
89
|
+
await this._baixarEDistribuir(componente.caminho, componente.nome);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Processo genérico: baixa do repositório e distribui nas pastas locais.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} caminhoRemoto - Caminho no repositório (ex: "components/whatsapp-btn")
|
|
97
|
+
* @param {string} nomePacote - Nome do pacote para exibição nos logs
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
async _baixarEDistribuir(caminhoRemoto, nomePacote) {
|
|
101
|
+
// Cria pasta temporária para o download
|
|
102
|
+
const pastaTmp = path.join(os.tmpdir(), `rook-cli-${Date.now()}`);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await fs.ensureDir(pastaTmp);
|
|
106
|
+
|
|
107
|
+
// Download via tiged
|
|
108
|
+
await this.downloadService.baixar(caminhoRemoto, pastaTmp);
|
|
109
|
+
|
|
110
|
+
// Distribui nas pastas Shopify locais
|
|
111
|
+
const diretorioAtual = process.cwd();
|
|
112
|
+
const totalCopiados = await this.fileMapper.distribuir(pastaTmp, diretorioAtual);
|
|
113
|
+
|
|
114
|
+
this.logger.sucesso(`\n🎉 "${nomePacote}" instalado! (${totalCopiados} arquivo(s) copiados)\n`);
|
|
115
|
+
|
|
116
|
+
} finally {
|
|
117
|
+
// Limpa pasta temporária
|
|
118
|
+
await fs.remove(pastaTmp);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigCommand — Comando "config" do CLI.
|
|
3
|
+
*
|
|
4
|
+
* Permite ao usuário configurar o token de acesso
|
|
5
|
+
* para repositórios privados do GitHub.
|
|
6
|
+
*
|
|
7
|
+
* Princípio: Responsabilidade Única (SRP) — só gerencia configuração
|
|
8
|
+
* Princípio: Inversão de Dependência (DIP) — recebe dependências via construtor
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { password, select } from '@inquirer/prompts';
|
|
12
|
+
|
|
13
|
+
export class ConfigCommand {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
17
|
+
* @param {import('../auth/TokenManager.js').TokenManager} tokenManager
|
|
18
|
+
*/
|
|
19
|
+
constructor(logger, tokenManager) {
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.tokenManager = tokenManager;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Executa o fluxo de configuração.
|
|
26
|
+
* @returns {Promise<void>}
|
|
27
|
+
*/
|
|
28
|
+
async executar() {
|
|
29
|
+
try {
|
|
30
|
+
const temToken = await this.tokenManager.temToken();
|
|
31
|
+
|
|
32
|
+
const acao = await select({
|
|
33
|
+
message: '⚙️ O que deseja configurar?',
|
|
34
|
+
choices: [
|
|
35
|
+
{
|
|
36
|
+
name: temToken
|
|
37
|
+
? '🔑 Atualizar token do GitHub (já configurado ✔)'
|
|
38
|
+
: '🔑 Configurar token do GitHub (necessário para repos privados)',
|
|
39
|
+
value: 'token',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: '🗑️ Remover token salvo',
|
|
43
|
+
value: 'remover',
|
|
44
|
+
disabled: !temToken ? '(nenhum token salvo)' : false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: '📋 Verificar status da configuração',
|
|
48
|
+
value: 'status',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
switch (acao) {
|
|
54
|
+
case 'token':
|
|
55
|
+
await this._configurarToken();
|
|
56
|
+
break;
|
|
57
|
+
case 'remover':
|
|
58
|
+
await this.tokenManager.removerToken();
|
|
59
|
+
break;
|
|
60
|
+
case 'status':
|
|
61
|
+
await this._exibirStatus();
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
} catch (erro) {
|
|
66
|
+
if (erro.name === 'ExitPromptError') {
|
|
67
|
+
this.logger.aviso('Configuração cancelada.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.logger.erro(`Erro na configuração: ${erro.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Solicita e salva o token do GitHub.
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
async _configurarToken() {
|
|
79
|
+
this.logger.info('Para acessar repositórios privados, você precisa de um Personal Access Token (PAT).');
|
|
80
|
+
this.logger.sutil('Crie um em: https://github.com/settings/tokens');
|
|
81
|
+
this.logger.sutil('Permissões necessárias: repo (acesso completo a repos privados)\n');
|
|
82
|
+
|
|
83
|
+
const token = await password({
|
|
84
|
+
message: '🔑 Cole seu GitHub Personal Access Token:',
|
|
85
|
+
mask: '•',
|
|
86
|
+
validate: (valor) => {
|
|
87
|
+
if (!valor || valor.trim().length === 0) {
|
|
88
|
+
return 'O token não pode ser vazio.';
|
|
89
|
+
}
|
|
90
|
+
if (valor.trim().length < 10) {
|
|
91
|
+
return 'Token parece inválido (muito curto).';
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await this.tokenManager.salvarToken(token.trim());
|
|
98
|
+
this.logger.sucesso('Token configurado com sucesso! Agora você pode acessar repositórios privados. 🔓');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Exibe o status atual da configuração.
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
async _exibirStatus() {
|
|
106
|
+
const temToken = await this.tokenManager.temToken();
|
|
107
|
+
|
|
108
|
+
console.log('');
|
|
109
|
+
this.logger.destaque('📊 Status da configuração:');
|
|
110
|
+
console.log('');
|
|
111
|
+
|
|
112
|
+
if (temToken) {
|
|
113
|
+
this.logger.sucesso('Token GitHub: Configurado ✔');
|
|
114
|
+
|
|
115
|
+
// Indica a origem do token
|
|
116
|
+
if (process.env.GITHUB_TOKEN) {
|
|
117
|
+
this.logger.sutil('Origem: variável de ambiente GITHUB_TOKEN');
|
|
118
|
+
} else {
|
|
119
|
+
this.logger.sutil('Origem: arquivo ~/.rookrc');
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
this.logger.aviso('Token GitHub: Não configurado ✖');
|
|
123
|
+
this.logger.sutil('Execute "rook config" para configurar.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constantes globais do projeto.
|
|
3
|
+
*
|
|
4
|
+
* Centraliza todas as configurações fixas do CLI,
|
|
5
|
+
* como URLs do repositório e diretórios válidos do Shopify.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Repositório de origem no GitHub (owner/repo) */
|
|
9
|
+
export const GITHUB_REPO = process.env.GITHUB_REPO;
|
|
10
|
+
|
|
11
|
+
/** URL base da API do GitHub para conteúdo do repositório */
|
|
12
|
+
export const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_REPO}/contents`;
|
|
13
|
+
|
|
14
|
+
/** URL base para download via tiged */
|
|
15
|
+
export const TIGED_BASE = `github:${GITHUB_REPO}`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Diretórios padrão da estrutura de um tema Shopify.
|
|
19
|
+
* Usados para mapear os arquivos do repositório remoto
|
|
20
|
+
* para as pastas corretas no tema local.
|
|
21
|
+
*/
|
|
22
|
+
export const SHOPIFY_DIRS = [
|
|
23
|
+
'assets',
|
|
24
|
+
'blocks',
|
|
25
|
+
'config',
|
|
26
|
+
'layout',
|
|
27
|
+
'locales',
|
|
28
|
+
'sections',
|
|
29
|
+
'snippets',
|
|
30
|
+
'templates',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Pastas raiz do repositório remoto */
|
|
34
|
+
export const REMOTE_PATHS = {
|
|
35
|
+
KITS: 'kits',
|
|
36
|
+
COMPONENTS: 'components',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Nome do CLI exibido nos logs */
|
|
40
|
+
export const CLI_NAME = 'rook';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConflictResolver — Tratamento de conflitos de arquivos.
|
|
3
|
+
*
|
|
4
|
+
* Quando um arquivo de destino já existe, este módulo
|
|
5
|
+
* solicita confirmação do usuário antes de sobrescrever.
|
|
6
|
+
* Implementa o RF04 do PRD.
|
|
7
|
+
*
|
|
8
|
+
* Princípio: Responsabilidade Única (SRP) — só trata conflitos
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { confirm } from '@inquirer/prompts';
|
|
12
|
+
|
|
13
|
+
export class ConflictResolver {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
17
|
+
*/
|
|
18
|
+
constructor(logger) {
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cache de decisão do usuário para "aplicar a todos".
|
|
23
|
+
* null = não decidido, true = sobrescrever tudo, false = pular tudo
|
|
24
|
+
* @type {boolean|null}
|
|
25
|
+
*/
|
|
26
|
+
this._decisaoGlobal = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Pergunta ao usuário se deseja sobrescrever o arquivo existente.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} nomeArquivo - Nome do arquivo em conflito
|
|
33
|
+
* @returns {Promise<boolean>} true para sobrescrever, false para pular
|
|
34
|
+
*/
|
|
35
|
+
async resolver(nomeArquivo) {
|
|
36
|
+
// Se já tem uma decisão global, usa ela
|
|
37
|
+
if (this._decisaoGlobal !== null) {
|
|
38
|
+
if (!this._decisaoGlobal) {
|
|
39
|
+
this.logger.aviso(` ⏩ Pulando: ${nomeArquivo} (decisão global)`);
|
|
40
|
+
}
|
|
41
|
+
return this._decisaoGlobal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.logger.aviso(`Arquivo já existe: ${nomeArquivo}`);
|
|
45
|
+
|
|
46
|
+
const sobrescrever = await confirm({
|
|
47
|
+
message: `Sobrescrever "${nomeArquivo}"?`,
|
|
48
|
+
default: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return sobrescrever;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Define uma decisão global para todos os conflitos restantes.
|
|
56
|
+
* Útil para opção "sobrescrever todos" ou "pular todos".
|
|
57
|
+
*
|
|
58
|
+
* @param {boolean} decisao - true para sobrescrever, false para pular
|
|
59
|
+
*/
|
|
60
|
+
definirDecisaoGlobal(decisao) {
|
|
61
|
+
this._decisaoGlobal = decisao;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reseta a decisão global (para novo ciclo de instalação).
|
|
66
|
+
*/
|
|
67
|
+
resetar() {
|
|
68
|
+
this._decisaoGlobal = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileMapper — Mapeamento e distribuição de arquivos.
|
|
3
|
+
*
|
|
4
|
+
* Responsável por identificar a estrutura de arquivos baixados
|
|
5
|
+
* e distribuí-los nas pastas corretas do tema Shopify local,
|
|
6
|
+
* conforme o padrão de diretórios definido no RF02.
|
|
7
|
+
*
|
|
8
|
+
* Princípio: Responsabilidade Única (SRP) — só mapeia e move
|
|
9
|
+
* Princípio: Inversão de Dependência (DIP) — recebe dependências via construtor
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import { SHOPIFY_DIRS } from '../config/constants.js';
|
|
15
|
+
|
|
16
|
+
export class FileMapper {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
20
|
+
* @param {import('./ConflictResolver.js').ConflictResolver} conflictResolver
|
|
21
|
+
*/
|
|
22
|
+
constructor(logger, conflictResolver) {
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
this.conflictResolver = conflictResolver;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Distribui os arquivos de uma pasta temporária para o diretório
|
|
29
|
+
* de trabalho atual, respeitando a estrutura Shopify.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} pastaOrigem - Caminho da pasta temporária com os arquivos baixados
|
|
32
|
+
* @param {string} pastaDestino - Diretório raiz do tema Shopify local (cwd)
|
|
33
|
+
* @returns {Promise<number>} Número de arquivos copiados
|
|
34
|
+
*/
|
|
35
|
+
async distribuir(pastaOrigem, pastaDestino) {
|
|
36
|
+
let totalCopiados = 0;
|
|
37
|
+
|
|
38
|
+
// Itera sobre cada diretório padrão do Shopify
|
|
39
|
+
for (const dir of SHOPIFY_DIRS) {
|
|
40
|
+
const origemDir = path.join(pastaOrigem, dir);
|
|
41
|
+
|
|
42
|
+
// Verifica se o diretório existe no conteúdo baixado
|
|
43
|
+
if (!await fs.pathExists(origemDir)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const destinoDir = path.join(pastaDestino, dir);
|
|
48
|
+
|
|
49
|
+
// Garante que o diretório de destino existe
|
|
50
|
+
await fs.ensureDir(destinoDir);
|
|
51
|
+
|
|
52
|
+
// Lista os arquivos dentro do diretório
|
|
53
|
+
const arquivos = await this._listarArquivosRecursivo(origemDir);
|
|
54
|
+
|
|
55
|
+
for (const arquivoRelativo of arquivos) {
|
|
56
|
+
const arquivoOrigem = path.join(origemDir, arquivoRelativo);
|
|
57
|
+
const arquivoDestino = path.join(destinoDir, arquivoRelativo);
|
|
58
|
+
|
|
59
|
+
// Verifica conflito antes de copiar
|
|
60
|
+
const deveCopiar = await this._verificarConflito(arquivoDestino, arquivoRelativo);
|
|
61
|
+
|
|
62
|
+
if (deveCopiar) {
|
|
63
|
+
await fs.ensureDir(path.dirname(arquivoDestino));
|
|
64
|
+
await fs.copy(arquivoOrigem, arquivoDestino);
|
|
65
|
+
this.logger.sucesso(` → ${dir}/${arquivoRelativo}`);
|
|
66
|
+
totalCopiados++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return totalCopiados;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lista todos os arquivos de um diretório de forma recursiva.
|
|
76
|
+
* Retorna caminhos relativos ao diretório base.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} diretorio - Caminho absoluto do diretório
|
|
79
|
+
* @param {string} [base=''] - Caminho base para relativizar
|
|
80
|
+
* @returns {Promise<string[]>} Lista de caminhos relativos dos arquivos
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
async _listarArquivosRecursivo(diretorio, base = '') {
|
|
84
|
+
const itens = await fs.readdir(diretorio, { withFileTypes: true });
|
|
85
|
+
let arquivos = [];
|
|
86
|
+
|
|
87
|
+
for (const item of itens) {
|
|
88
|
+
const caminhoRelativo = base ? path.join(base, item.name) : item.name;
|
|
89
|
+
|
|
90
|
+
if (item.isDirectory()) {
|
|
91
|
+
// Recursa em subdiretórios
|
|
92
|
+
const subArquivos = await this._listarArquivosRecursivo(
|
|
93
|
+
path.join(diretorio, item.name),
|
|
94
|
+
caminhoRelativo
|
|
95
|
+
);
|
|
96
|
+
arquivos = arquivos.concat(subArquivos);
|
|
97
|
+
} else {
|
|
98
|
+
arquivos.push(caminhoRelativo);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return arquivos;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Verifica se existe conflito e delega ao ConflictResolver.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} caminhoDestino - Caminho absoluto do arquivo de destino
|
|
109
|
+
* @param {string} nomeArquivo - Nome do arquivo para exibição
|
|
110
|
+
* @returns {Promise<boolean>} true se deve copiar, false se cancelado
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
async _verificarConflito(caminhoDestino, nomeArquivo) {
|
|
114
|
+
const existe = await fs.pathExists(caminhoDestino);
|
|
115
|
+
|
|
116
|
+
if (!existe) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Delega ao ConflictResolver (RF04)
|
|
121
|
+
return this.conflictResolver.resolver(nomeArquivo);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DownloadService — Motor de download de componentes.
|
|
3
|
+
*
|
|
4
|
+
* Suporta dois modos de download:
|
|
5
|
+
* - Repositórios públicos: usa tiged (rápido, sem .git)
|
|
6
|
+
* - Repositórios privados: usa GitHub API (tarball autenticado)
|
|
7
|
+
*
|
|
8
|
+
* Princípio: Responsabilidade Única (SRP) — só faz download
|
|
9
|
+
* Princípio: Inversão de Dependência (DIP) — recebe dependências via construtor
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import tiged from 'tiged';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import { pipeline } from 'stream/promises';
|
|
16
|
+
import { createWriteStream } from 'fs';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import { TIGED_BASE, GITHUB_REPO } from '../config/constants.js';
|
|
19
|
+
|
|
20
|
+
export class DownloadService {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('../utils/logger.js').Logger} logger - Instância do logger
|
|
24
|
+
* @param {import('../auth/TokenManager.js').TokenManager} tokenManager - Gerenciador de tokens
|
|
25
|
+
*/
|
|
26
|
+
constructor(logger, tokenManager) {
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.tokenManager = tokenManager;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Realiza o download de um subdiretório do repositório.
|
|
33
|
+
* Escolhe automaticamente o método baseado na disponibilidade do token.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} caminhoRemoto - Subcaminho no repositório (ex: "kits/base")
|
|
36
|
+
* @param {string} destinoLocal - Caminho absoluto para a pasta de destino
|
|
37
|
+
* @returns {Promise<void>}
|
|
38
|
+
*/
|
|
39
|
+
async baixar(caminhoRemoto, destinoLocal) {
|
|
40
|
+
const temToken = await this.tokenManager.temToken();
|
|
41
|
+
|
|
42
|
+
if (temToken) {
|
|
43
|
+
// Repositório privado: download via GitHub API
|
|
44
|
+
await this._baixarViaAPI(caminhoRemoto, destinoLocal);
|
|
45
|
+
} else {
|
|
46
|
+
// Repositório público: download via tiged (mais rápido)
|
|
47
|
+
await this._baixarViaTiged(caminhoRemoto, destinoLocal);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Download via GitHub API — funciona com repos privados.
|
|
53
|
+
* Baixa o tarball do repositório e extrai apenas o subdiretório desejado.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} caminhoRemoto - Subcaminho no repositório
|
|
56
|
+
* @param {string} destinoLocal - Pasta de destino
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
async _baixarViaAPI(caminhoRemoto, destinoLocal) {
|
|
60
|
+
this.logger.info(`Baixando (modo autenticado): ${caminhoRemoto}...`);
|
|
61
|
+
|
|
62
|
+
const headers = await this.tokenManager.obterHeaders();
|
|
63
|
+
const tarballUrl = `https://api.github.com/repos/${GITHUB_REPO}/tarball`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const resposta = await fetch(tarballUrl, {
|
|
67
|
+
headers,
|
|
68
|
+
redirect: 'follow',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!resposta.ok) {
|
|
72
|
+
throw new Error(`Erro ao baixar tarball: HTTP ${resposta.status}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Salva o tarball em arquivo temporário
|
|
76
|
+
const tarballPath = path.join(destinoLocal, '..', `rook-download-${Date.now()}.tar.gz`);
|
|
77
|
+
const extractDir = path.join(destinoLocal, '..', `rook-extract-${Date.now()}`);
|
|
78
|
+
|
|
79
|
+
await fs.ensureDir(extractDir);
|
|
80
|
+
|
|
81
|
+
// Escreve o tarball no disco
|
|
82
|
+
const fileStream = createWriteStream(tarballPath);
|
|
83
|
+
await pipeline(resposta.body, fileStream);
|
|
84
|
+
|
|
85
|
+
// Extrai o tarball
|
|
86
|
+
execSync(`tar -xzf "${tarballPath}" -C "${extractDir}"`, { stdio: 'pipe' });
|
|
87
|
+
|
|
88
|
+
// O tarball do GitHub cria uma pasta com o nome do repo + hash
|
|
89
|
+
// Precisamos encontrar essa pasta raiz
|
|
90
|
+
const extraidos = await fs.readdir(extractDir);
|
|
91
|
+
const pastaRaiz = path.join(extractDir, extraidos[0]);
|
|
92
|
+
|
|
93
|
+
// Copia apenas o subdiretório desejado para o destino
|
|
94
|
+
const pastaDesejada = path.join(pastaRaiz, caminhoRemoto);
|
|
95
|
+
|
|
96
|
+
if (!await fs.pathExists(pastaDesejada)) {
|
|
97
|
+
throw new Error(`Caminho "${caminhoRemoto}" não encontrado no repositório.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await fs.copy(pastaDesejada, destinoLocal);
|
|
101
|
+
|
|
102
|
+
// Limpa arquivos temporários
|
|
103
|
+
await fs.remove(tarballPath);
|
|
104
|
+
await fs.remove(extractDir);
|
|
105
|
+
|
|
106
|
+
this.logger.sucesso(`Download concluído: ${caminhoRemoto}`);
|
|
107
|
+
|
|
108
|
+
} catch (erro) {
|
|
109
|
+
this.logger.erro(`Falha no download de "${caminhoRemoto}": ${erro.message}`);
|
|
110
|
+
throw erro;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Download via tiged — funciona apenas com repos públicos.
|
|
116
|
+
* Mais rápido pois baixa diretamente o subdiretório.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} caminhoRemoto - Subcaminho no repositório
|
|
119
|
+
* @param {string} destinoLocal - Pasta de destino
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
async _baixarViaTiged(caminhoRemoto, destinoLocal) {
|
|
123
|
+
const origem = `${TIGED_BASE}/${caminhoRemoto}`;
|
|
124
|
+
|
|
125
|
+
this.logger.info(`Baixando: ${caminhoRemoto}...`);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const emitter = tiged(origem, {
|
|
129
|
+
disableCache: true,
|
|
130
|
+
force: true,
|
|
131
|
+
verbose: false,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Escuta eventos do tiged para feedback
|
|
135
|
+
emitter.on('info', (info) => {
|
|
136
|
+
this.logger.sutil(info.message);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
emitter.on('warn', (warning) => {
|
|
140
|
+
this.logger.aviso(warning.message);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await emitter.clone(destinoLocal);
|
|
144
|
+
|
|
145
|
+
this.logger.sucesso(`Download concluído: ${caminhoRemoto}`);
|
|
146
|
+
} catch (erro) {
|
|
147
|
+
this.logger.erro(`Falha no download de "${caminhoRemoto}": ${erro.message}`);
|
|
148
|
+
throw erro;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHubService — Comunicação com a API do GitHub.
|
|
3
|
+
*
|
|
4
|
+
* Responsável por listar kits e componentes disponíveis
|
|
5
|
+
* no repositório remoto. Suporta repositórios públicos e privados
|
|
6
|
+
* através do TokenManager para autenticação.
|
|
7
|
+
*
|
|
8
|
+
* Princípio: Responsabilidade Única (SRP)
|
|
9
|
+
* Princípio: Inversão de Dependência (DIP) — recebe logger e tokenManager via construtor
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { GITHUB_API_BASE, REMOTE_PATHS } from '../config/constants.js';
|
|
13
|
+
|
|
14
|
+
export class GitHubService {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('../utils/logger.js').Logger} logger - Instância do logger
|
|
18
|
+
* @param {import('../auth/TokenManager.js').TokenManager} tokenManager - Gerenciador de tokens
|
|
19
|
+
*/
|
|
20
|
+
constructor(logger, tokenManager) {
|
|
21
|
+
this.logger = logger;
|
|
22
|
+
this.tokenManager = tokenManager;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Busca o conteúdo de um diretório no repositório GitHub.
|
|
27
|
+
* Usa autenticação se houver token configurado.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} caminho - Caminho relativo no repositório
|
|
30
|
+
* @returns {Promise<Array>} Lista de itens no diretório
|
|
31
|
+
*/
|
|
32
|
+
async listarDiretorio(caminho) {
|
|
33
|
+
const url = `${GITHUB_API_BASE}/${caminho}`;
|
|
34
|
+
const headers = await this.tokenManager.obterHeaders();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const resposta = await fetch(url, { headers });
|
|
38
|
+
|
|
39
|
+
if (resposta.status === 401 || resposta.status === 403) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
'Acesso negado. Verifique se o token está correto.\n' +
|
|
42
|
+
' Execute "rook config" para reconfigurar.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (resposta.status === 404) {
|
|
47
|
+
const temToken = await this.tokenManager.temToken();
|
|
48
|
+
const dica = temToken
|
|
49
|
+
? 'Verifique se o caminho existe no repositório.'
|
|
50
|
+
: 'Se o repositório é privado, execute "rook config" para configurar o token.';
|
|
51
|
+
throw new Error(`Repositório ou caminho não encontrado. ${dica}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!resposta.ok) {
|
|
55
|
+
throw new Error(`Erro HTTP ${resposta.status}: ${resposta.statusText}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const dados = await resposta.json();
|
|
59
|
+
return dados;
|
|
60
|
+
} catch (erro) {
|
|
61
|
+
this.logger.erro(`Falha ao acessar o repositório: ${erro.message}`);
|
|
62
|
+
throw erro;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Lista os kits disponíveis no repositório.
|
|
68
|
+
* @returns {Promise<Array<{nome: string, caminho: string}>>} Lista de kits
|
|
69
|
+
*/
|
|
70
|
+
async listarKits() {
|
|
71
|
+
this.logger.info('Buscando kits disponíveis...');
|
|
72
|
+
|
|
73
|
+
const itens = await this.listarDiretorio(REMOTE_PATHS.KITS);
|
|
74
|
+
|
|
75
|
+
return itens
|
|
76
|
+
.filter(item => item.type === 'dir')
|
|
77
|
+
.map(item => ({
|
|
78
|
+
nome: item.name,
|
|
79
|
+
caminho: `${REMOTE_PATHS.KITS}/${item.name}`,
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Lista os componentes individuais disponíveis no repositório.
|
|
85
|
+
* @returns {Promise<Array<{nome: string, caminho: string}>>} Lista de componentes
|
|
86
|
+
*/
|
|
87
|
+
async listarComponentes() {
|
|
88
|
+
this.logger.info('Buscando componentes disponíveis...');
|
|
89
|
+
|
|
90
|
+
const itens = await this.listarDiretorio(REMOTE_PATHS.COMPONENTS);
|
|
91
|
+
|
|
92
|
+
return itens
|
|
93
|
+
.filter(item => item.type === 'dir')
|
|
94
|
+
.map(item => ({
|
|
95
|
+
nome: item.name,
|
|
96
|
+
caminho: `${REMOTE_PATHS.COMPONENTS}/${item.name}`,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PromptUI — Interface interativa do terminal.
|
|
3
|
+
*
|
|
4
|
+
* Gerencia todos os menus e prompts de seleção
|
|
5
|
+
* apresentados ao usuário via @inquirer/prompts.
|
|
6
|
+
*
|
|
7
|
+
* Princípio: Responsabilidade Única (SRP) — lida apenas com UI
|
|
8
|
+
* Princípio: Aberto/Fechado (OCP) — fácil adicionar novos menus
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { select, checkbox } from '@inquirer/prompts';
|
|
12
|
+
|
|
13
|
+
export class PromptUI {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Exibe o menu principal com as opções de instalação.
|
|
17
|
+
*
|
|
18
|
+
* @returns {Promise<string>} Opção selecionada ('kit' ou 'componente')
|
|
19
|
+
*/
|
|
20
|
+
async menuPrincipal() {
|
|
21
|
+
const escolha = await select({
|
|
22
|
+
message: 'O que você deseja instalar?',
|
|
23
|
+
choices: [
|
|
24
|
+
{
|
|
25
|
+
name: '🎯 Kit Base — Estrutura completa para iniciar projetos',
|
|
26
|
+
value: 'kit',
|
|
27
|
+
description: 'Instala todos os arquivos base de um kit',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: '🧩 Componente Individual — Selecione itens específicos',
|
|
31
|
+
value: 'componente',
|
|
32
|
+
description: 'Escolha componentes como botões, menus, etc.',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return escolha;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Exibe a lista de kits disponíveis para seleção.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array<{nome: string, caminho: string}>} kits - Kits disponíveis
|
|
44
|
+
* @returns {Promise<{nome: string, caminho: string}>} Kit selecionado
|
|
45
|
+
*/
|
|
46
|
+
async selecionarKit(kits) {
|
|
47
|
+
if (kits.length === 0) {
|
|
48
|
+
throw new Error('Nenhum kit disponível no repositório.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const caminho = await select({
|
|
52
|
+
message: '🎯 Selecione o kit para instalar:',
|
|
53
|
+
choices: kits.map(kit => ({
|
|
54
|
+
name: `📁 ${kit.nome}`,
|
|
55
|
+
value: kit.caminho,
|
|
56
|
+
})),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return kits.find(k => k.caminho === caminho);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Exibe a lista de componentes para seleção múltipla (checkbox).
|
|
64
|
+
*
|
|
65
|
+
* @param {Array<{nome: string, caminho: string}>} componentes - Componentes disponíveis
|
|
66
|
+
* @returns {Promise<Array<{nome: string, caminho: string}>>} Componentes selecionados
|
|
67
|
+
*/
|
|
68
|
+
async selecionarComponentes(componentes) {
|
|
69
|
+
if (componentes.length === 0) {
|
|
70
|
+
throw new Error('Nenhum componente disponível no repositório.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const selecionados = await checkbox({
|
|
74
|
+
message: '🧩 Selecione os componentes desejados (espaço para marcar):',
|
|
75
|
+
choices: componentes.map(comp => ({
|
|
76
|
+
name: `📦 ${comp.nome}`,
|
|
77
|
+
value: comp.caminho,
|
|
78
|
+
})),
|
|
79
|
+
required: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return componentes.filter(c => selecionados.includes(c.caminho));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger — Responsável pela saída visual no terminal.
|
|
3
|
+
*
|
|
4
|
+
* Encapsula o uso do picocolors para fornecer
|
|
5
|
+
* métodos semânticos de log (sucesso, erro, info, etc).
|
|
6
|
+
*
|
|
7
|
+
* Princípio: Responsabilidade Única (SRP)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import pc from 'picocolors';
|
|
11
|
+
|
|
12
|
+
export class Logger {
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Exibe uma mensagem de informação (azul).
|
|
16
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
17
|
+
*/
|
|
18
|
+
info(mensagem) {
|
|
19
|
+
console.log(pc.cyan(`ℹ ${mensagem}`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Exibe uma mensagem de sucesso (verde).
|
|
24
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
25
|
+
*/
|
|
26
|
+
sucesso(mensagem) {
|
|
27
|
+
console.log(pc.green(`✔ ${mensagem}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Exibe uma mensagem de aviso (amarelo).
|
|
32
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
33
|
+
*/
|
|
34
|
+
aviso(mensagem) {
|
|
35
|
+
console.log(pc.yellow(`⚠ ${mensagem}`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Exibe uma mensagem de erro (vermelho).
|
|
40
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
41
|
+
*/
|
|
42
|
+
erro(mensagem) {
|
|
43
|
+
console.error(pc.red(`✖ ${mensagem}`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exibe o banner/header do CLI.
|
|
48
|
+
*/
|
|
49
|
+
banner() {
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(pc.bold(pc.magenta(' ╔══════════════════════════════╗')));
|
|
52
|
+
console.log(pc.bold(pc.magenta(' ║ 🚀 ROOK CLI v1.0.0 ║')));
|
|
53
|
+
console.log(pc.bold(pc.magenta(' ║ Shopify Component Tool ║')));
|
|
54
|
+
console.log(pc.bold(pc.magenta(' ╚══════════════════════════════╝')));
|
|
55
|
+
console.log('');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Exibe texto em destaque (negrito).
|
|
60
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
61
|
+
*/
|
|
62
|
+
destaque(mensagem) {
|
|
63
|
+
console.log(pc.bold(mensagem));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Exibe texto esmaecido (para detalhes secundários).
|
|
68
|
+
* @param {string} mensagem - Texto a ser exibido
|
|
69
|
+
*/
|
|
70
|
+
sutil(mensagem) {
|
|
71
|
+
console.log(pc.dim(` ${mensagem}`));
|
|
72
|
+
}
|
|
73
|
+
}
|