@asaro/git-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 +189 -0
- package/dist/adapters/git/SimpleGitAdapter.js +126 -0
- package/dist/adapters/github/GitHubAPIAdapter.js +243 -0
- package/dist/adapters/storage/FileSecureStorageAdapter.js +67 -0
- package/dist/adapters/ui/ConsoleUIAdapter.js +91 -0
- package/dist/commands/BaseCommand.js +11 -0
- package/dist/commands/BranchesCommand.js +152 -0
- package/dist/commands/Command.js +1 -0
- package/dist/commands/CommitCommand.js +85 -0
- package/dist/commands/IntegrationCommand.js +151 -0
- package/dist/commands/LogCommand.js +42 -0
- package/dist/commands/PullCommand.js +55 -0
- package/dist/commands/PushCommand.js +227 -0
- package/dist/commands/RepositoryCommand.js +268 -0
- package/dist/commands/StashCommand.js +140 -0
- package/dist/commands/StatusCommand.js +66 -0
- package/dist/commands/github/BaseGitHubCommand.js +17 -0
- package/dist/commands/github/GitHubAuthCommand.js +48 -0
- package/dist/commands/github/GitHubMenuCommand.js +57 -0
- package/dist/commands/github/GitHubReposCommand.js +210 -0
- package/dist/commands/github/GitHubWorkflowsCommand.js +243 -0
- package/dist/commit.js +98 -0
- package/dist/core/errors/GitError.js +19 -0
- package/dist/core/errors/GitHubError.js +36 -0
- package/dist/core/ports/GitHubPort.js +1 -0
- package/dist/core/ports/GitPort.js +1 -0
- package/dist/core/ports/SecureStoragePort.js +1 -0
- package/dist/core/ports/UIPort.js +1 -0
- package/dist/core/services/GitHubService.js +95 -0
- package/dist/core/services/GitService.js +44 -0
- package/dist/estado.js +97 -0
- package/dist/factories/CommandFactory.js +51 -0
- package/dist/index.js +71 -0
- package/dist/integracion.js +152 -0
- package/dist/ramas.js +178 -0
- package/dist/remoto.js +129 -0
- package/dist/repositorio.js +159 -0
- package/dist/stash.js +169 -0
- package/dist/utils.js +49 -0
- package/package.json +53 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { BaseCommand } from './BaseCommand.js';
|
|
2
|
+
export class StatusCommand extends BaseCommand {
|
|
3
|
+
async execute() {
|
|
4
|
+
try {
|
|
5
|
+
const isRepo = await this.gitPort.isRepository();
|
|
6
|
+
if (!isRepo) {
|
|
7
|
+
this.uiPort.error(' ✗ Este directorio no es un repositorio Git.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const spin = this.uiPort.spinner('Obteniendo estado del repositorio...');
|
|
11
|
+
const status = await this.gitPort.getStatus();
|
|
12
|
+
spin.stop();
|
|
13
|
+
const c = this.uiColors;
|
|
14
|
+
const rama = status.current ?? 'desconocida';
|
|
15
|
+
this.uiPort.log('');
|
|
16
|
+
this.uiPort.log(c.titulo(' 📋 Estado del repositorio'));
|
|
17
|
+
this.uiPort.separator();
|
|
18
|
+
this.uiPort.log(` Rama actual: ${c.rama(rama)}`);
|
|
19
|
+
if (status.ahead > 0 && status.behind > 0) {
|
|
20
|
+
this.uiPort.log(c.aviso(` ⚡ ${status.ahead} commit(s) adelante y ${status.behind} atrás del remoto`));
|
|
21
|
+
}
|
|
22
|
+
else if (status.ahead > 0) {
|
|
23
|
+
this.uiPort.log(c.aviso(` ↑ ${status.ahead} commit(s) por subir`));
|
|
24
|
+
}
|
|
25
|
+
else if (status.behind > 0) {
|
|
26
|
+
this.uiPort.log(c.aviso(` ↓ ${status.behind} commit(s) por bajar`));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.uiPort.log(c.exito(' ✓ Sincronizado con el remoto'));
|
|
30
|
+
}
|
|
31
|
+
this.uiPort.separator();
|
|
32
|
+
if (status.staged.length > 0) {
|
|
33
|
+
this.uiPort.log(c.exito(' Preparados para commit (staged):'));
|
|
34
|
+
status.staged.forEach((f) => this.uiPort.log(c.exito(` + ${f}`)));
|
|
35
|
+
}
|
|
36
|
+
if (status.modified.length > 0) {
|
|
37
|
+
this.uiPort.log(c.aviso(' Modificados (sin stagear):'));
|
|
38
|
+
status.modified.forEach((f) => this.uiPort.log(c.aviso(` ~ ${f}`)));
|
|
39
|
+
}
|
|
40
|
+
if (status.not_added.length > 0) {
|
|
41
|
+
this.uiPort.log(c.tenue(' Sin seguimiento (untracked):'));
|
|
42
|
+
status.not_added.forEach((f) => this.uiPort.log(c.tenue(` ? ${f}`)));
|
|
43
|
+
}
|
|
44
|
+
if (status.deleted.length > 0) {
|
|
45
|
+
this.uiPort.log(c.error(' Eliminados:'));
|
|
46
|
+
status.deleted.forEach((f) => this.uiPort.log(c.error(` - ${f}`)));
|
|
47
|
+
}
|
|
48
|
+
if (status.conflicted.length > 0) {
|
|
49
|
+
this.uiPort.log(c.error(' Con conflictos:'));
|
|
50
|
+
status.conflicted.forEach((f) => this.uiPort.log(c.error(` ✗ ${f}`)));
|
|
51
|
+
}
|
|
52
|
+
const total = status.staged.length +
|
|
53
|
+
status.modified.length +
|
|
54
|
+
status.not_added.length +
|
|
55
|
+
status.deleted.length;
|
|
56
|
+
if (total === 0) {
|
|
57
|
+
this.uiPort.log(c.tenue(' Árbol de trabajo limpio.'));
|
|
58
|
+
}
|
|
59
|
+
this.uiPort.separator();
|
|
60
|
+
this.uiPort.log('');
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
this.uiPort.error(` ✗ Error: ${err.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseCommand } from '../BaseCommand.js';
|
|
2
|
+
export class BaseGitHubCommand extends BaseCommand {
|
|
3
|
+
gitHubService;
|
|
4
|
+
constructor(gitHubService, gitPort, uiPort) {
|
|
5
|
+
super(gitPort, uiPort);
|
|
6
|
+
this.gitHubService = gitHubService;
|
|
7
|
+
}
|
|
8
|
+
async ensureAuthenticated() {
|
|
9
|
+
const isAuth = await this.gitHubService.isAuthenticated();
|
|
10
|
+
if (!isAuth) {
|
|
11
|
+
this.uiPort.error(' ✗ No estás autenticado con GitHub');
|
|
12
|
+
this.uiPort.info(' Usa la opción de inicio de sesión primero');
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseGitHubCommand } from './BaseGitHubCommand.js';
|
|
2
|
+
export class GitHubAuthCommand extends BaseGitHubCommand {
|
|
3
|
+
async execute() {
|
|
4
|
+
try {
|
|
5
|
+
const isAuth = await this.gitHubService.isAuthenticated();
|
|
6
|
+
const c = this.uiColors;
|
|
7
|
+
if (isAuth) {
|
|
8
|
+
const user = await this.gitHubService.getCurrentUser();
|
|
9
|
+
this.uiPort.log('');
|
|
10
|
+
this.uiPort.log(c.info(` Autenticado como: ${user.name || user.login}`));
|
|
11
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
12
|
+
{ name: 'Cerrar sesión', value: 'logout' },
|
|
13
|
+
{ name: 'Volver', value: 'back' },
|
|
14
|
+
]);
|
|
15
|
+
if (action === 'logout') {
|
|
16
|
+
await this.gitHubService.logout();
|
|
17
|
+
this.uiPort.success(' ✓ Sesión cerrada correctamente');
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Not authenticated - show login flow
|
|
22
|
+
this.uiPort.log('');
|
|
23
|
+
this.uiPort.log(c.titulo(' 🔐 Inicio de sesión GitHub'));
|
|
24
|
+
this.uiPort.separator();
|
|
25
|
+
this.uiPort.log(c.info(' Necesitas un Personal Access Token (PAT) de GitHub'));
|
|
26
|
+
this.uiPort.log(c.tenue(' Permisos necesarios: repo, workflow, delete_repo'));
|
|
27
|
+
this.uiPort.log(c.tenue(' Genera uno aquí: https://github.com/settings/tokens/new'));
|
|
28
|
+
this.uiPort.separator();
|
|
29
|
+
const token = await this.uiPort.input('Ingresa tu PAT de GitHub:', {
|
|
30
|
+
validate: (val) => val.trim().length > 0 || 'El token no puede estar vacío',
|
|
31
|
+
});
|
|
32
|
+
const spin = this.uiPort.spinner('Validando token...');
|
|
33
|
+
try {
|
|
34
|
+
await this.gitHubService.login(token.trim());
|
|
35
|
+
spin.stop();
|
|
36
|
+
const user = await this.gitHubService.getCurrentUser();
|
|
37
|
+
this.uiPort.success(` ✓ Autenticado correctamente como ${user.name || user.login}`);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
spin.stop();
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.uiPort.error(` ✗ ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseGitHubCommand } from './BaseGitHubCommand.js';
|
|
2
|
+
import { GitHubAuthCommand } from './GitHubAuthCommand.js';
|
|
3
|
+
import { GitHubReposCommand } from './GitHubReposCommand.js';
|
|
4
|
+
import { GitHubWorkflowsCommand } from './GitHubWorkflowsCommand.js';
|
|
5
|
+
export class GitHubMenuCommand extends BaseGitHubCommand {
|
|
6
|
+
authCommand;
|
|
7
|
+
reposCommand;
|
|
8
|
+
workflowsCommand;
|
|
9
|
+
constructor(gitHubService, gitPort, uiPort) {
|
|
10
|
+
super(gitHubService, gitPort, uiPort);
|
|
11
|
+
this.authCommand = new GitHubAuthCommand(gitHubService, gitPort, uiPort);
|
|
12
|
+
this.reposCommand = new GitHubReposCommand(gitHubService, gitPort, uiPort);
|
|
13
|
+
this.workflowsCommand = new GitHubWorkflowsCommand(gitHubService, gitPort, uiPort);
|
|
14
|
+
}
|
|
15
|
+
async execute() {
|
|
16
|
+
try {
|
|
17
|
+
const c = this.uiColors;
|
|
18
|
+
let continuar = true;
|
|
19
|
+
while (continuar) {
|
|
20
|
+
const isAuth = await this.gitHubService.isAuthenticated();
|
|
21
|
+
this.uiPort.log('');
|
|
22
|
+
this.uiPort.log(c.titulo(' 🌐 Menú GitHub'));
|
|
23
|
+
this.uiPort.separator();
|
|
24
|
+
if (isAuth) {
|
|
25
|
+
const user = await this.gitHubService.getCurrentUser();
|
|
26
|
+
this.uiPort.log(c.exito(` ✅ Autenticado como: ${user.name || user.login}`));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.uiPort.log(c.aviso(' ⚠️ No autenticado'));
|
|
30
|
+
}
|
|
31
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
32
|
+
{ name: 'Autenticación (Login/Logout)', value: 'auth' },
|
|
33
|
+
{ name: 'Gestión de repositorios', value: 'repos' },
|
|
34
|
+
{ name: 'Gestión de GitHub Actions', value: 'workflows' },
|
|
35
|
+
{ name: 'Volver al menú principal', value: 'back' },
|
|
36
|
+
]);
|
|
37
|
+
switch (action) {
|
|
38
|
+
case 'auth':
|
|
39
|
+
await this.authCommand.execute();
|
|
40
|
+
break;
|
|
41
|
+
case 'repos':
|
|
42
|
+
await this.reposCommand.execute();
|
|
43
|
+
break;
|
|
44
|
+
case 'workflows':
|
|
45
|
+
await this.workflowsCommand.execute();
|
|
46
|
+
break;
|
|
47
|
+
case 'back':
|
|
48
|
+
continuar = false;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
this.uiPort.error(` ✗ ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { BaseGitHubCommand } from './BaseGitHubCommand.js';
|
|
2
|
+
import { simpleGit } from 'simple-git';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export class GitHubReposCommand extends BaseGitHubCommand {
|
|
5
|
+
async execute() {
|
|
6
|
+
if (!(await this.ensureAuthenticated()))
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
const c = this.uiColors;
|
|
10
|
+
let continuar = true;
|
|
11
|
+
while (continuar) {
|
|
12
|
+
this.uiPort.log('');
|
|
13
|
+
this.uiPort.log(c.titulo(' 📦 Gestión de repositorios GitHub'));
|
|
14
|
+
this.uiPort.separator();
|
|
15
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
16
|
+
{ name: 'Listar mis repositorios', value: 'list' },
|
|
17
|
+
{ name: 'Crear nuevo repositorio', value: 'create' },
|
|
18
|
+
{ name: 'Archivar/Desarchivar repositorio', value: 'archive' },
|
|
19
|
+
{ name: 'Clonar repositorio', value: 'clone' },
|
|
20
|
+
{ name: 'Volver', value: 'back' },
|
|
21
|
+
]);
|
|
22
|
+
switch (action) {
|
|
23
|
+
case 'list':
|
|
24
|
+
await this.listRepos();
|
|
25
|
+
break;
|
|
26
|
+
case 'create':
|
|
27
|
+
await this.createRepo();
|
|
28
|
+
break;
|
|
29
|
+
case 'archive':
|
|
30
|
+
await this.archiveRepo();
|
|
31
|
+
break;
|
|
32
|
+
case 'clone':
|
|
33
|
+
await this.cloneRepo();
|
|
34
|
+
break;
|
|
35
|
+
case 'back':
|
|
36
|
+
continuar = false;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
if (continuar && action !== 'back') {
|
|
40
|
+
await this.uiPort.pause();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.uiPort.error(` ✗ ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async listRepos() {
|
|
49
|
+
const c = this.uiColors;
|
|
50
|
+
this.uiPort.log('');
|
|
51
|
+
// Ask for filters
|
|
52
|
+
const useFilters = await this.uiPort.confirm('¿Deseas aplicar filtros?', false);
|
|
53
|
+
let filters = {};
|
|
54
|
+
if (useFilters) {
|
|
55
|
+
const visibility = await this.uiPort.select('Visibilidad:', [
|
|
56
|
+
{ name: 'Todos', value: 'all' },
|
|
57
|
+
{ name: 'Públicos', value: 'public' },
|
|
58
|
+
{ name: 'Privados', value: 'private' },
|
|
59
|
+
]);
|
|
60
|
+
if (visibility !== 'all')
|
|
61
|
+
filters.visibility = visibility;
|
|
62
|
+
const language = await this.uiPort.input('Filtrar por lenguaje (deja vacío para no filtrar):');
|
|
63
|
+
if (language.trim())
|
|
64
|
+
filters.language = language.trim();
|
|
65
|
+
const sinceDays = await this.uiPort.input('Mostrar repos actualizados en los últimos X días (deja vacío):');
|
|
66
|
+
if (sinceDays.trim()) {
|
|
67
|
+
const days = parseInt(sinceDays.trim());
|
|
68
|
+
if (!isNaN(days)) {
|
|
69
|
+
const date = new Date();
|
|
70
|
+
date.setDate(date.getDate() - days);
|
|
71
|
+
filters.since = date.toISOString();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const spin = this.uiPort.spinner('Cargando repositorios...');
|
|
76
|
+
const repos = await this.gitHubService.listRepos(Object.keys(filters).length > 0 ? filters : undefined);
|
|
77
|
+
spin.stop();
|
|
78
|
+
this.uiPort.log('');
|
|
79
|
+
this.uiPort.log(c.titulo(` 📋 ${repos.length} repositorios encontrados`));
|
|
80
|
+
this.uiPort.separator();
|
|
81
|
+
repos.forEach((repo, index) => {
|
|
82
|
+
const archivedBadge = repo.archived ? c.tenue(' [ARCHIVADO]') : '';
|
|
83
|
+
this.uiPort.log(` ${c.info(`${index + 1}.`)} ${c.titulo(repo.fullName)}${archivedBadge}`);
|
|
84
|
+
if (repo.description) {
|
|
85
|
+
this.uiPort.log(c.tenue(` ${repo.description}`));
|
|
86
|
+
}
|
|
87
|
+
this.uiPort.log(c.tenue(` ⭐ ${repo.stars} | ${repo.language || 'N/A'} | Última actualización: ${repo.updatedAt ? new Date(repo.updatedAt).toLocaleDateString('es-CL') : 'N/A'}`));
|
|
88
|
+
this.uiPort.log('');
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async createRepo() {
|
|
92
|
+
const c = this.uiColors;
|
|
93
|
+
this.uiPort.log('');
|
|
94
|
+
this.uiPort.log(c.titulo(' ➕ Crear nuevo repositorio'));
|
|
95
|
+
this.uiPort.separator();
|
|
96
|
+
const name = await this.uiPort.input('Nombre del repositorio:', {
|
|
97
|
+
validate: (val) => val.trim().length > 0 || 'El nombre es obligatorio',
|
|
98
|
+
});
|
|
99
|
+
const description = await this.uiPort.input('Descripción (opcional):');
|
|
100
|
+
const visibility = await this.uiPort.select('Visibilidad:', [
|
|
101
|
+
{ name: 'Público', value: 'public' },
|
|
102
|
+
{ name: 'Privado', value: 'private' },
|
|
103
|
+
]);
|
|
104
|
+
const autoInit = await this.uiPort.confirm('¿Inicializar con README?', true);
|
|
105
|
+
let gitignoreTemplate;
|
|
106
|
+
if (autoInit) {
|
|
107
|
+
const gitignore = await this.uiPort.input('Plantilla .gitignore (ej: Node, Python, dejar vacío para ninguna):');
|
|
108
|
+
if (gitignore.trim())
|
|
109
|
+
gitignoreTemplate = gitignore.trim();
|
|
110
|
+
}
|
|
111
|
+
let licenseTemplate;
|
|
112
|
+
if (autoInit) {
|
|
113
|
+
const license = await this.uiPort.input('Licencia (ej: mit, apache-2.0, dejar vacío para ninguna):');
|
|
114
|
+
if (license.trim())
|
|
115
|
+
licenseTemplate = license.trim();
|
|
116
|
+
}
|
|
117
|
+
const confirm = await this.uiPort.confirm(`¿Crear repositorio ${name}?`, true);
|
|
118
|
+
if (!confirm) {
|
|
119
|
+
this.uiPort.warning(' Operación cancelada');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const spin = this.uiPort.spinner('Creando repositorio...');
|
|
123
|
+
const repo = await this.gitHubService.createRepo({
|
|
124
|
+
name: name.trim(),
|
|
125
|
+
description: description.trim() || undefined,
|
|
126
|
+
visibility,
|
|
127
|
+
autoInit,
|
|
128
|
+
gitignoreTemplate,
|
|
129
|
+
licenseTemplate,
|
|
130
|
+
});
|
|
131
|
+
spin.stop();
|
|
132
|
+
this.uiPort.success(` ✓ Repositorio creado exitosamente!`);
|
|
133
|
+
this.uiPort.log(` 🔗 URL: ${c.info(repo.htmlUrl)}`);
|
|
134
|
+
this.uiPort.log(` 📥 HTTPS: ${c.info(repo.cloneUrl)}`);
|
|
135
|
+
this.uiPort.log(` 📥 SSH: ${c.info(repo.sshUrl)}`);
|
|
136
|
+
}
|
|
137
|
+
async archiveRepo() {
|
|
138
|
+
const c = this.uiColors;
|
|
139
|
+
this.uiPort.log('');
|
|
140
|
+
const spin = this.uiPort.spinner('Cargando repositorios...');
|
|
141
|
+
const repos = await this.gitHubService.listRepos();
|
|
142
|
+
spin.stop();
|
|
143
|
+
if (repos.length === 0) {
|
|
144
|
+
this.uiPort.warning(' No tienes repositorios');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const repoChoices = repos.map((r) => ({
|
|
148
|
+
name: `${r.fullName} ${r.archived ? c.tenue('[ARCHIVADO]') : ''}`,
|
|
149
|
+
value: r.fullName,
|
|
150
|
+
}));
|
|
151
|
+
const selectedRepo = await this.uiPort.select('Selecciona un repositorio:', repoChoices);
|
|
152
|
+
const [owner, repoName] = selectedRepo.split('/');
|
|
153
|
+
const repo = repos.find((r) => r.fullName === selectedRepo);
|
|
154
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
155
|
+
{ name: repo.archived ? 'Desarchivar repositorio' : 'Archivar repositorio', value: repo.archived ? 'unarchive' : 'archive' },
|
|
156
|
+
{ name: 'Volver', value: 'back' },
|
|
157
|
+
]);
|
|
158
|
+
if (action === 'back')
|
|
159
|
+
return;
|
|
160
|
+
const confirmMsg = action === 'archive'
|
|
161
|
+
? `¿Seguro que quieres ARCHIVAR ${repo.fullName}?`
|
|
162
|
+
: `¿Seguro que quieres DESARCHIVAR ${repo.fullName}?`;
|
|
163
|
+
const confirm = await this.uiPort.confirm(confirmMsg, false);
|
|
164
|
+
if (!confirm) {
|
|
165
|
+
this.uiPort.warning(' Operación cancelada');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const spin2 = this.uiPort.spinner(action === 'archive' ? 'Archivando repositorio...' : 'Desarchivando repositorio...');
|
|
169
|
+
if (action === 'archive') {
|
|
170
|
+
await this.gitHubService.archiveRepo(owner, repoName);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
await this.gitHubService.unarchiveRepo(owner, repoName);
|
|
174
|
+
}
|
|
175
|
+
spin2.stop();
|
|
176
|
+
this.uiPort.success(` ✓ Repositorio ${action === 'archive' ? 'archivado' : 'desarchivado'} correctamente`);
|
|
177
|
+
}
|
|
178
|
+
async cloneRepo() {
|
|
179
|
+
const c = this.uiColors;
|
|
180
|
+
this.uiPort.log('');
|
|
181
|
+
const spin = this.uiPort.spinner('Cargando repositorios...');
|
|
182
|
+
const repos = await this.gitHubService.listRepos();
|
|
183
|
+
spin.stop();
|
|
184
|
+
if (repos.length === 0) {
|
|
185
|
+
this.uiPort.warning(' No tienes repositorios');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const repoChoices = repos.map((r) => ({ name: r.fullName, value: r }));
|
|
189
|
+
const selectedRepo = await this.uiPort.select('Selecciona un repositorio:', repoChoices);
|
|
190
|
+
const cloneMethod = await this.uiPort.select('Método de clonación:', [
|
|
191
|
+
{ name: 'HTTPS', value: 'https' },
|
|
192
|
+
{ name: 'SSH', value: 'ssh' },
|
|
193
|
+
]);
|
|
194
|
+
const cloneUrl = cloneMethod === 'https' ? selectedRepo.cloneUrl : selectedRepo.sshUrl;
|
|
195
|
+
const defaultDest = selectedRepo.name;
|
|
196
|
+
const dest = await this.uiPort.input('Carpeta de destino:', { default: defaultDest });
|
|
197
|
+
const spinClone = this.uiPort.spinner('Clonando repositorio...');
|
|
198
|
+
try {
|
|
199
|
+
const git = simpleGit(process.cwd());
|
|
200
|
+
await git.clone(cloneUrl, dest.trim());
|
|
201
|
+
spinClone.stop();
|
|
202
|
+
const destPath = path.join(process.cwd(), dest.trim());
|
|
203
|
+
this.uiPort.success(` ✓ Repositorio clonado en: ${c.info(destPath)}`);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
spinClone.stop();
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { BaseGitHubCommand } from './BaseGitHubCommand.js';
|
|
2
|
+
export class GitHubWorkflowsCommand extends BaseGitHubCommand {
|
|
3
|
+
async execute() {
|
|
4
|
+
if (!(await this.ensureAuthenticated()))
|
|
5
|
+
return;
|
|
6
|
+
try {
|
|
7
|
+
const c = this.uiColors;
|
|
8
|
+
let continuar = true;
|
|
9
|
+
while (continuar) {
|
|
10
|
+
this.uiPort.log('');
|
|
11
|
+
this.uiPort.log(c.titulo(' ⚙️ Gestión de GitHub Actions'));
|
|
12
|
+
this.uiPort.separator();
|
|
13
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
14
|
+
{ name: 'Ver workflows de un repositorio', value: 'list' },
|
|
15
|
+
{ name: 'Ver runs de un workflow', value: 'runs' },
|
|
16
|
+
{ name: 'Habilitar/Deshabilitar workflow', value: 'toggle' },
|
|
17
|
+
{ name: 'Disparar workflow manualmente', value: 'dispatch' },
|
|
18
|
+
{ name: 'Volver', value: 'back' },
|
|
19
|
+
]);
|
|
20
|
+
switch (action) {
|
|
21
|
+
case 'list':
|
|
22
|
+
await this.listWorkflows();
|
|
23
|
+
break;
|
|
24
|
+
case 'runs':
|
|
25
|
+
await this.listWorkflowRuns();
|
|
26
|
+
break;
|
|
27
|
+
case 'toggle':
|
|
28
|
+
await this.toggleWorkflow();
|
|
29
|
+
break;
|
|
30
|
+
case 'dispatch':
|
|
31
|
+
await this.dispatchWorkflow();
|
|
32
|
+
break;
|
|
33
|
+
case 'back':
|
|
34
|
+
continuar = false;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (continuar && action !== 'back') {
|
|
38
|
+
await this.uiPort.pause();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
this.uiPort.error(` ✗ ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async selectRepo() {
|
|
47
|
+
const spin = this.uiPort.spinner('Cargando repositorios...');
|
|
48
|
+
const repos = await this.gitHubService.listRepos();
|
|
49
|
+
spin.stop();
|
|
50
|
+
if (repos.length === 0) {
|
|
51
|
+
this.uiPort.warning(' No tienes repositorios');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const repoChoices = repos.map((r) => ({ name: r.fullName, value: r.fullName }));
|
|
55
|
+
const selectedRepo = await this.uiPort.select('Selecciona un repositorio:', repoChoices);
|
|
56
|
+
const [owner, repo] = selectedRepo.split('/');
|
|
57
|
+
return { owner, repo };
|
|
58
|
+
}
|
|
59
|
+
async listWorkflows() {
|
|
60
|
+
const c = this.uiColors;
|
|
61
|
+
const repo = await this.selectRepo();
|
|
62
|
+
if (!repo)
|
|
63
|
+
return;
|
|
64
|
+
const spin = this.uiPort.spinner('Cargando workflows...');
|
|
65
|
+
const workflows = await this.gitHubService.listWorkflows(repo.owner, repo.repo);
|
|
66
|
+
spin.stop();
|
|
67
|
+
if (workflows.length === 0) {
|
|
68
|
+
this.uiPort.warning(' Este repositorio no tiene workflows');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.uiPort.log('');
|
|
72
|
+
this.uiPort.log(c.titulo(` 📋 ${workflows.length} workflows encontrados`));
|
|
73
|
+
this.uiPort.separator();
|
|
74
|
+
workflows.forEach((workflow, index) => {
|
|
75
|
+
const stateBadge = this.getStateBadge(workflow.state);
|
|
76
|
+
this.uiPort.log(` ${c.info(`${index + 1}.`)} ${c.titulo(workflow.name)} ${stateBadge}`);
|
|
77
|
+
this.uiPort.log(c.tenue(` Archivo: ${workflow.path}`));
|
|
78
|
+
this.uiPort.log(c.tenue(` Última actualización: ${new Date(workflow.updatedAt).toLocaleDateString('es-CL')}`));
|
|
79
|
+
this.uiPort.log(c.tenue(` 🔗 URL: ${workflow.htmlUrl}`));
|
|
80
|
+
this.uiPort.log('');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async listWorkflowRuns() {
|
|
84
|
+
const repo = await this.selectRepo();
|
|
85
|
+
if (!repo)
|
|
86
|
+
return;
|
|
87
|
+
const c = this.uiColors;
|
|
88
|
+
const spin = this.uiPort.spinner('Cargando workflows...');
|
|
89
|
+
const workflows = await this.gitHubService.listWorkflows(repo.owner, repo.repo);
|
|
90
|
+
spin.stop();
|
|
91
|
+
if (workflows.length === 0) {
|
|
92
|
+
this.uiPort.warning(' Este repositorio no tiene workflows');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const workflowChoices = workflows.map((w) => ({ name: w.name, value: w }));
|
|
96
|
+
const selectedWorkflow = await this.uiPort.select('Selecciona un workflow:', workflowChoices);
|
|
97
|
+
const useFilters = await this.uiPort.confirm('¿Deseas aplicar filtros?', false);
|
|
98
|
+
let filters = {};
|
|
99
|
+
if (useFilters) {
|
|
100
|
+
const status = await this.uiPort.select('Estado:', [
|
|
101
|
+
{ name: 'Todos', value: '' },
|
|
102
|
+
{ name: 'Éxito', value: 'success' },
|
|
103
|
+
{ name: 'Fallido', value: 'failure' },
|
|
104
|
+
{ name: 'En progreso', value: 'in_progress' },
|
|
105
|
+
{ name: 'Cancelado', value: 'cancelled' },
|
|
106
|
+
]);
|
|
107
|
+
if (status)
|
|
108
|
+
filters.status = status;
|
|
109
|
+
const branch = await this.uiPort.input('Rama (deja vacío):');
|
|
110
|
+
if (branch.trim())
|
|
111
|
+
filters.branch = branch.trim();
|
|
112
|
+
}
|
|
113
|
+
const spinRuns = this.uiPort.spinner('Cargando runs...');
|
|
114
|
+
const runs = await this.gitHubService.listWorkflowRuns(repo.owner, repo.repo, Object.keys(filters).length > 0 ? filters : undefined);
|
|
115
|
+
spinRuns.stop();
|
|
116
|
+
if (runs.length === 0) {
|
|
117
|
+
this.uiPort.warning(' No hay runs para este workflow');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.uiPort.log('');
|
|
121
|
+
this.uiPort.log(c.titulo(` 📋 ${runs.length} runs encontrados`));
|
|
122
|
+
this.uiPort.separator();
|
|
123
|
+
runs.forEach((run, index) => {
|
|
124
|
+
const conclusionBadge = this.getConclusionBadge(run.status, run.conclusion);
|
|
125
|
+
this.uiPort.log(` ${c.info(`${index + 1}.`)} ${c.titulo(run.name || 'Unnamed workflow')} ${conclusionBadge}`);
|
|
126
|
+
this.uiPort.log(c.tenue(` Rama: ${run.headBranch || 'N/A'} | SHA: ${run.headSha.substring(0, 7)}`));
|
|
127
|
+
if (run.actor) {
|
|
128
|
+
this.uiPort.log(c.tenue(` Autor: ${run.actor.login}`));
|
|
129
|
+
}
|
|
130
|
+
this.uiPort.log(c.tenue(` Creado: ${new Date(run.createdAt).toLocaleDateString('es-CL')}`));
|
|
131
|
+
this.uiPort.log(c.tenue(` 🔗 URL: ${run.htmlUrl}`));
|
|
132
|
+
this.uiPort.log('');
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async toggleWorkflow() {
|
|
136
|
+
const repo = await this.selectRepo();
|
|
137
|
+
if (!repo)
|
|
138
|
+
return;
|
|
139
|
+
const spin = this.uiPort.spinner('Cargando workflows...');
|
|
140
|
+
const workflows = await this.gitHubService.listWorkflows(repo.owner, repo.repo);
|
|
141
|
+
spin.stop();
|
|
142
|
+
if (workflows.length === 0) {
|
|
143
|
+
this.uiPort.warning(' Este repositorio no tiene workflows');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const workflowChoices = workflows.map((w) => ({
|
|
147
|
+
name: `${w.name} ${this.getStateBadge(w.state)}`,
|
|
148
|
+
value: w,
|
|
149
|
+
}));
|
|
150
|
+
const selectedWorkflow = await this.uiPort.select('Selecciona un workflow:', workflowChoices);
|
|
151
|
+
const isEnabled = selectedWorkflow.state === 'active';
|
|
152
|
+
const action = await this.uiPort.select('¿Qué deseas hacer?', [
|
|
153
|
+
{ name: isEnabled ? 'Deshabilitar workflow' : 'Habilitar workflow', value: isEnabled ? 'disable' : 'enable' },
|
|
154
|
+
{ name: 'Volver', value: 'back' },
|
|
155
|
+
]);
|
|
156
|
+
if (action === 'back')
|
|
157
|
+
return;
|
|
158
|
+
const confirmMsg = action === 'enable'
|
|
159
|
+
? `¿Seguro que quieres HABILITAR ${selectedWorkflow.name}?`
|
|
160
|
+
: `¿Seguro que quieres DESHABILITAR ${selectedWorkflow.name}?`;
|
|
161
|
+
const confirm = await this.uiPort.confirm(confirmMsg, false);
|
|
162
|
+
if (!confirm) {
|
|
163
|
+
this.uiPort.warning(' Operación cancelada');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const spin2 = this.uiPort.spinner(action === 'enable' ? 'Habilitando workflow...' : 'Deshabilitando workflow...');
|
|
167
|
+
if (action === 'enable') {
|
|
168
|
+
await this.gitHubService.enableWorkflow(repo.owner, repo.repo, selectedWorkflow.id);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
await this.gitHubService.disableWorkflow(repo.owner, repo.repo, selectedWorkflow.id);
|
|
172
|
+
}
|
|
173
|
+
spin2.stop();
|
|
174
|
+
this.uiPort.success(` ✓ Workflow ${action === 'enable' ? 'habilitado' : 'deshabilitado'} correctamente`);
|
|
175
|
+
}
|
|
176
|
+
async dispatchWorkflow() {
|
|
177
|
+
const repo = await this.selectRepo();
|
|
178
|
+
if (!repo)
|
|
179
|
+
return;
|
|
180
|
+
const spin = this.uiPort.spinner('Cargando workflows...');
|
|
181
|
+
const workflows = await this.gitHubService.listWorkflows(repo.owner, repo.repo);
|
|
182
|
+
spin.stop();
|
|
183
|
+
if (workflows.length === 0) {
|
|
184
|
+
this.uiPort.warning(' Este repositorio no tiene workflows');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const workflowChoices = workflows.map((w) => ({ name: w.name, value: w }));
|
|
188
|
+
const selectedWorkflow = await this.uiPort.select('Selecciona un workflow:', workflowChoices);
|
|
189
|
+
const ref = await this.uiPort.input('Rama o tag para ejecutar:', { default: 'main' });
|
|
190
|
+
const useInputs = await this.uiPort.confirm('¿Deseas agregar inputs?', false);
|
|
191
|
+
let inputs = {};
|
|
192
|
+
if (useInputs) {
|
|
193
|
+
while (true) {
|
|
194
|
+
const key = await this.uiPort.input('Nombre del input (deja vacío para terminar):');
|
|
195
|
+
if (!key.trim())
|
|
196
|
+
break;
|
|
197
|
+
const value = await this.uiPort.input(`Valor para ${key}:`);
|
|
198
|
+
inputs[key.trim()] = value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const confirm = await this.uiPort.confirm(`¿Disparar ${selectedWorkflow.name} en ${ref}?`, true);
|
|
202
|
+
if (!confirm) {
|
|
203
|
+
this.uiPort.warning(' Operación cancelada');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const spin2 = this.uiPort.spinner('Disparando workflow...');
|
|
207
|
+
await this.gitHubService.triggerWorkflowDispatch(repo.owner, repo.repo, selectedWorkflow.id, ref, Object.keys(inputs).length > 0 ? inputs : undefined);
|
|
208
|
+
spin2.stop();
|
|
209
|
+
this.uiPort.success(' ✓ Workflow disparado correctamente');
|
|
210
|
+
}
|
|
211
|
+
getStateBadge(state) {
|
|
212
|
+
const c = this.uiColors;
|
|
213
|
+
switch (state) {
|
|
214
|
+
case 'active':
|
|
215
|
+
return c.exito('[ACTIVO]');
|
|
216
|
+
case 'disabled_manually':
|
|
217
|
+
return c.error('[DESHABILITADO]');
|
|
218
|
+
case 'disabled_inactivity':
|
|
219
|
+
return c.aviso('[INACTIVO]');
|
|
220
|
+
default:
|
|
221
|
+
return c.tenue(`[${state.toUpperCase()}]`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
getConclusionBadge(status, conclusion) {
|
|
225
|
+
const c = this.uiColors;
|
|
226
|
+
if (status === 'in_progress')
|
|
227
|
+
return c.titulo('[EN PROGRESO]');
|
|
228
|
+
if (status === 'queued')
|
|
229
|
+
return c.tenue('[EN COLA]');
|
|
230
|
+
switch (conclusion) {
|
|
231
|
+
case 'success':
|
|
232
|
+
return c.exito('[ÉXITO]');
|
|
233
|
+
case 'failure':
|
|
234
|
+
return c.error('[FALLO]');
|
|
235
|
+
case 'cancelled':
|
|
236
|
+
return c.aviso('[CANCELADO]');
|
|
237
|
+
case 'skipped':
|
|
238
|
+
return c.tenue('[SALTADO]');
|
|
239
|
+
default:
|
|
240
|
+
return c.tenue(`[${(conclusion || status).toUpperCase()}]`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|