mozhost-cli 2.5.0 → 2.6.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/bin/mozhost.js CHANGED
@@ -8,6 +8,7 @@ const deployCommands = require('../src/commands/deploy');
8
8
  const domainCommands = require('../src/commands/domains');
9
9
  const databaseCommands = require('../src/commands/databases'); // 👈 NOVO
10
10
  const terminalCommands = require('../src/commands/terminal');
11
+ const gitCommands = require('../src/commands/git');
11
12
  const packageJson = require('../package.json');
12
13
 
13
14
  program
@@ -45,8 +46,8 @@ program
45
46
  program
46
47
  .command('create')
47
48
  .description('Criar novo container')
48
- .requiredOption('-n, --name <name>', 'Nome do container')
49
- .requiredOption('-t, --type <type>', 'Tipo (nodejs, python, php)')
49
+ .option('-n, --name <name>', 'Nome do container')
50
+ .option('-t, --type <type>', 'Tipo: bot-baileys, bot-whatsapp-web-js, nodejs, python, php')
50
51
  .action(containerCommands.create);
51
52
 
52
53
  program
@@ -186,6 +187,40 @@ program
186
187
  .action(terminalCommands.exec);
187
188
 
188
189
 
190
+ // ============================================
191
+ // GIT / GITHUB COMMANDS
192
+ // ============================================
193
+ program
194
+ .command('git:auth')
195
+ .description('Conectar conta GitHub')
196
+ .action(gitCommands.auth);
197
+
198
+ program
199
+ .command('git:status')
200
+ .description('Ver status da conexão GitHub')
201
+ .action(gitCommands.status);
202
+
203
+ program
204
+ .command('git:repos')
205
+ .alias('git:ls')
206
+ .description('Listar repositórios GitHub')
207
+ .action(gitCommands.repos);
208
+
209
+ program
210
+ .command('git:connect [container]')
211
+ .description('Conectar repositório a um container')
212
+ .action(gitCommands.connect);
213
+
214
+ program
215
+ .command('git:deploys <container>')
216
+ .description('Ver histórico de deploys via Git')
217
+ .action(gitCommands.deploys);
218
+
219
+ program
220
+ .command('git:disconnect')
221
+ .description('Desconectar conta GitHub')
222
+ .action(gitCommands.disconnect);
223
+
189
224
  // ============================================
190
225
  // PARSE & HELP
191
226
  // ============================================
@@ -206,5 +241,9 @@ if (!process.argv.slice(2).length) {
206
241
  console.log(chalk.white(' mozhost domain:list # Listar domínios'));
207
242
  console.log(chalk.white(' mozhost domain:add bot exemplo.com # Adicionar domínio'));
208
243
  console.log(chalk.white(' mozhost domain:verify exemplo.com # Verificar domínio'));
244
+ console.log(chalk.white(' mozhost git:auth # Conectar GitHub'));
245
+ console.log(chalk.white(' mozhost git:repos # Listar repositórios'));
246
+ console.log(chalk.white(' mozhost git:connect # Conectar repo a container'));
247
+ console.log(chalk.white(' mozhost git:deploys <container> # Histórico de deploys'));
209
248
  console.log();
210
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mozhost-cli",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Command-line interface for MozHost container platform",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,21 +15,21 @@ async function resolveContainer(identifier) {
15
15
  // Se não encontrar (404), busca na lista por nome
16
16
  if (error.response?.status === 404 || error.response?.data?.error?.includes('not found')) {
17
17
  const listResponse = await api.listContainers();
18
-
18
+
19
19
  // Procura por nome, ID completo ou ID parcial
20
- const found = listResponse.containers.find(c =>
21
- c.name === identifier ||
20
+ const found = listResponse.containers.find(c =>
21
+ c.name === identifier ||
22
22
  c.id === identifier ||
23
23
  c.id.startsWith(identifier)
24
24
  );
25
-
25
+
26
26
  if (!found) {
27
27
  // Procura containers similares para sugerir
28
- const similar = listResponse.containers.filter(c =>
28
+ const similar = listResponse.containers.filter(c =>
29
29
  c.name.toLowerCase().includes(identifier.toLowerCase()) ||
30
30
  c.id.includes(identifier)
31
31
  );
32
-
32
+
33
33
  if (similar.length > 0) {
34
34
  console.log(chalk.yellow('\n💡 Você quis dizer:'));
35
35
  similar.forEach(c => {
@@ -38,10 +38,10 @@ async function resolveContainer(identifier) {
38
38
  } else {
39
39
  console.log(chalk.gray('\n💡 Use: mozhost ls para ver todos os containers'));
40
40
  }
41
-
41
+
42
42
  throw new Error(`Container "${identifier}" não encontrado`);
43
43
  }
44
-
44
+
45
45
  return found;
46
46
  }
47
47
  throw error;
@@ -61,7 +61,7 @@ async function list() {
61
61
 
62
62
  if (response.containers.length === 0) {
63
63
  console.log(chalk.yellow('\n⚠️ Nenhum container encontrado'));
64
- console.log(chalk.gray(' Use: mozhost create -n <nome> -t <tipo>'));
64
+ console.log(chalk.gray(' Use: mozhost create'));
65
65
  return;
66
66
  }
67
67
 
@@ -100,24 +100,159 @@ async function list() {
100
100
  }
101
101
 
102
102
  // ============================================
103
- // CREATE - Criar novo container
103
+ // CREATE - Criar novo container (INTERATIVO)
104
104
  // ============================================
105
105
  async function create(options) {
106
106
  try {
107
- const { name, type } = options;
107
+ let name = options.name;
108
+ let type = options.type;
109
+ let prefix = '!'; // Prefix padrão para bots
110
+
111
+ // ========================================
112
+ // MODO INTERATIVO (se não passar opções)
113
+ // ========================================
114
+ if (!name || !type) {
115
+ console.log(chalk.cyan.bold('\n🔨 Criar Novo Container\n'));
116
+
117
+ const answers = await inquirer.prompt([
118
+ // 1. Nome do container
119
+ {
120
+ type: 'input',
121
+ name: 'name',
122
+ message: 'Nome do container:',
123
+ when: !name,
124
+ validate: (input) => {
125
+ if (!input || input.trim().length === 0) {
126
+ return 'Nome não pode ser vazio';
127
+ }
128
+ if (!/^[a-z0-9-]+$/.test(input)) {
129
+ return 'Use apenas letras minúsculas, números e hífens';
130
+ }
131
+ if (input.length < 3) {
132
+ return 'Nome deve ter pelo menos 3 caracteres';
133
+ }
134
+ if (input.length > 30) {
135
+ return 'Nome não pode ter mais de 30 caracteres';
136
+ }
137
+ return true;
138
+ }
139
+ },
140
+
141
+ // 2. Categoria (Bot ou API)
142
+ {
143
+ type: 'list',
144
+ name: 'category',
145
+ message: 'O que você deseja criar?',
146
+ when: !type,
147
+ choices: [
148
+ { name: '🤖 Bot WhatsApp', value: 'bot' },
149
+ { name: '🌐 API / Aplicação Web', value: 'api' }
150
+ ]
151
+ },
152
+
153
+ // 3. Biblioteca do Bot (se escolheu Bot)
154
+ {
155
+ type: 'list',
156
+ name: 'botLibrary',
157
+ message: 'Escolha a biblioteca:',
158
+ when: (answers) => answers.category === 'bot',
159
+ choices: [
160
+ { name: 'Baileys (recomendado, mais estável)', value: 'baileys' },
161
+ { name: 'whatsapp-web.js (compatível com navegador)', value: 'whatsapp-web-js' }
162
+ ]
163
+ },
164
+
165
+ // 4. Prefix do Bot (se escolheu Bot)
166
+ {
167
+ type: 'input',
168
+ name: 'botPrefix',
169
+ message: 'Prefix dos comandos do bot:',
170
+ default: '!',
171
+ when: (answers) => answers.category === 'bot',
172
+ validate: (input) => {
173
+ if (!input || input.trim().length === 0) {
174
+ return 'Prefix não pode ser vazio';
175
+ }
176
+ if (input.length > 3) {
177
+ return 'Prefix deve ter no máximo 3 caracteres';
178
+ }
179
+ return true;
180
+ }
181
+ },
182
+
183
+ // 5. Linguagem da API (se escolheu API)
184
+ {
185
+ type: 'list',
186
+ name: 'apiLanguage',
187
+ message: 'Escolha a linguagem:',
188
+ when: (answers) => answers.category === 'api',
189
+ choices: [
190
+ { name: 'Node.js (JavaScript/TypeScript)', value: 'nodejs' },
191
+ { name: 'Python (Flask/Django/FastAPI)', value: 'python' },
192
+ { name: 'PHP (Laravel/Slim)', value: 'php' }
193
+ ]
194
+ }
195
+ ]);
108
196
 
109
- if (!['nodejs', 'python', 'php'].includes(type)) {
110
- console.error(chalk.red('❌ Tipo inválido. Use: nodejs, python ou php'));
197
+ // Atribuir valores
198
+ name = answers.name || name;
199
+
200
+ // Montar o tipo baseado nas escolhas
201
+ if (answers.category === 'bot') {
202
+ type = `bot-${answers.botLibrary}`;
203
+ prefix = answers.botPrefix || '!';
204
+ } else if (answers.category === 'api') {
205
+ type = answers.apiLanguage;
206
+ }
207
+ }
208
+
209
+ // ========================================
210
+ // VALIDAÇÃO FINAL
211
+ // ========================================
212
+ const validTypes = ['nodejs', 'python', 'php', 'bot-baileys', 'bot-whatsapp-web-js'];
213
+
214
+ if (!validTypes.includes(type)) {
215
+ console.error(chalk.red('\n❌ Tipo inválido.'));
216
+ console.error(chalk.yellow('\n Bots:'));
217
+ console.error(chalk.white(' • bot-baileys'));
218
+ console.error(chalk.white(' • bot-whatsapp-web-js'));
219
+ console.error(chalk.yellow('\n APIs:'));
220
+ console.error(chalk.white(' • nodejs'));
221
+ console.error(chalk.white(' • python'));
222
+ console.error(chalk.white(' • php\n'));
111
223
  process.exit(1);
112
224
  }
113
225
 
226
+ // Detectar se é bot
227
+ const isBot = type.startsWith('bot-');
228
+
229
+ // ========================================
230
+ // CRIAR CONTAINER
231
+ // ========================================
114
232
  console.log(chalk.cyan.bold('\n🔨 Criando container...\n'));
115
233
  console.log(chalk.gray(` Nome: ${name}`));
116
- console.log(chalk.gray(` Tipo: ${type}\n`));
234
+ console.log(chalk.gray(` Tipo: ${type}`));
235
+ if (isBot) {
236
+ console.log(chalk.gray(` Prefix: ${prefix}`));
237
+ }
238
+ console.log();
117
239
 
118
240
  const spinner = ora('Criando container...').start();
119
241
 
120
- const response = await api.createContainer({ name, type });
242
+ // Payload para criar container
243
+ const payload = {
244
+ name,
245
+ type
246
+ };
247
+
248
+ // Se for bot, adicionar variáveis de ambiente
249
+ if (isBot) {
250
+ payload.env = {
251
+ PREFIX: prefix
252
+ };
253
+ }
254
+
255
+ const response = await api.createContainer(payload);
121
256
 
122
257
  spinner.succeed(chalk.green('✅ Container criado com sucesso!'));
123
258
 
@@ -129,9 +264,25 @@ async function create(options) {
129
264
  console.log(chalk.cyan(` URL: https://${response.container.domain}`));
130
265
  console.log(chalk.white(` Porta: ${response.container.port}`));
131
266
 
267
+ // ========================================
268
+ // PRÓXIMOS PASSOS (diferente para Bot vs API)
269
+ // ========================================
132
270
  console.log(chalk.gray('\nPróximos passos:'));
133
- console.log(chalk.white(` 1. mozhost start ${response.container.name}`));
134
- console.log(chalk.white(` 2. mozhost deploy ${response.container.name}`));
271
+
272
+ if (isBot) {
273
+ console.log(chalk.white(` 1. mozhost start ${response.container.name}`));
274
+ console.log(chalk.white(` 2. Aguarde ~10 segundos para o bot inicializar`));
275
+ console.log(chalk.white(` 3. Acesse o painel web para escanear o QR Code:`));
276
+ console.log(chalk.cyan(` https://mozhost.topaziocoin.online/containers/${response.container.id}`));
277
+ console.log(chalk.gray(`\n 💡 Comandos do bot usarão o prefix: ${prefix}`));
278
+ console.log(chalk.gray(` Exemplo: ${prefix}ping`));
279
+ } else {
280
+ console.log(chalk.white(` 1. mozhost start ${response.container.name}`));
281
+ console.log(chalk.white(` 2. mozhost deploy ${response.container.name}`));
282
+ console.log(chalk.gray(`\n 💡 Ou use 'mozhost init' para inicializar um projeto local`));
283
+ }
284
+
285
+ console.log();
135
286
 
136
287
  } catch (error) {
137
288
  console.error(chalk.red('\n❌ Erro ao criar container:'));
@@ -161,7 +312,7 @@ async function start(identifier) {
161
312
  try {
162
313
  const spinner = ora('Resolvendo container...').start();
163
314
  const container = await resolveContainer(identifier);
164
-
315
+
165
316
  spinner.text = `Iniciando ${container.name}...`;
166
317
  const response = await api.startContainer(container.id);
167
318
 
@@ -183,7 +334,7 @@ async function stop(identifier) {
183
334
  try {
184
335
  const spinner = ora('Resolvendo container...').start();
185
336
  const container = await resolveContainer(identifier);
186
-
337
+
187
338
  spinner.text = `Parando ${container.name}...`;
188
339
  await api.stopContainer(container.id);
189
340
 
@@ -203,7 +354,7 @@ async function restart(identifier) {
203
354
  try {
204
355
  const spinner = ora('Resolvendo container...').start();
205
356
  const container = await resolveContainer(identifier);
206
-
357
+
207
358
  spinner.text = `Reiniciando ${container.name}...`;
208
359
  await api.restartContainer(container.id);
209
360
 
@@ -258,7 +409,7 @@ async function logs(identifier, options) {
258
409
  try {
259
410
  const spinner = ora('Resolvendo container...').start();
260
411
  const container = await resolveContainer(identifier);
261
-
412
+
262
413
  spinner.text = `Carregando logs de ${container.name}...`;
263
414
  const response = await api.getContainerLogs(container.id, options.lines);
264
415
 
@@ -288,9 +439,9 @@ async function info(identifier) {
288
439
  try {
289
440
  const spinner = ora('Resolvendo container...').start();
290
441
  const container = await resolveContainer(identifier);
291
-
442
+
292
443
  spinner.text = `Carregando informações de ${container.name}...`;
293
-
444
+
294
445
  // Busca detalhes completos (com stats se disponível)
295
446
  const response = await api.getContainer(container.id);
296
447
  spinner.stop();
@@ -0,0 +1,314 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const inquirer = require('inquirer');
4
+ const api = require('../utils/api');
5
+
6
+ // ============================================
7
+ // git:auth - Autenticar com GitHub via Device Flow
8
+ // ============================================
9
+ async function auth() {
10
+ try {
11
+ console.log(chalk.cyan.bold('\n🔑 Conectar GitHub\n'));
12
+
13
+ const spinner = ora('Iniciando autenticação...').start();
14
+ const response = await api.githubDeviceStart();
15
+ spinner.stop();
16
+
17
+ console.log(chalk.yellow.bold(`\n📋 Código: ${response.user_code}\n`));
18
+ console.log(chalk.white(`1. Acesse: ${chalk.cyan.underline(response.verification_uri)}`));
19
+ console.log(chalk.white(`2. Cole o código: ${chalk.yellow.bold(response.user_code)}`));
20
+ console.log(chalk.white('3. Autorize o acesso\n'));
21
+
22
+ const pollSpinner = ora('Aguardando autorização no navegador...').start();
23
+
24
+ const interval = (response.interval || 5) * 1000;
25
+ const maxAttempts = Math.floor((response.expires_in || 900) / (response.interval || 5));
26
+
27
+ for (let i = 0; i < maxAttempts; i++) {
28
+ await new Promise(resolve => setTimeout(resolve, interval));
29
+
30
+ try {
31
+ const pollResult = await api.githubDevicePoll(response.device_code);
32
+
33
+ if (pollResult.status === 'connected') {
34
+ pollSpinner.succeed(chalk.green(`✅ GitHub conectado como @${pollResult.github_username}`));
35
+ return;
36
+ }
37
+
38
+ if (pollResult.status === 'slow_down') {
39
+ await new Promise(resolve => setTimeout(resolve, 5000));
40
+ }
41
+ } catch (error) {
42
+ if (error.response?.status === 400) {
43
+ pollSpinner.fail(chalk.red('❌ Código expirado. Tente novamente.'));
44
+ return;
45
+ }
46
+ }
47
+ }
48
+
49
+ pollSpinner.fail(chalk.red('❌ Tempo esgotado. Tente novamente.'));
50
+
51
+ } catch (error) {
52
+ console.error(chalk.red('\n❌ Erro ao autenticar:'));
53
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ // ============================================
59
+ // git:status - Ver status da conexão GitHub
60
+ // ============================================
61
+ async function status() {
62
+ try {
63
+ const spinner = ora('Verificando conexão GitHub...').start();
64
+ const response = await api.githubStatus();
65
+ spinner.stop();
66
+
67
+ if (response.connected) {
68
+ console.log(chalk.green(`\n✅ GitHub conectado como @${response.github_username}\n`));
69
+ } else {
70
+ console.log(chalk.yellow('\n⚠️ GitHub não conectado'));
71
+ console.log(chalk.gray(' Use: mozhost git:auth\n'));
72
+ }
73
+
74
+ } catch (error) {
75
+ console.error(chalk.red('\n❌ Erro ao verificar status:'));
76
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ // ============================================
82
+ // git:repos - Listar repositórios do GitHub
83
+ // ============================================
84
+ async function repos() {
85
+ try {
86
+ const spinner = ora('Carregando repositórios...').start();
87
+ const response = await api.githubRepos();
88
+ spinner.stop();
89
+
90
+ if (!response.repos || response.repos.length === 0) {
91
+ console.log(chalk.yellow('\n⚠️ Nenhum repositório encontrado\n'));
92
+ return;
93
+ }
94
+
95
+ console.log(chalk.cyan.bold(`\n📦 Seus Repositórios (${response.repos.length})\n`));
96
+
97
+ for (const repo of response.repos) {
98
+ const visibility = repo.private ? chalk.red('🔒 privado') : chalk.green('🌍 público');
99
+ console.log(chalk.white(` ${chalk.bold(repo.full_name)} ${visibility}`));
100
+ console.log(chalk.gray(` Branch: ${repo.default_branch} | ${repo.url}`));
101
+ }
102
+
103
+ console.log();
104
+
105
+ } catch (error) {
106
+ if (error.response?.status === 404) {
107
+ console.log(chalk.yellow('\n⚠️ GitHub não conectado'));
108
+ console.log(chalk.gray(' Use: mozhost git:auth\n'));
109
+ return;
110
+ }
111
+ console.error(chalk.red('\n❌ Erro ao listar repositórios:'));
112
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ // ============================================
118
+ // git:connect <container> - Conectar repo a container
119
+ // ============================================
120
+ async function connect(containerId) {
121
+ try {
122
+ console.log(chalk.cyan.bold('\n🔗 Conectar Repositório ao Container\n'));
123
+
124
+ // Se não passou container, listar para selecionar
125
+ if (!containerId) {
126
+ const listSpinner = ora('Carregando containers...').start();
127
+ const containersRes = await api.listContainers();
128
+ listSpinner.stop();
129
+
130
+ if (!containersRes.containers || containersRes.containers.length === 0) {
131
+ console.log(chalk.red('\n❌ Nenhum container encontrado'));
132
+ return;
133
+ }
134
+
135
+ const { selectedContainer } = await inquirer.prompt([
136
+ {
137
+ type: 'list',
138
+ name: 'selectedContainer',
139
+ message: 'Selecione o container:',
140
+ choices: containersRes.containers.map(c => ({
141
+ name: `${c.name} (${c.type}) - ${c.status}`,
142
+ value: c.id
143
+ }))
144
+ }
145
+ ]);
146
+
147
+ containerId = selectedContainer;
148
+ }
149
+
150
+ // Listar repositórios
151
+ const repoSpinner = ora('Carregando repositórios...').start();
152
+ const reposRes = await api.githubRepos();
153
+ repoSpinner.stop();
154
+
155
+ if (!reposRes.repos || reposRes.repos.length === 0) {
156
+ console.log(chalk.red('\n❌ Nenhum repositório encontrado'));
157
+ console.log(chalk.gray(' Verifique se o GitHub está conectado: mozhost git:status'));
158
+ return;
159
+ }
160
+
161
+ // Selecionar repositório
162
+ const { selectedRepo } = await inquirer.prompt([
163
+ {
164
+ type: 'list',
165
+ name: 'selectedRepo',
166
+ message: 'Selecione o repositório:',
167
+ choices: reposRes.repos.map(r => ({
168
+ name: `${r.full_name} ${r.private ? '🔒' : '🌍'} (${r.default_branch})`,
169
+ value: r
170
+ }))
171
+ }
172
+ ]);
173
+
174
+ // Listar branches do repositório
175
+ const [owner, repo] = selectedRepo.full_name.split('/');
176
+ const branchSpinner = ora('Carregando branches...').start();
177
+ const branchesRes = await api.githubBranches(owner, repo);
178
+ branchSpinner.stop();
179
+
180
+ let branch = selectedRepo.default_branch;
181
+
182
+ if (branchesRes.branches && branchesRes.branches.length > 1) {
183
+ const { selectedBranch } = await inquirer.prompt([
184
+ {
185
+ type: 'list',
186
+ name: 'selectedBranch',
187
+ message: 'Selecione a branch:',
188
+ choices: branchesRes.branches.map(b => ({
189
+ name: `${b.name} ${b.name === selectedRepo.default_branch ? chalk.gray('(default)') : ''}`,
190
+ value: b.name
191
+ }))
192
+ }
193
+ ]);
194
+ branch = selectedBranch;
195
+ }
196
+
197
+ // Confirmar
198
+ const { confirm } = await inquirer.prompt([
199
+ {
200
+ type: 'confirm',
201
+ name: 'confirm',
202
+ message: `Conectar ${chalk.cyan(selectedRepo.full_name)}@${chalk.yellow(branch)} ao container?`,
203
+ default: true
204
+ }
205
+ ]);
206
+
207
+ if (!confirm) {
208
+ console.log(chalk.gray('\nOperação cancelada.'));
209
+ return;
210
+ }
211
+
212
+ // Conectar
213
+ const connectSpinner = ora('Conectando repositório e clonando código...').start();
214
+ const result = await api.githubConnect({
215
+ container_id: containerId,
216
+ repo_url: selectedRepo.url,
217
+ repo_name: selectedRepo.full_name,
218
+ branch
219
+ });
220
+ connectSpinner.succeed(chalk.green('✅ Repositório conectado com sucesso!'));
221
+
222
+ console.log(chalk.gray(`\n Repo: ${selectedRepo.full_name}`));
223
+ console.log(chalk.gray(` Branch: ${branch}`));
224
+ console.log(chalk.gray(` Webhook: ${result.webhook_registered ? '✅ Ativo' : '⚠️ Não registrado'}`));
225
+ console.log(chalk.cyan('\n💡 Agora cada push nessa branch fará deploy automático!\n'));
226
+
227
+ } catch (error) {
228
+ if (error.response?.status === 404) {
229
+ console.log(chalk.yellow('\n⚠️ GitHub não conectado'));
230
+ console.log(chalk.gray(' Use: mozhost git:auth\n'));
231
+ return;
232
+ }
233
+ console.error(chalk.red('\n❌ Erro ao conectar repositório:'));
234
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
235
+ process.exit(1);
236
+ }
237
+ }
238
+
239
+ // ============================================
240
+ // git:deploys <container> - Ver histórico de deploys
241
+ // ============================================
242
+ async function deploys(containerId) {
243
+ try {
244
+ const spinner = ora('Carregando histórico de deploys...').start();
245
+ const response = await api.githubDeploys(containerId);
246
+ spinner.stop();
247
+
248
+ if (!response.deploys || response.deploys.length === 0) {
249
+ console.log(chalk.yellow('\n⚠️ Nenhum deploy encontrado para este container\n'));
250
+ return;
251
+ }
252
+
253
+ console.log(chalk.cyan.bold(`\n📋 Histórico de Deploys (${response.deploys.length})\n`));
254
+
255
+ for (const deploy of response.deploys) {
256
+ const statusIcon = deploy.status === 'success' ? chalk.green('✅')
257
+ : deploy.status === 'failed' ? chalk.red('❌')
258
+ : chalk.yellow('⏳');
259
+
260
+ const sha = deploy.commit_sha ? deploy.commit_sha.substring(0, 7) : '-------';
261
+ const date = new Date(deploy.created_at).toLocaleString('pt-BR');
262
+
263
+ console.log(` ${statusIcon} ${chalk.gray(sha)} ${chalk.white(deploy.commit_message || 'Sem mensagem')}`);
264
+ console.log(chalk.gray(` ${date} | via ${deploy.triggered_by}`));
265
+ }
266
+
267
+ console.log();
268
+
269
+ } catch (error) {
270
+ console.error(chalk.red('\n❌ Erro ao buscar deploys:'));
271
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
272
+ process.exit(1);
273
+ }
274
+ }
275
+
276
+ // ============================================
277
+ // git:disconnect - Desconectar GitHub
278
+ // ============================================
279
+ async function disconnect() {
280
+ try {
281
+ const { confirm } = await inquirer.prompt([
282
+ {
283
+ type: 'confirm',
284
+ name: 'confirm',
285
+ message: chalk.red('Tem certeza que deseja desconectar o GitHub? Todos os webhooks serão removidos.'),
286
+ default: false
287
+ }
288
+ ]);
289
+
290
+ if (!confirm) {
291
+ console.log(chalk.gray('\nOperação cancelada.'));
292
+ return;
293
+ }
294
+
295
+ const spinner = ora('Desconectando GitHub...').start();
296
+ await api.githubDisconnect();
297
+ spinner.succeed(chalk.green('✅ GitHub desconectado com sucesso!'));
298
+ console.log();
299
+
300
+ } catch (error) {
301
+ console.error(chalk.red('\n❌ Erro ao desconectar:'));
302
+ console.error(chalk.red(` ${error.response?.data?.error || error.message}`));
303
+ process.exit(1);
304
+ }
305
+ }
306
+
307
+ module.exports = {
308
+ auth,
309
+ status,
310
+ repos,
311
+ connect,
312
+ deploys,
313
+ disconnect
314
+ };
package/src/utils/api.js CHANGED
@@ -232,6 +232,55 @@ class ApiClient {
232
232
  return response.data;
233
233
  }
234
234
 
235
+ // GitHub
236
+ async githubDeviceStart() {
237
+ const client = await this.getClient();
238
+ const response = await client.post('/api/github/device/start');
239
+ return response.data;
240
+ }
241
+
242
+ async githubDevicePoll(deviceCode) {
243
+ const client = await this.getClient();
244
+ const response = await client.post('/api/github/device/poll', { device_code: deviceCode });
245
+ return response.data;
246
+ }
247
+
248
+ async githubStatus() {
249
+ const client = await this.getClient();
250
+ const response = await client.get('/api/github/status');
251
+ return response.data;
252
+ }
253
+
254
+ async githubRepos() {
255
+ const client = await this.getClient();
256
+ const response = await client.get('/api/github/repos');
257
+ return response.data;
258
+ }
259
+
260
+ async githubBranches(owner, repo) {
261
+ const client = await this.getClient();
262
+ const response = await client.get(`/api/github/repos/${owner}/${repo}/branches`);
263
+ return response.data;
264
+ }
265
+
266
+ async githubConnect(data) {
267
+ const client = await this.getClient();
268
+ const response = await client.post('/api/github/connect', data);
269
+ return response.data;
270
+ }
271
+
272
+ async githubDeploys(containerId) {
273
+ const client = await this.getClient();
274
+ const response = await client.get(`/api/github/deploys/${containerId}`);
275
+ return response.data;
276
+ }
277
+
278
+ async githubDisconnect() {
279
+ const client = await this.getClient();
280
+ const response = await client.delete('/api/github/disconnect');
281
+ return response.data;
282
+ }
283
+
235
284
  // Terminal
236
285
  async executeCommand(containerId, command) {
237
286
  const client = await this.getClient();