@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 +20 -6
- package/dist/adapters/git/SimpleGitAdapter.js +18 -0
- package/dist/adapters/github/GitHubAPIAdapter.js +9 -1
- package/dist/adapters/storage/FileSecureStorageAdapter.js +31 -10
- package/dist/adapters/ui/ConsoleUIAdapter.js +15 -3
- package/dist/{factories → commands}/CommandFactory.js +13 -10
- package/dist/commands/CommitCommand.js +80 -17
- package/dist/commands/DiffCommand.js +60 -0
- package/dist/commands/LogCommand.js +90 -20
- package/dist/commands/PushCommand.js +1 -3
- package/dist/commands/RepositoryCommand.js +349 -105
- package/dist/commands/github/GitHubReposCommand.js +1 -3
- package/dist/core/services/GitHubService.test.js +61 -0
- package/dist/index.js +19 -2
- package/package.json +5 -2
- package/dist/commit.js +0 -98
- package/dist/core/errors/GitError.js +0 -19
- package/dist/core/services/GitService.js +0 -44
- package/dist/estado.js +0 -97
- package/dist/integracion.js +0 -152
- package/dist/ramas.js +0 -178
- package/dist/remoto.js +0 -129
- package/dist/repositorio.js +0 -159
- package/dist/stash.js +0 -169
- package/dist/utils.js +0 -49
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
|
|
29
|
+
#### 1. Instalación global directa desde GitHub (¡Recomendada!)
|
|
30
30
|
|
|
31
|
-
|
|
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 @
|
|
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 (`@
|
|
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 @
|
|
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 @
|
|
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({
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
3
|
-
import { LogCommand } from '
|
|
4
|
-
import { CommitCommand } from '
|
|
5
|
-
import { PushCommand } from '
|
|
6
|
-
import { PullCommand } from '
|
|
7
|
-
import { BranchesCommand } from '
|
|
8
|
-
import { IntegrationCommand } from '
|
|
9
|
-
import { StashCommand } from '
|
|
10
|
-
import { RepositoryCommand } from '
|
|
11
|
-
import { GitHubMenuCommand } from '
|
|
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('
|
|
53
|
-
{ name: '
|
|
54
|
-
{ name: '
|
|
55
|
-
{ name: '
|
|
56
|
-
{ name: '
|
|
57
|
-
{ name: '
|
|
58
|
-
{ name: 'test
|
|
59
|
-
{ name: '
|
|
60
|
-
{ name: '
|
|
61
|
-
{ name: 'ci
|
|
62
|
-
{ name: 'Ninguno
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
184
|
+
await this.gitPort.addRemote('origin', url);
|
|
187
185
|
spinRemote.stop();
|
|
188
186
|
this.uiPort.success(c.exito(` ✓ Remoto origin agregado: ${url}`));
|
|
189
187
|
}
|