@asaro/git-cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,13 +26,27 @@ Git CLI interactivo en español para gestionar repositorios Git y GitHub desde l
26
26
 
27
27
  ### Métodos de instalación
28
28
 
29
- #### 1. Instalación global desde npm (la forma más fácil!)
29
+ #### 1. Instalación global directa desde GitHub (¡Recomendada!)
30
30
 
31
- Si el paquete está publicado en npm:
31
+ Puedes instalar la herramienta directamente desde el repositorio en tu PC corriendo:
32
+
33
+ ```bash
34
+ npm install -g iTzAsaro/GitCLI
35
+ ```
36
+
37
+ Y luego utilízalo en cualquier lugar con:
38
+
39
+ ```bash
40
+ git-cli
41
+ ```
42
+
43
+ #### 2. Instalación global desde npm
44
+
45
+ Si el paquete está publicado en el registro de npm:
32
46
 
33
47
  ```bash
34
48
  # Instálalo globalmente en tu PC
35
- npm install -g @itzasaro/git-cli
49
+ npm install -g @asaro/git-cli
36
50
 
37
51
  # Ahora puedes usar el comando 'git-cli' desde cualquier lugar!
38
52
  git-cli
@@ -97,7 +111,7 @@ Edita `package.json` y cambia:
97
111
 
98
112
  ### Paso 4: Publicar!
99
113
 
100
- Como es un paquete con scope (`@iTzAsaro/`), necesitas publicar con acceso público:
114
+ Como es un paquete con scope (`@asaro/`), necesitas publicar con acceso público:
101
115
 
102
116
  ```bash
103
117
  npm publish --access public
@@ -106,7 +120,7 @@ npm publish --access public
106
120
  ¡Listo! Ahora cualquier persona puede instalar tu paquete con:
107
121
 
108
122
  ```bash
109
- npm install -g @itzasaro/git-cli
123
+ npm install -g @asaro/git-cli
110
124
  ```
111
125
 
112
126
  ### Para publicar una nueva versión
@@ -177,7 +191,7 @@ npm install -g .
177
191
  ## Desinstalación
178
192
 
179
193
  ```bash
180
- npm uninstall -g @itzasaro/git-cli
194
+ npm uninstall -g @asaro/git-cli
181
195
  ```
182
196
 
183
197
  ## Archivos de configuración
@@ -120,7 +120,25 @@ export class SimpleGitAdapter {
120
120
  async addRemote(name, url) {
121
121
  await this.git.addRemote(name, url);
122
122
  }
123
+ async removeRemote(name) {
124
+ await this.git.removeRemote(name);
125
+ }
126
+ async renameRemote(oldName, newName) {
127
+ await this.git.remote(['rename', oldName, newName]);
128
+ }
129
+ async setRemoteUrl(name, url) {
130
+ await this.git.remote(['set-url', name, url]);
131
+ }
123
132
  getGitInstance() {
124
133
  return this.git;
125
134
  }
135
+ async diff(options) {
136
+ return this.git.diff(options);
137
+ }
138
+ async show(options) {
139
+ if (options) {
140
+ return this.git.show(options);
141
+ }
142
+ return this.git.show();
143
+ }
126
144
  }
@@ -3,7 +3,15 @@ import { GitHubAuthError, GitHubRateLimitError, GitHubPermissionError, GitHubNot
3
3
  export class GitHubAPIAdapter {
4
4
  octokit = null;
5
5
  async authenticate(token) {
6
- this.octokit = new Octokit({ auth: token });
6
+ this.octokit = new Octokit({
7
+ auth: token,
8
+ log: {
9
+ debug: () => { },
10
+ info: () => { },
11
+ warn: () => { },
12
+ error: () => { },
13
+ },
14
+ });
7
15
  }
8
16
  async validateToken() {
9
17
  if (!this.octokit)
@@ -1,20 +1,14 @@
1
1
  import CryptoJS from 'crypto-js';
2
+ import crypto from 'crypto';
2
3
  import fs from 'fs/promises';
3
4
  import path from 'path';
4
5
  import os from 'os';
5
6
  export class FileSecureStorageAdapter {
6
7
  storageDir;
7
- secretKey;
8
+ secretKey = null;
8
9
  constructor() {
9
10
  // Use a directory in user's home for storage
10
11
  this.storageDir = path.join(os.homedir(), '.gitcli');
11
- // Use machine-specific info as part of secret (not perfect but better than plaintext)
12
- this.secretKey = this.getMachineSecret();
13
- this.ensureStorageDir();
14
- }
15
- getMachineSecret() {
16
- // Combine hostname and user info for a basic machine-specific secret
17
- return `${os.hostname()}-${os.userInfo().uid}-gitcli-secret-key-v1`;
18
12
  }
19
13
  async ensureStorageDir() {
20
14
  try {
@@ -24,21 +18,48 @@ export class FileSecureStorageAdapter {
24
18
  await fs.mkdir(this.storageDir, { recursive: true });
25
19
  }
26
20
  }
21
+ async getSecretKey() {
22
+ if (this.secretKey) {
23
+ return this.secretKey;
24
+ }
25
+ await this.ensureStorageDir();
26
+ const keyPath = path.join(this.storageDir, 'key.bin');
27
+ try {
28
+ const storedKey = await fs.readFile(keyPath, 'utf8');
29
+ this.secretKey = storedKey.trim();
30
+ }
31
+ catch {
32
+ // Key doesn't exist, generate a secure random 256-bit (32 bytes) key in hex
33
+ const randomKey = crypto.randomBytes(32).toString('hex');
34
+ await fs.writeFile(keyPath, randomKey, 'utf8');
35
+ try {
36
+ // Set permissions to 0o600 (owner read/write) for security on UNIX-like OS
37
+ await fs.chmod(keyPath, 0o600);
38
+ }
39
+ catch {
40
+ // Chmod might fail on Windows, ignore it
41
+ }
42
+ this.secretKey = randomKey;
43
+ }
44
+ return this.secretKey;
45
+ }
27
46
  getFilePath(key) {
28
47
  // Sanitize key for filename
29
48
  const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');
30
49
  return path.join(this.storageDir, `${safeKey}.enc`);
31
50
  }
32
51
  async save(key, value) {
33
- const encrypted = CryptoJS.AES.encrypt(value, this.secretKey).toString();
52
+ const secretKey = await this.getSecretKey();
53
+ const encrypted = CryptoJS.AES.encrypt(value, secretKey).toString();
34
54
  const filePath = this.getFilePath(key);
35
55
  await fs.writeFile(filePath, encrypted, 'utf8');
36
56
  }
37
57
  async load(key) {
38
58
  const filePath = this.getFilePath(key);
39
59
  try {
60
+ const secretKey = await this.getSecretKey();
40
61
  const encrypted = await fs.readFile(filePath, 'utf8');
41
- const bytes = CryptoJS.AES.decrypt(encrypted, this.secretKey);
62
+ const bytes = CryptoJS.AES.decrypt(encrypted, secretKey);
42
63
  return bytes.toString(CryptoJS.enc.Utf8);
43
64
  }
44
65
  catch {
@@ -1,4 +1,4 @@
1
- import { select, input, checkbox, confirm } from '@inquirer/prompts';
1
+ import { select, input, checkbox, confirm, Separator } from '@inquirer/prompts';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  const c = {
@@ -59,13 +59,25 @@ export class ConsoleUIAdapter {
59
59
  this.separator();
60
60
  }
61
61
  select(message, choices) {
62
- return select({ message, choices });
62
+ const mappedChoices = choices.map(choice => {
63
+ if (choice.disabled === true && (choice.name.includes('──') || String(choice.value).startsWith('sep'))) {
64
+ return new Separator(choice.name);
65
+ }
66
+ return choice;
67
+ });
68
+ return select({ message, choices: mappedChoices, pageSize: 25 });
63
69
  }
64
70
  input(message, options) {
65
71
  return input({ message, ...options });
66
72
  }
67
73
  checkbox(message, choices) {
68
- return checkbox({ message, choices });
74
+ const mappedChoices = choices.map(choice => {
75
+ if (choice.disabled === true && (choice.name.includes('──') || String(choice.value).startsWith('sep'))) {
76
+ return new Separator(choice.name);
77
+ }
78
+ return choice;
79
+ });
80
+ return checkbox({ message, choices: mappedChoices, pageSize: 25 });
69
81
  }
70
82
  confirm(message, defaultVal = true) {
71
83
  return confirm({ message, default: defaultVal });
@@ -1,14 +1,15 @@
1
1
  import { GitHubService } from '../core/services/GitHubService.js';
2
- import { StatusCommand } from '../commands/StatusCommand.js';
3
- import { LogCommand } from '../commands/LogCommand.js';
4
- import { CommitCommand } from '../commands/CommitCommand.js';
5
- import { PushCommand } from '../commands/PushCommand.js';
6
- import { PullCommand } from '../commands/PullCommand.js';
7
- import { BranchesCommand } from '../commands/BranchesCommand.js';
8
- import { IntegrationCommand } from '../commands/IntegrationCommand.js';
9
- import { StashCommand } from '../commands/StashCommand.js';
10
- import { RepositoryCommand } from '../commands/RepositoryCommand.js';
11
- import { GitHubMenuCommand } from '../commands/github/GitHubMenuCommand.js';
2
+ import { StatusCommand } from './StatusCommand.js';
3
+ import { LogCommand } from './LogCommand.js';
4
+ import { CommitCommand } from './CommitCommand.js';
5
+ import { PushCommand } from './PushCommand.js';
6
+ import { PullCommand } from './PullCommand.js';
7
+ import { BranchesCommand } from './BranchesCommand.js';
8
+ import { IntegrationCommand } from './IntegrationCommand.js';
9
+ import { StashCommand } from './StashCommand.js';
10
+ import { RepositoryCommand } from './RepositoryCommand.js';
11
+ import { GitHubMenuCommand } from './github/GitHubMenuCommand.js';
12
+ import { DiffCommand } from './DiffCommand.js';
12
13
  export class CommandFactory {
13
14
  gitPort;
14
15
  uiPort;
@@ -28,6 +29,8 @@ export class CommandFactory {
28
29
  return new StatusCommand(this.gitPort, this.uiPort);
29
30
  case 'log':
30
31
  return new LogCommand(this.gitPort, this.uiPort);
32
+ case 'diff':
33
+ return new DiffCommand(this.gitPort, this.uiPort);
31
34
  case 'commit':
32
35
  return new CommitCommand(this.gitPort, this.uiPort);
33
36
  case 'push':
@@ -49,24 +49,87 @@ export class CommitCommand extends BaseCommand {
49
49
  }
50
50
  this.uiPort.log('');
51
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: '' },
52
+ const tipo = await this.uiPort.select('¿Qué tipo de cambio realizaste?', [
53
+ { name: '🆕 Nueva característica o funcionalidad (feat)', value: 'feat' },
54
+ { name: '🐛 Corrección de un fallo o error (fix)', value: 'fix' },
55
+ { name: '📝 Documentación, guías o textos (docs)', value: 'docs' },
56
+ { name: '🎨 Diseño, apariencia, estilos o formato (style)', value: 'style' },
57
+ { name: ' Refactorización o mejora de código existente (refactor)', value: 'refactor' },
58
+ { name: '🧪 Pruebas unitarias o tests (test)', value: 'test' },
59
+ { name: '⚙️ Tareas internas o mantenimiento (chore)', value: 'chore' },
60
+ { name: '📦 Dependencias o configuración del build (build)', value: 'build' },
61
+ { name: '🚀 Automatización o Integración Continua (ci)', value: 'ci' },
62
+ { name: '✏️ Ninguno (escribir un mensaje libre sin formato)', value: '' },
63
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();
64
+ let mensaje = '';
65
+ if (!tipo) {
66
+ // Free message commit
67
+ const mensajeLibre = await this.uiPort.input('Escribe el mensaje para tu commit:', {
68
+ validate: (v) => v.trim().length > 0 || 'El mensaje no puede estar vacío.',
69
+ });
70
+ mensaje = mensajeLibre.trim();
71
+ }
72
+ else {
73
+ // Conventional Commit Walkthrough
74
+ const ambitoInput = await this.uiPort.input('¿A qué parte del proyecto afecta principalmente? (opcional. Ej: interfaz, login, base-de-datos):');
75
+ const ambito = ambitoInput.trim();
76
+ const descripcion = await this.uiPort.input('Resumen corto de lo que hiciste (ej: corregir diseño de botón):', {
77
+ validate: (v) => v.trim().length > 0 || 'La descripción no puede estar vacía.',
78
+ });
79
+ const mostrarOpcionesAvanzadas = await this.uiPort.confirm('¿Deseas agregar información avanzada? (como explicación larga o cambios importantes)', false);
80
+ let cuerpo = '';
81
+ let introduceBreaking = false;
82
+ let breakingDesc = '';
83
+ let cierraIssue = false;
84
+ let issueId = '';
85
+ if (mostrarOpcionesAvanzadas) {
86
+ const agregarCuerpo = await this.uiPort.confirm('¿Deseas agregar una explicación más larga y detallada?', false);
87
+ if (agregarCuerpo) {
88
+ cuerpo = await this.uiPort.input('Escribe la explicación detallada del cambio:');
89
+ }
90
+ introduceBreaking = await this.uiPort.confirm('¿Este cambio rompe la compatibilidad con versiones anteriores? (Breaking Change)', false);
91
+ if (introduceBreaking) {
92
+ breakingDesc = await this.uiPort.input('Describe el cambio importante que rompe la compatibilidad (BREAKING CHANGE):', {
93
+ validate: (v) => v.trim().length > 0 || 'Debes describir el cambio de ruptura.',
94
+ });
95
+ }
96
+ cierraIssue = await this.uiPort.confirm('¿Este cambio cierra algún issue o tarea en GitHub?', false);
97
+ if (cierraIssue) {
98
+ issueId = await this.uiPort.input('Número o ID de la tarea/issue (ej: #123):', {
99
+ validate: (v) => v.trim().length > 0 || 'Debes ingresar el número de issue.',
100
+ });
101
+ }
102
+ }
103
+ // Message Construction
104
+ let header = tipo;
105
+ if (ambito) {
106
+ header += `(${ambito})`;
107
+ }
108
+ if (introduceBreaking) {
109
+ header += '!';
110
+ }
111
+ header += `: ${descripcion.trim()}`;
112
+ mensaje = header;
113
+ if (cuerpo && cuerpo.trim()) {
114
+ mensaje += `\n\n${cuerpo.trim()}`;
115
+ }
116
+ const footers = [];
117
+ if (introduceBreaking && breakingDesc.trim()) {
118
+ footers.push(`BREAKING CHANGE: ${breakingDesc.trim()}`);
119
+ }
120
+ if (cierraIssue && issueId.trim()) {
121
+ const formattedIssue = issueId.trim().startsWith('#') ? issueId.trim() : `#${issueId.trim()}`;
122
+ footers.push(`Closes ${formattedIssue}`);
123
+ }
124
+ if (footers.length > 0) {
125
+ mensaje += `\n\n${footers.join('\n')}`;
126
+ }
127
+ }
68
128
  this.uiPort.separator();
69
- this.uiPort.log(c.titulo(' Commit: ') + c.info(`"${mensaje}"`));
129
+ this.uiPort.log(c.titulo(' Commit Propuesto:\n'));
130
+ mensaje.split('\n').forEach((line) => {
131
+ this.uiPort.log(` ${c.info(line)}`);
132
+ });
70
133
  this.uiPort.separator();
71
134
  const confirmar = await this.uiPort.confirm('¿Confirmar commit?', true);
72
135
  if (!confirmar) {
@@ -76,7 +139,7 @@ export class CommitCommand extends BaseCommand {
76
139
  const spinCommit = this.uiPort.spinner('Realizando commit...');
77
140
  await this.gitPort.commit(mensaje);
78
141
  spinCommit.stop();
79
- this.uiPort.success(`\n ✓ Commit realizado: "${mensaje}"\n`);
142
+ this.uiPort.success(`\n ✓ Commit realizado con éxito.\n`);
80
143
  }
81
144
  catch (err) {
82
145
  this.uiPort.error(` ✗ Error: ${err.message}`);
@@ -0,0 +1,60 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export class DiffCommand 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 tipoDiff = await this.uiPort.select('¿Qué diferencias deseas ver?', [
11
+ { name: '📂 Cambios en el área de trabajo (sin stagear)', value: 'working' },
12
+ { name: '✅ Cambios preparados para commit (staged)', value: 'staged' },
13
+ { name: '🚪 Volver', value: 'volver' },
14
+ ]);
15
+ if (tipoDiff === 'volver') {
16
+ return;
17
+ }
18
+ const options = [];
19
+ if (tipoDiff === 'staged') {
20
+ options.push('--staged');
21
+ }
22
+ const spin = this.uiPort.spinner('Obteniendo diferencias...');
23
+ const diffText = await this.gitPort.diff(options);
24
+ spin.stop();
25
+ const c = this.uiColors;
26
+ this.uiPort.log('');
27
+ this.uiPort.log(c.titulo(' 🔍 Diferencias del repositorio'));
28
+ this.uiPort.separator();
29
+ if (!diffText || diffText.trim().length === 0) {
30
+ this.uiPort.log(c.tenue(' No se encontraron diferencias en esta sección.'));
31
+ this.uiPort.separator();
32
+ this.uiPort.log('');
33
+ return;
34
+ }
35
+ const lines = diffText.split('\n');
36
+ lines.forEach((line) => {
37
+ if (line.startsWith('+') && !line.startsWith('+++')) {
38
+ this.uiPort.log(c.exito(line));
39
+ }
40
+ else if (line.startsWith('-') && !line.startsWith('---')) {
41
+ this.uiPort.log(c.error(line));
42
+ }
43
+ else if (line.startsWith('@@')) {
44
+ this.uiPort.log(c.info(line));
45
+ }
46
+ else if (line.startsWith('diff')) {
47
+ this.uiPort.log(c.titulo(line));
48
+ }
49
+ else {
50
+ this.uiPort.log(line);
51
+ }
52
+ });
53
+ this.uiPort.separator();
54
+ this.uiPort.log('');
55
+ }
56
+ catch (err) {
57
+ this.uiPort.error(` ✗ Error al obtener diff: ${err.message}`);
58
+ }
59
+ }
60
+ }
@@ -12,28 +12,98 @@ export class LogCommand extends BaseCommand {
12
12
  { name: 'Últimos 20', value: 20 },
13
13
  { name: 'Últimos 50', value: 50 },
14
14
  ]);
15
- const spin = this.uiPort.spinner('Cargando historial...');
16
- const log = await this.gitPort.getLog(cantidad);
17
- spin.stop();
18
15
  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',
16
+ let verDetalle = true;
17
+ while (verDetalle) {
18
+ const spin = this.uiPort.spinner('Cargando historial...');
19
+ const log = await this.gitPort.getLog(cantidad);
20
+ spin.stop();
21
+ this.uiPort.clear();
22
+ this.uiPort.showBanner();
23
+ this.uiPort.log(c.titulo(` 📜 Últimos ${cantidad} commits`));
24
+ this.uiPort.separator();
25
+ log.all.forEach((commit, i) => {
26
+ const esUltimo = i === 0;
27
+ const prefijo = esUltimo ? '◉' : '○';
28
+ const fecha = new Date(commit.date).toLocaleDateString('es-CL', {
29
+ day: '2-digit',
30
+ month: 'short',
31
+ year: 'numeric',
32
+ });
33
+ this.uiPort.log(` ${c.hash(prefijo)} ${c.hash(commit.hash.slice(0, 7))}` +
34
+ ` ${c.tenue(fecha)} ` +
35
+ `${c.titulo(commit.message.slice(0, 45))}` +
36
+ ` ${c.tenue(commit.author_name)}`);
29
37
  });
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('');
38
+ this.uiPort.separator();
39
+ this.uiPort.log('');
40
+ const opcion = await this.uiPort.select('¿Qué deseas hacer?', [
41
+ { name: '🔍 Inspeccionar un commit (ver detalle y diff)', value: 'inspeccionar' },
42
+ { name: '🌿 Checkout temporal a un commit', value: 'checkout' },
43
+ { name: '🚪 Volver al menú principal', value: 'salir' },
44
+ ]);
45
+ if (opcion === 'salir') {
46
+ verDetalle = false;
47
+ }
48
+ else if (opcion === 'inspeccionar') {
49
+ const choices = log.all.map((commit) => ({
50
+ name: `${commit.hash.slice(0, 7)} - ${commit.message.slice(0, 50)}`,
51
+ value: commit.hash,
52
+ }));
53
+ choices.push({ name: '🚪 Volver', value: 'volver' });
54
+ const commitHash = await this.uiPort.select('Selecciona un commit para ver su detalle:', choices);
55
+ if (commitHash !== 'volver') {
56
+ const spinShow = this.uiPort.spinner('Obteniendo detalles del commit...');
57
+ const commitDetailText = await this.gitPort.show([commitHash]);
58
+ spinShow.stop();
59
+ this.uiPort.clear();
60
+ this.uiPort.showBanner();
61
+ this.uiPort.log(c.titulo(` 🔍 Detalles del Commit: ${commitHash.slice(0, 7)}`));
62
+ this.uiPort.separator();
63
+ const lines = commitDetailText.split('\n');
64
+ lines.forEach((line) => {
65
+ if (line.startsWith('+') && !line.startsWith('+++')) {
66
+ this.uiPort.log(c.exito(line));
67
+ }
68
+ else if (line.startsWith('-') && !line.startsWith('---')) {
69
+ this.uiPort.log(c.error(line));
70
+ }
71
+ else if (line.startsWith('@@')) {
72
+ this.uiPort.log(c.info(line));
73
+ }
74
+ else if (line.startsWith('diff') || line.startsWith('commit ')) {
75
+ this.uiPort.log(c.titulo(line));
76
+ }
77
+ else {
78
+ this.uiPort.log(line);
79
+ }
80
+ });
81
+ this.uiPort.separator();
82
+ await this.uiPort.pause();
83
+ }
84
+ }
85
+ else if (opcion === 'checkout') {
86
+ const choices = log.all.map((commit) => ({
87
+ name: `${commit.hash.slice(0, 7)} - ${commit.message.slice(0, 50)}`,
88
+ value: commit.hash,
89
+ }));
90
+ choices.push({ name: '🚪 Volver', value: 'volver' });
91
+ const commitHash = await this.uiPort.select('Selecciona el commit para checkout:', choices);
92
+ if (commitHash !== 'volver') {
93
+ this.uiPort.warning(`\n ⚠️ Hacer checkout a un commit te dejará en estado 'Detached HEAD'.`);
94
+ this.uiPort.warning(` Para regresar, deberás hacer checkout a tu rama actual desde el menú de Ramas.\n`);
95
+ const confirmar = await this.uiPort.confirm(`¿Quieres hacer checkout al commit ${commitHash.slice(0, 7)}?`, false);
96
+ if (confirmar) {
97
+ const spinCheck = this.uiPort.spinner(`Haciendo checkout a ${commitHash.slice(0, 7)}...`);
98
+ await this.gitPort.checkout(commitHash);
99
+ spinCheck.stop();
100
+ this.uiPort.success(`\n ✓ Ahora estás en el commit ${c.rama(commitHash.slice(0, 7))}.`);
101
+ await this.uiPort.pause();
102
+ verDetalle = false; // salir de la vista ya que el contexto (HEAD) cambió
103
+ }
104
+ }
105
+ }
106
+ }
37
107
  }
38
108
  catch (err) {
39
109
  this.uiPort.error(` ✗ Error: ${err.message}`);
@@ -1,5 +1,4 @@
1
1
  import { BaseCommand } from './BaseCommand.js';
2
- import { simpleGit } from 'simple-git';
3
2
  import path from 'path';
4
3
  export class PushCommand extends BaseCommand {
5
4
  gitHubService;
@@ -176,14 +175,13 @@ export class PushCommand extends BaseCommand {
176
175
  // Paso 2: Agregar remoto
177
176
  const spinRemote = this.uiPort.spinner('Configurando remoto...');
178
177
  try {
179
- const git = simpleGit(cwd);
180
178
  // Elegir método: HTTPS o SSH
181
179
  const metodo = await this.uiPort.select('Método de conexión:', [
182
180
  { name: 'HTTPS', value: 'https' },
183
181
  { name: 'SSH', value: 'ssh' },
184
182
  ]);
185
183
  const url = metodo === 'https' ? repoCreado.cloneUrl : repoCreado.sshUrl;
186
- await git.addRemote('origin', url);
184
+ await this.gitPort.addRemote('origin', url);
187
185
  spinRemote.stop();
188
186
  this.uiPort.success(c.exito(` ✓ Remoto origin agregado: ${url}`));
189
187
  }