@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.
Files changed (40) hide show
  1. package/README.md +189 -0
  2. package/dist/adapters/git/SimpleGitAdapter.js +126 -0
  3. package/dist/adapters/github/GitHubAPIAdapter.js +243 -0
  4. package/dist/adapters/storage/FileSecureStorageAdapter.js +67 -0
  5. package/dist/adapters/ui/ConsoleUIAdapter.js +91 -0
  6. package/dist/commands/BaseCommand.js +11 -0
  7. package/dist/commands/BranchesCommand.js +152 -0
  8. package/dist/commands/Command.js +1 -0
  9. package/dist/commands/CommitCommand.js +85 -0
  10. package/dist/commands/IntegrationCommand.js +151 -0
  11. package/dist/commands/LogCommand.js +42 -0
  12. package/dist/commands/PullCommand.js +55 -0
  13. package/dist/commands/PushCommand.js +227 -0
  14. package/dist/commands/RepositoryCommand.js +268 -0
  15. package/dist/commands/StashCommand.js +140 -0
  16. package/dist/commands/StatusCommand.js +66 -0
  17. package/dist/commands/github/BaseGitHubCommand.js +17 -0
  18. package/dist/commands/github/GitHubAuthCommand.js +48 -0
  19. package/dist/commands/github/GitHubMenuCommand.js +57 -0
  20. package/dist/commands/github/GitHubReposCommand.js +210 -0
  21. package/dist/commands/github/GitHubWorkflowsCommand.js +243 -0
  22. package/dist/commit.js +98 -0
  23. package/dist/core/errors/GitError.js +19 -0
  24. package/dist/core/errors/GitHubError.js +36 -0
  25. package/dist/core/ports/GitHubPort.js +1 -0
  26. package/dist/core/ports/GitPort.js +1 -0
  27. package/dist/core/ports/SecureStoragePort.js +1 -0
  28. package/dist/core/ports/UIPort.js +1 -0
  29. package/dist/core/services/GitHubService.js +95 -0
  30. package/dist/core/services/GitService.js +44 -0
  31. package/dist/estado.js +97 -0
  32. package/dist/factories/CommandFactory.js +51 -0
  33. package/dist/index.js +71 -0
  34. package/dist/integracion.js +152 -0
  35. package/dist/ramas.js +178 -0
  36. package/dist/remoto.js +129 -0
  37. package/dist/repositorio.js +159 -0
  38. package/dist/stash.js +169 -0
  39. package/dist/utils.js +49 -0
  40. package/package.json +53 -0
@@ -0,0 +1,91 @@
1
+ import { select, input, checkbox, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ const c = {
5
+ titulo: (t) => chalk.bold.hex('#cad3f5')(t),
6
+ exito: (t) => chalk.hex('#a6da95')(t),
7
+ error: (t) => chalk.hex('#ed8796')(t),
8
+ aviso: (t) => chalk.hex('#eed49f')(t),
9
+ info: (t) => chalk.hex('#8aadf4')(t),
10
+ tenue: (t) => chalk.hex('#6e738d')(t),
11
+ rama: (t) => chalk.bold.hex('#f5bde6')(t),
12
+ hash: (t) => chalk.hex('#f5a97f')(t),
13
+ archivo: (t) => chalk.hex('#91d7e3')(t),
14
+ };
15
+ export class ConsoleUIAdapter {
16
+ clear() {
17
+ console.clear();
18
+ }
19
+ log(message) {
20
+ console.log(message);
21
+ }
22
+ info(message) {
23
+ console.log(c.info(message));
24
+ }
25
+ success(message) {
26
+ console.log(c.exito(message));
27
+ }
28
+ warning(message) {
29
+ console.log(c.aviso(message));
30
+ }
31
+ error(message) {
32
+ console.log(c.error(message));
33
+ }
34
+ separator() {
35
+ console.log(c.tenue('─'.repeat(52)));
36
+ }
37
+ showBanner() {
38
+ console.log();
39
+ console.log(c.titulo(' ██████╗ ██╗████████╗ ██████╗██╗ ██╗'));
40
+ console.log(c.titulo(' ██╔════╝ ██║╚══██╔══╝██╔════╝██║ ██║'));
41
+ console.log(c.titulo(' ██║ ███╗██║ ██║ ██║ ██║ ██║'));
42
+ console.log(c.titulo(' ██║ ██║██║ ██║ ██║ ██║ ██║'));
43
+ console.log(c.titulo(' ╚██████╔╝██║ ██║ ╚██████╗███████╗██║'));
44
+ console.log(c.titulo(' ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝'));
45
+ console.log(c.tenue(' Git CLI interactivo · Alex · v2.0.0 (Arquitectura Limpia)'));
46
+ console.log();
47
+ }
48
+ showContextInfo(rama, ahead, behind, cambios) {
49
+ const indicadorRama = c.rama(` 🌿 ${rama}`);
50
+ const indicadorSync = ahead > 0
51
+ ? c.aviso(` ↑${ahead}`)
52
+ : behind > 0
53
+ ? c.aviso(` ↓${behind}`)
54
+ : c.exito(' ✓');
55
+ const indicadorCambios = cambios > 0
56
+ ? c.aviso(` ✎ ${cambios} cambio(s) sin commit`)
57
+ : c.tenue(' árbol limpio');
58
+ console.log(indicadorRama + indicadorSync + indicadorCambios);
59
+ this.separator();
60
+ }
61
+ select(message, choices) {
62
+ return select({ message, choices });
63
+ }
64
+ input(message, options) {
65
+ return input({ message, ...options });
66
+ }
67
+ checkbox(message, choices) {
68
+ return checkbox({ message, choices });
69
+ }
70
+ confirm(message, defaultVal = true) {
71
+ return confirm({ message, default: defaultVal });
72
+ }
73
+ spinner(text) {
74
+ return ora({ text: c.info(text), spinner: 'dots' }).start();
75
+ }
76
+ pause() {
77
+ return new Promise((resolve) => {
78
+ process.stdout.write(c.tenue(' Presiona Enter para continuar...'));
79
+ process.stdin.resume();
80
+ process.stdin.setEncoding('utf8');
81
+ process.stdin.once('data', () => {
82
+ process.stdin.pause();
83
+ resolve();
84
+ });
85
+ });
86
+ }
87
+ // Helper methods for color formatting
88
+ get colors() {
89
+ return c;
90
+ }
91
+ }
@@ -0,0 +1,11 @@
1
+ export class BaseCommand {
2
+ gitPort;
3
+ uiPort;
4
+ constructor(gitPort, uiPort) {
5
+ this.gitPort = gitPort;
6
+ this.uiPort = uiPort;
7
+ }
8
+ get uiColors() {
9
+ return this.uiPort.colors;
10
+ }
11
+ }
@@ -0,0 +1,152 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class BranchesCommand 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 accion = await this.uiPort.select('Ramas — ¿qué deseas hacer?', [
11
+ { name: 'Listar todas las ramas', value: 'listar' },
12
+ { name: 'Cambiar de rama (checkout)', value: 'cambiar' },
13
+ { name: 'Crear nueva rama', value: 'crear' },
14
+ { name: 'Crear y cambiar a nueva rama', value: 'crearCambiar' },
15
+ { name: 'Eliminar rama', value: 'eliminar' },
16
+ { name: 'Renombrar rama actual', value: 'renombrar' },
17
+ { name: '← Volver al menú principal', value: 'volver' },
18
+ ]);
19
+ switch (accion) {
20
+ case 'listar':
21
+ await this.listarRamas();
22
+ break;
23
+ case 'cambiar':
24
+ await this.cambiarRama();
25
+ break;
26
+ case 'crear':
27
+ await this.crearRama(false);
28
+ break;
29
+ case 'crearCambiar':
30
+ await this.crearRama(true);
31
+ break;
32
+ case 'eliminar':
33
+ await this.eliminarRama();
34
+ break;
35
+ case 'renombrar':
36
+ await this.renombrarRama();
37
+ break;
38
+ case 'volver':
39
+ return;
40
+ }
41
+ }
42
+ catch (err) {
43
+ this.uiPort.error(` ✗ Error: ${err.message}`);
44
+ }
45
+ }
46
+ async listarRamas() {
47
+ const spin = this.uiPort.spinner('Cargando ramas...');
48
+ const ramas = await this.gitPort.getBranches();
49
+ spin.stop();
50
+ const c = this.uiColors;
51
+ this.uiPort.log('');
52
+ this.uiPort.log(c.titulo(' 🌿 Ramas del repositorio'));
53
+ this.uiPort.separator();
54
+ ramas.all.forEach((rama) => {
55
+ const esActual = rama === ramas.current;
56
+ const esRemota = rama.startsWith('remotes/');
57
+ const nombre = rama.replace('remotes/', '');
58
+ if (esActual) {
59
+ this.uiPort.log(c.exito(` ◉ ${nombre}`) + c.tenue(' ← actual'));
60
+ }
61
+ else if (esRemota) {
62
+ this.uiPort.log(c.tenue(` ○ ${nombre}`) + c.info(' (remota)'));
63
+ }
64
+ else {
65
+ this.uiPort.log(c.titulo(` ○ ${nombre}`));
66
+ }
67
+ });
68
+ this.uiPort.separator();
69
+ this.uiPort.log('');
70
+ }
71
+ async cambiarRama() {
72
+ const spin = this.uiPort.spinner('Cargando ramas locales...');
73
+ const ramas = await this.gitPort.getBranches();
74
+ spin.stop();
75
+ const opciones = ramas.all
76
+ .filter((r) => r !== ramas.current && !r.startsWith('remotes/'))
77
+ .map((r) => ({ name: r, value: r }));
78
+ if (opciones.length === 0) {
79
+ this.uiPort.warning('\n No hay otras ramas locales disponibles.\n');
80
+ return;
81
+ }
82
+ const destino = await this.uiPort.select('Selecciona la rama de destino:', opciones);
83
+ const spinSwitch = this.uiPort.spinner(`Cambiando a ${destino}...`);
84
+ await this.gitPort.checkout(destino);
85
+ spinSwitch.stop();
86
+ const c = this.uiColors;
87
+ this.uiPort.success(`\n ✓ Ahora estás en: ${c.rama(destino)}\n`);
88
+ }
89
+ async crearRama(cambiarAlCrear) {
90
+ const nombre = await this.uiPort.input('Nombre de la nueva rama:', {
91
+ validate: (v) => {
92
+ if (!v.trim())
93
+ return 'El nombre no puede estar vacío.';
94
+ if (/\s/.test(v))
95
+ return 'El nombre no puede tener espacios.';
96
+ return true;
97
+ },
98
+ });
99
+ const spinCrear = this.uiPort.spinner(`Creando rama ${nombre}...`);
100
+ if (cambiarAlCrear) {
101
+ await this.gitPort.checkoutLocalBranch(nombre);
102
+ }
103
+ else {
104
+ await this.gitPort.createBranch(nombre);
105
+ }
106
+ spinCrear.stop();
107
+ const c = this.uiColors;
108
+ this.uiPort.success(`\n ✓ Rama ${cambiarAlCrear ? 'creada y activa' : 'creada'}: ${c.rama(nombre)}\n`);
109
+ }
110
+ async eliminarRama() {
111
+ const spin = this.uiPort.spinner('Cargando ramas locales...');
112
+ const ramas = await this.gitPort.getBranches();
113
+ spin.stop();
114
+ const opciones = ramas.all
115
+ .filter((r) => r !== ramas.current && !r.startsWith('remotes/'))
116
+ .map((r) => ({ name: r, value: r }));
117
+ if (opciones.length === 0) {
118
+ this.uiPort.warning('\n No hay ramas locales para eliminar (no puedes eliminar la actual).\n');
119
+ return;
120
+ }
121
+ const objetivo = await this.uiPort.select('¿Qué rama deseas eliminar?', opciones);
122
+ const forzar = await this.uiPort.confirm(`¿Forzar eliminación de "${objetivo}"? (--force)`, false);
123
+ const confirmar = await this.uiPort.confirm(this.uiColors.error(`¿Seguro que deseas eliminar "${objetivo}"?`), false);
124
+ if (!confirmar) {
125
+ this.uiPort.warning(' Operación cancelada.\n');
126
+ return;
127
+ }
128
+ const spinDel = this.uiPort.spinner(`Eliminando rama ${objetivo}...`);
129
+ await this.gitPort.deleteBranch(objetivo, forzar);
130
+ spinDel.stop();
131
+ this.uiPort.success(`\n ✓ Rama eliminada: ${objetivo}\n`);
132
+ }
133
+ async renombrarRama() {
134
+ const ramas = await this.gitPort.getBranches();
135
+ const actual = ramas.current;
136
+ const c = this.uiColors;
137
+ this.uiPort.log(c.info(`\n Rama actual: ${c.rama(actual)}`));
138
+ const nuevoNombre = await this.uiPort.input('Nuevo nombre:', {
139
+ validate: (v) => {
140
+ if (!v.trim())
141
+ return 'El nombre no puede estar vacío.';
142
+ if (/\s/.test(v))
143
+ return 'Sin espacios.';
144
+ return true;
145
+ },
146
+ });
147
+ const spinRen = this.uiPort.spinner('Renombrando...');
148
+ await this.gitPort.renameBranch(actual, nuevoNombre);
149
+ spinRen.stop();
150
+ this.uiPort.success(`\n ✓ Renombrada: ${actual} → ${c.rama(nuevoNombre)}\n`);
151
+ }
152
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class CommitCommand 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('Leyendo estado del repositorio...');
11
+ const status = await this.gitPort.getStatus();
12
+ spin.stop();
13
+ const c = this.uiColors;
14
+ const disponibles = [
15
+ ...status.modified.map((f) => ({ name: c.aviso(` ~ ${f}`) + c.tenue(' (modificado)'), value: f })),
16
+ ...status.not_added.map((f) => ({ name: c.tenue(` ? ${f}`) + c.tenue(' (nuevo)'), value: f })),
17
+ ...status.deleted.map((f) => ({ name: c.error(` - ${f}`) + c.tenue(' (eliminado)'), value: f })),
18
+ ];
19
+ let archivosParaStage = [];
20
+ if (status.staged.length > 0) {
21
+ this.uiPort.log('');
22
+ this.uiPort.log(c.info(' Ya tienes archivos preparados (staged):'));
23
+ status.staged.forEach((f) => this.uiPort.log(c.exito(` + ${f}`)));
24
+ this.uiPort.log('');
25
+ const usarExistentes = await this.uiPort.confirm('¿Usar los archivos ya preparados sin agregar más?', true);
26
+ if (!usarExistentes && disponibles.length > 0) {
27
+ archivosParaStage = await this.uiPort.checkbox('Selecciona archivos adicionales para incluir:', disponibles);
28
+ }
29
+ }
30
+ else {
31
+ if (disponibles.length === 0) {
32
+ this.uiPort.warning(' No hay cambios para hacer commit.\n');
33
+ return;
34
+ }
35
+ const modo = await this.uiPort.select('¿Qué archivos incluir en el commit?', [
36
+ { name: 'Todos los cambios (git add .)', value: 'todos' },
37
+ { name: 'Seleccionar manualmente', value: 'manual' },
38
+ ]);
39
+ if (modo === 'todos') {
40
+ archivosParaStage = ['.'];
41
+ }
42
+ else {
43
+ archivosParaStage = await this.uiPort.checkbox('Selecciona los archivos (espacio para marcar):', disponibles);
44
+ }
45
+ const spinAdd = this.uiPort.spinner('Agregando archivos...');
46
+ await this.gitPort.add(archivosParaStage);
47
+ spinAdd.stop();
48
+ this.uiPort.success(' ✓ Archivos agregados al stage');
49
+ }
50
+ this.uiPort.log('');
51
+ this.uiPort.separator();
52
+ const tipo = await this.uiPort.select('Tipo de commit:', [
53
+ { name: 'feat · Nueva funcionalidad', value: 'feat' },
54
+ { name: 'fix · Corrección de bug', value: 'fix' },
55
+ { name: 'docs · Documentación', value: 'docs' },
56
+ { name: 'style · Estilos / formato', value: 'style' },
57
+ { name: 'refactor · Refactorización', value: 'refactor' },
58
+ { name: 'test · Tests', value: 'test' },
59
+ { name: 'chore · Tareas de mantenimiento', value: 'chore' },
60
+ { name: 'build · Sistema de build', value: 'build' },
61
+ { name: 'ci · CI/CD', value: 'ci' },
62
+ { name: 'Ninguno · Mensaje libre', value: '' },
63
+ ]);
64
+ const descripcion = await this.uiPort.input('Descripción del commit:', {
65
+ validate: (v) => v.trim().length > 0 || 'El mensaje no puede estar vacío.',
66
+ });
67
+ const mensaje = tipo ? `${tipo}: ${descripcion.trim()}` : descripcion.trim();
68
+ this.uiPort.separator();
69
+ this.uiPort.log(c.titulo(' Commit: ') + c.info(`"${mensaje}"`));
70
+ this.uiPort.separator();
71
+ const confirmar = await this.uiPort.confirm('¿Confirmar commit?', true);
72
+ if (!confirmar) {
73
+ this.uiPort.warning(' Commit cancelado.\n');
74
+ return;
75
+ }
76
+ const spinCommit = this.uiPort.spinner('Realizando commit...');
77
+ await this.gitPort.commit(mensaje);
78
+ spinCommit.stop();
79
+ this.uiPort.success(`\n ✓ Commit realizado: "${mensaje}"\n`);
80
+ }
81
+ catch (err) {
82
+ this.uiPort.error(` ✗ Error: ${err.message}`);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,151 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class IntegrationCommand 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 accion = await this.uiPort.select('Integración — ¿qué deseas hacer?', [
11
+ { name: 'Merge (fusionar rama en la actual)', value: 'merge' },
12
+ { name: 'Rebase (reescribir historial sobre base)', value: 'rebase' },
13
+ { name: 'Abortar merge en curso', value: 'abortMerge' },
14
+ { name: 'Abortar rebase en curso', value: 'abortRebase' },
15
+ { name: '← Volver al menú principal', value: 'volver' },
16
+ ]);
17
+ switch (accion) {
18
+ case 'merge':
19
+ await this.hacerMerge();
20
+ break;
21
+ case 'rebase':
22
+ await this.hacerRebase();
23
+ break;
24
+ case 'abortMerge':
25
+ await this.abortarMerge();
26
+ break;
27
+ case 'abortRebase':
28
+ await this.abortarRebase();
29
+ break;
30
+ case 'volver':
31
+ return;
32
+ }
33
+ }
34
+ catch (err) {
35
+ this.uiPort.error(` ✗ Error: ${err.message}`);
36
+ }
37
+ }
38
+ async hacerMerge() {
39
+ const spin = this.uiPort.spinner('Cargando ramas...');
40
+ const ramas = await this.gitPort.getBranches();
41
+ spin.stop();
42
+ const actual = ramas.current;
43
+ const opciones = ramas.all
44
+ .filter(r => r !== actual && !r.startsWith('remotes/'))
45
+ .map(r => ({ name: r, value: r }));
46
+ if (opciones.length === 0) {
47
+ this.uiPort.warning('\n No hay otras ramas para fusionar.\n');
48
+ return;
49
+ }
50
+ const origen = await this.uiPort.select(`Fusionar en "${this.uiColors.rama(actual)}" ← ¿desde qué rama?`, opciones);
51
+ const modo = await this.uiPort.select('Modo de merge:', [
52
+ { name: 'Normal (crea commit de merge)', value: 'normal' },
53
+ { name: 'No fast-forward (fuerza merge commit siempre)', value: 'noff' },
54
+ { name: 'Squash (aplana todos los commits en uno)', value: 'squash' },
55
+ ]);
56
+ this.uiPort.separator();
57
+ this.uiPort.log(` ${this.uiColors.rama(origen)} → ${this.uiColors.rama(actual)}`);
58
+ this.uiPort.separator();
59
+ const confirmar = await this.uiPort.confirm('¿Confirmar merge?', true);
60
+ if (!confirmar) {
61
+ this.uiPort.warning(' Merge cancelado.\n');
62
+ return;
63
+ }
64
+ const spinMerge = this.uiPort.spinner(`Fusionando ${origen} en ${actual}...`);
65
+ try {
66
+ switch (modo) {
67
+ case 'noff':
68
+ await this.gitPort.merge(origen, ['--no-ff']);
69
+ break;
70
+ case 'squash':
71
+ await this.gitPort.merge(origen, ['--squash']);
72
+ spinMerge.stop();
73
+ this.uiPort.success('\n ✓ Squash merge listo. Debes hacer commit manual.\n');
74
+ return;
75
+ default:
76
+ await this.gitPort.merge(origen);
77
+ }
78
+ spinMerge.stop();
79
+ this.uiPort.success(`\n ✓ Merge completado: ${origen} → ${actual}\n`);
80
+ }
81
+ catch (err) {
82
+ spinMerge.stop();
83
+ this.uiPort.error(`\n ✗ Error en merge (posibles conflictos):\n ${err.message}\n`);
84
+ this.uiPort.warning(' Usa "Abortar merge en curso" si deseas revertir.\n');
85
+ }
86
+ }
87
+ async hacerRebase() {
88
+ const spin = this.uiPort.spinner('Cargando ramas...');
89
+ const ramas = await this.gitPort.getBranches();
90
+ spin.stop();
91
+ const actual = ramas.current;
92
+ const opciones = ramas.all
93
+ .filter(r => r !== actual && !r.startsWith('remotes/'))
94
+ .map(r => ({ name: r, value: r }));
95
+ if (opciones.length === 0) {
96
+ this.uiPort.warning('\n No hay otras ramas para hacer rebase.\n');
97
+ return;
98
+ }
99
+ const base = await this.uiPort.select(`Rebasar "${this.uiColors.rama(actual)}" sobre ¿qué rama?`, opciones);
100
+ this.uiPort.log('');
101
+ this.uiPort.warning(' ⚠ El rebase reescribe el historial de commits.');
102
+ this.uiPort.warning(' No lo uses en ramas compartidas/remotas.');
103
+ this.uiPort.separator();
104
+ const confirmar = await this.uiPort.confirm(`¿Confirmar rebase sobre ${base}?`, false);
105
+ if (!confirmar) {
106
+ this.uiPort.warning(' Rebase cancelado.\n');
107
+ return;
108
+ }
109
+ const spinRebase = this.uiPort.spinner(`Aplicando rebase sobre ${base}...`);
110
+ try {
111
+ await this.gitPort.rebase(base);
112
+ spinRebase.stop();
113
+ this.uiPort.success(`\n ✓ Rebase completado sobre ${this.uiColors.rama(base)}\n`);
114
+ }
115
+ catch (err) {
116
+ spinRebase.stop();
117
+ this.uiPort.error(`\n ✗ Conflictos durante rebase:\n ${err.message}\n`);
118
+ this.uiPort.warning(' Resuelve conflictos manualmente o usa "Abortar rebase".\n');
119
+ }
120
+ }
121
+ async abortarMerge() {
122
+ const confirmar = await this.uiPort.confirm('¿Abortar el merge en curso?', false);
123
+ if (!confirmar)
124
+ return;
125
+ const spin = this.uiPort.spinner('Abortando merge...');
126
+ try {
127
+ await this.gitPort.abortMerge();
128
+ spin.stop();
129
+ this.uiPort.success('\n ✓ Merge abortado. Estado restaurado.\n');
130
+ }
131
+ catch (err) {
132
+ spin.stop();
133
+ this.uiPort.error('\n ✗ No hay merge en curso o falló el abort.\n');
134
+ }
135
+ }
136
+ async abortarRebase() {
137
+ const confirmar = await this.uiPort.confirm('¿Abortar el rebase en curso?', false);
138
+ if (!confirmar)
139
+ return;
140
+ const spin = this.uiPort.spinner('Abortando rebase...');
141
+ try {
142
+ await this.gitPort.abortRebase();
143
+ spin.stop();
144
+ this.uiPort.success('\n ✓ Rebase abortado. Estado restaurado.\n');
145
+ }
146
+ catch (err) {
147
+ spin.stop();
148
+ this.uiPort.error('\n ✗ No hay rebase en curso o falló el abort.\n');
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,42 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class LogCommand 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 cantidad = await this.uiPort.select('¿Cuántos commits mostrar?', [
11
+ { name: 'Últimos 10', value: 10 },
12
+ { name: 'Últimos 20', value: 20 },
13
+ { name: 'Últimos 50', value: 50 },
14
+ ]);
15
+ const spin = this.uiPort.spinner('Cargando historial...');
16
+ const log = await this.gitPort.getLog(cantidad);
17
+ spin.stop();
18
+ const c = this.uiColors;
19
+ this.uiPort.log('');
20
+ this.uiPort.log(c.titulo(` 📜 Últimos ${cantidad} commits`));
21
+ this.uiPort.separator();
22
+ log.all.forEach((commit, i) => {
23
+ const esUltimo = i === 0;
24
+ const prefijo = esUltimo ? '◉' : '○';
25
+ const fecha = new Date(commit.date).toLocaleDateString('es-CL', {
26
+ day: '2-digit',
27
+ month: 'short',
28
+ year: 'numeric',
29
+ });
30
+ this.uiPort.log(` ${c.hash(prefijo)} ${c.hash(commit.hash.slice(0, 7))}` +
31
+ ` ${c.tenue(fecha)} ` +
32
+ `${c.titulo(commit.message.slice(0, 45))}` +
33
+ ` ${c.tenue(commit.author_name)}`);
34
+ });
35
+ this.uiPort.separator();
36
+ this.uiPort.log('');
37
+ }
38
+ catch (err) {
39
+ this.uiPort.error(` ✗ Error: ${err.message}`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,55 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class PullCommand 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('Verificando estado...');
11
+ const status = await this.gitPort.getStatus();
12
+ const remotos = await this.gitPort.getRemotes();
13
+ spin.stop();
14
+ if (remotos.length === 0) {
15
+ this.uiPort.error('\n ✗ No hay remotos configurados.\n');
16
+ return;
17
+ }
18
+ const c = this.uiColors;
19
+ const rama = status.current ?? 'main';
20
+ this.uiPort.log('');
21
+ this.uiPort.log(c.titulo(' ↓ Bajar cambios (pull)'));
22
+ this.uiPort.separator();
23
+ this.uiPort.log(` Rama actual: ${c.rama(rama)}`);
24
+ if (status.behind > 0) {
25
+ this.uiPort.log(c.aviso(` ${status.behind} commit(s) disponibles en el remoto`));
26
+ }
27
+ this.uiPort.separator();
28
+ let remoto = 'origin';
29
+ if (remotos.length > 1) {
30
+ remoto = await this.uiPort.select('¿Desde qué remoto hacer pull?', remotos.map((r) => ({ name: r.name, value: r.name })));
31
+ }
32
+ const estrategia = await this.uiPort.select('Estrategia de integración:', [
33
+ { name: 'Merge (por defecto)', value: 'merge' },
34
+ { name: 'Rebase (historial más limpio)', value: 'rebase' },
35
+ ]);
36
+ const confirmar = await this.uiPort.confirm(`¿Confirmar pull desde ${remoto}/${rama}?`, true);
37
+ if (!confirmar) {
38
+ this.uiPort.warning(' Pull cancelado.\n');
39
+ return;
40
+ }
41
+ const spinPull = this.uiPort.spinner('Bajando cambios...');
42
+ if (estrategia === 'rebase') {
43
+ await this.gitPort.pull(remoto, rama, { '--rebase': 'true' });
44
+ }
45
+ else {
46
+ await this.gitPort.pull(remoto, rama);
47
+ }
48
+ spinPull.stop();
49
+ this.uiPort.success(`\n ✓ Pull exitoso desde ${remoto}/${rama}\n`);
50
+ }
51
+ catch (err) {
52
+ this.uiPort.error(` ✗ Error: ${err.message}`);
53
+ }
54
+ }
55
+ }