@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseCommand } from './BaseCommand.js';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
export class RepositoryCommand extends BaseCommand {
|
|
5
6
|
gitHubService;
|
|
6
7
|
constructor(gitPort, uiPort, gitHubService) {
|
|
@@ -10,11 +11,10 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
10
11
|
async execute() {
|
|
11
12
|
try {
|
|
12
13
|
const accion = await this.uiPort.select('Repositorio — ¿qué deseas hacer?', [
|
|
13
|
-
{ name: 'Clonar repositorio remoto', value: 'clonar' },
|
|
14
|
-
{ name: 'Inicializar
|
|
15
|
-
{ name: '
|
|
16
|
-
{ name: '
|
|
17
|
-
{ name: 'Agregar remoto', value: 'agregarRemoto' },
|
|
14
|
+
{ name: '📥 Clonar un repositorio remoto', value: 'clonar' },
|
|
15
|
+
{ name: '🆕 Inicializar un repositorio Git local', value: 'init' },
|
|
16
|
+
{ name: '🚀 Publicar este proyecto local en GitHub', value: 'publicarGitHub' },
|
|
17
|
+
{ name: '🌐 Gestionar enlaces remotos (remotes)', value: 'gestionarRemotos' },
|
|
18
18
|
{ name: '← Volver al menú principal', value: 'volver' },
|
|
19
19
|
]);
|
|
20
20
|
switch (accion) {
|
|
@@ -24,14 +24,11 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
24
24
|
case 'init':
|
|
25
25
|
await this.inicializarRepo();
|
|
26
26
|
break;
|
|
27
|
-
case '
|
|
28
|
-
await this.
|
|
27
|
+
case 'publicarGitHub':
|
|
28
|
+
await this.publicarRepositorioEnGitHub();
|
|
29
29
|
break;
|
|
30
|
-
case '
|
|
31
|
-
await this.
|
|
32
|
-
break;
|
|
33
|
-
case 'agregarRemoto':
|
|
34
|
-
await this.agregarRemoto();
|
|
30
|
+
case 'gestionarRemotos':
|
|
31
|
+
await this.gestionarRemotosSubMenu();
|
|
35
32
|
break;
|
|
36
33
|
case 'volver':
|
|
37
34
|
return;
|
|
@@ -46,9 +43,48 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
46
43
|
this.uiPort.log('');
|
|
47
44
|
this.uiPort.log(c.titulo(' 📥 Clonar repositorio'));
|
|
48
45
|
this.uiPort.separator();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
let url = '';
|
|
47
|
+
const isAuth = await this.gitHubService.isAuthenticated();
|
|
48
|
+
if (isAuth) {
|
|
49
|
+
const modo = await this.uiPort.select('¿Cómo deseas seleccionar el repositorio a clonar?', [
|
|
50
|
+
{ name: '🌐 Seleccionar de mis repositorios de GitHub', value: 'github' },
|
|
51
|
+
{ name: '🔗 Ingresar URL manualmente (HTTPS o SSH)', value: 'manual' },
|
|
52
|
+
]);
|
|
53
|
+
if (modo === 'github') {
|
|
54
|
+
const spin = this.uiPort.spinner('Cargando tus repositorios de GitHub...');
|
|
55
|
+
let repos = [];
|
|
56
|
+
try {
|
|
57
|
+
repos = await this.gitHubService.listRepos();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
this.uiPort.error(` ✗ Error al cargar repositorios: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
spin.stop();
|
|
63
|
+
if (repos.length === 0) {
|
|
64
|
+
this.uiPort.warning(' No se encontraron repositorios en tu cuenta de GitHub.');
|
|
65
|
+
const ingresarManual = await this.uiPort.confirm('¿Deseas ingresar la URL manualmente?', true);
|
|
66
|
+
if (!ingresarManual)
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const selectedRepo = await this.seleccionarRepoPaginado(repos);
|
|
71
|
+
if (!selectedRepo) {
|
|
72
|
+
this.uiPort.warning(' Clonación cancelada.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const metodoClonacion = await this.uiPort.select('Método de clonación:', [
|
|
76
|
+
{ name: 'HTTPS', value: 'https' },
|
|
77
|
+
{ name: 'SSH', value: 'ssh' },
|
|
78
|
+
]);
|
|
79
|
+
url = metodoClonacion === 'https' ? selectedRepo.cloneUrl : selectedRepo.sshUrl;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!url) {
|
|
84
|
+
url = await this.uiPort.input('URL del repositorio (HTTPS o SSH):', {
|
|
85
|
+
validate: (v) => v.trim().length > 0 || 'La URL no puede estar vacía.',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
52
88
|
const nombreSugerido = url.split('/').pop()?.replace('.git', '') || 'repo';
|
|
53
89
|
const destino = await this.uiPort.input('Carpeta de destino:', {
|
|
54
90
|
default: nombreSugerido,
|
|
@@ -72,12 +108,11 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
72
108
|
}
|
|
73
109
|
const spinClone = this.uiPort.spinner('Clonando repositorio...');
|
|
74
110
|
try {
|
|
75
|
-
const git = simpleGit(cwd);
|
|
76
111
|
const opciones = [];
|
|
77
112
|
if (rama.trim()) {
|
|
78
113
|
opciones.push('-b', rama.trim());
|
|
79
114
|
}
|
|
80
|
-
await
|
|
115
|
+
await this.gitPort.clone(url, destino, opciones);
|
|
81
116
|
spinClone.stop();
|
|
82
117
|
this.uiPort.success(`\n ✓ Repositorio clonado en: ${c.archivo(destinoAbsoluto)}\n`);
|
|
83
118
|
}
|
|
@@ -87,6 +122,93 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
87
122
|
this.uiPort.error(`\n ✗ Error al clonar:\n ${msg}\n`);
|
|
88
123
|
}
|
|
89
124
|
}
|
|
125
|
+
async seleccionarRepoPaginado(repos) {
|
|
126
|
+
const itemsPerPage = 10;
|
|
127
|
+
let currentPage = 0;
|
|
128
|
+
const totalPages = Math.ceil(repos.length / itemsPerPage);
|
|
129
|
+
let selectedIndexInPage = 0;
|
|
130
|
+
const isRaw = process.stdin.isRaw;
|
|
131
|
+
process.stdin.setRawMode(true);
|
|
132
|
+
process.stdin.resume();
|
|
133
|
+
readline.emitKeypressEvents(process.stdin);
|
|
134
|
+
const c = this.uiColors;
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const render = () => {
|
|
137
|
+
this.uiPort.clear();
|
|
138
|
+
this.uiPort.showBanner();
|
|
139
|
+
this.uiPort.log(c.titulo(` 🌐 Seleccionar Repositorio (Página ${currentPage + 1} de ${totalPages})`));
|
|
140
|
+
this.uiPort.log(c.tenue(' [←/→] Cambiar página | [↑/↓] Mover cursor | [Enter] Seleccionar | [Esc] Cancelar'));
|
|
141
|
+
this.uiPort.separator();
|
|
142
|
+
const start = currentPage * itemsPerPage;
|
|
143
|
+
const end = Math.min(start + itemsPerPage, repos.length);
|
|
144
|
+
const pageItems = repos.slice(start, end);
|
|
145
|
+
if (selectedIndexInPage >= pageItems.length) {
|
|
146
|
+
selectedIndexInPage = pageItems.length - 1;
|
|
147
|
+
}
|
|
148
|
+
pageItems.forEach((r, idx) => {
|
|
149
|
+
const esSeleccionado = idx === selectedIndexInPage;
|
|
150
|
+
const indicador = esSeleccionado ? ' ❯ ' : ' ';
|
|
151
|
+
const infoText = `${r.fullName} (${r.visibility})`;
|
|
152
|
+
if (esSeleccionado) {
|
|
153
|
+
this.uiPort.log(chalk.bgHex('#313244').hex('#a6da95').bold(` ${indicador}${infoText.padEnd(55)}`));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.uiPort.log(` ${indicador}${infoText}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
this.uiPort.separator();
|
|
160
|
+
const paginador = Array.from({ length: totalPages }).map((_, idx) => {
|
|
161
|
+
return idx === currentPage ? c.exito('●') : c.tenue('○');
|
|
162
|
+
}).join(' ');
|
|
163
|
+
this.uiPort.log(` Páginas: ${paginador}`);
|
|
164
|
+
this.uiPort.separator();
|
|
165
|
+
};
|
|
166
|
+
const handleKeypress = (str, key) => {
|
|
167
|
+
if (!key)
|
|
168
|
+
return;
|
|
169
|
+
const start = currentPage * itemsPerPage;
|
|
170
|
+
const end = Math.min(start + itemsPerPage, repos.length);
|
|
171
|
+
const pageItems = repos.slice(start, end);
|
|
172
|
+
if (key.name === 'up') {
|
|
173
|
+
selectedIndexInPage = (selectedIndexInPage - 1 + pageItems.length) % pageItems.length;
|
|
174
|
+
render();
|
|
175
|
+
}
|
|
176
|
+
else if (key.name === 'down') {
|
|
177
|
+
selectedIndexInPage = (selectedIndexInPage + 1) % pageItems.length;
|
|
178
|
+
render();
|
|
179
|
+
}
|
|
180
|
+
else if (key.name === 'left') {
|
|
181
|
+
if (currentPage > 0) {
|
|
182
|
+
currentPage--;
|
|
183
|
+
selectedIndexInPage = 0;
|
|
184
|
+
render();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (key.name === 'right') {
|
|
188
|
+
if (currentPage < totalPages - 1) {
|
|
189
|
+
currentPage++;
|
|
190
|
+
selectedIndexInPage = 0;
|
|
191
|
+
render();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (key.name === 'return') {
|
|
195
|
+
cleanup();
|
|
196
|
+
resolve(pageItems[selectedIndexInPage]);
|
|
197
|
+
}
|
|
198
|
+
else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
199
|
+
cleanup();
|
|
200
|
+
resolve(null);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
process.stdin.removeListener('keypress', handleKeypress);
|
|
205
|
+
process.stdin.setRawMode(isRaw);
|
|
206
|
+
process.stdin.pause();
|
|
207
|
+
};
|
|
208
|
+
process.stdin.on('keypress', handleKeypress);
|
|
209
|
+
render();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
90
212
|
async inicializarRepo() {
|
|
91
213
|
const c = this.uiColors;
|
|
92
214
|
this.uiPort.log('');
|
|
@@ -111,8 +233,7 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
111
233
|
return;
|
|
112
234
|
const spinInit = this.uiPort.spinner('Inicializando...');
|
|
113
235
|
try {
|
|
114
|
-
|
|
115
|
-
await git.init({ '--initial-branch': ramaInicial.trim() || 'main' });
|
|
236
|
+
await this.gitPort.init(destino, ramaInicial.trim() || 'main');
|
|
116
237
|
spinInit.stop();
|
|
117
238
|
this.uiPort.success(`\n ✓ Repositorio inicializado en: ${c.archivo(destino)}\n`);
|
|
118
239
|
}
|
|
@@ -122,12 +243,162 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
122
243
|
this.uiPort.error(`\n ✗ Error al inicializar:\n ${msg}\n`);
|
|
123
244
|
}
|
|
124
245
|
}
|
|
125
|
-
async
|
|
246
|
+
async publicarRepositorioEnGitHub() {
|
|
247
|
+
const isAuth = await this.gitHubService.isAuthenticated();
|
|
248
|
+
if (!isAuth) {
|
|
249
|
+
this.uiPort.error(' ✗ No estás autenticado con GitHub.');
|
|
250
|
+
this.uiPort.info(' Por favor, ve al menú "🌐 Gestión GitHub" y selecciona "Autenticación" para iniciar sesión.');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const c = this.uiColors;
|
|
254
|
+
this.uiPort.log('');
|
|
255
|
+
this.uiPort.log(c.titulo(' 🚀 Publicar repositorio actual en GitHub'));
|
|
256
|
+
this.uiPort.separator();
|
|
257
|
+
let isRepo = await this.gitPort.isRepository();
|
|
258
|
+
if (!isRepo) {
|
|
259
|
+
this.uiPort.warning(' El directorio actual no es un repositorio Git local.');
|
|
260
|
+
const initConfirm = await this.uiPort.confirm('¿Deseas inicializar un repositorio Git en esta carpeta ahora?', true);
|
|
261
|
+
if (!initConfirm) {
|
|
262
|
+
this.uiPort.warning(' Operación cancelada.');
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const spinInit = this.uiPort.spinner('Inicializando repositorio local...');
|
|
266
|
+
try {
|
|
267
|
+
await this.gitPort.init(process.cwd(), 'main');
|
|
268
|
+
spinInit.stop();
|
|
269
|
+
this.uiPort.success(' ✓ Repositorio Git local inicializado (rama principal: main).');
|
|
270
|
+
isRepo = true;
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
spinInit.stop();
|
|
274
|
+
this.uiPort.error(` ✗ Error al inicializar: ${err.message}`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const remotos = await this.gitPort.getRemotes();
|
|
280
|
+
const originExiste = remotos.some((r) => r.name === 'origin');
|
|
281
|
+
if (originExiste) {
|
|
282
|
+
this.uiPort.warning(' ⚠️ Ya tienes un remoto configurado con el nombre "origin".');
|
|
283
|
+
this.uiPort.info(' Si deseas cambiarlo, ve a la sección de "Gestionar enlaces remotos".');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
// Ignorar e intentar continuar
|
|
289
|
+
}
|
|
290
|
+
const defaultName = path.basename(process.cwd());
|
|
291
|
+
const nombre = await this.uiPort.input('Nombre del repositorio en GitHub:', {
|
|
292
|
+
default: defaultName,
|
|
293
|
+
validate: (v) => v.trim().length > 0 || 'El nombre es obligatorio.',
|
|
294
|
+
});
|
|
295
|
+
const descripcion = await this.uiPort.input('Descripción (opcional):');
|
|
296
|
+
const visibilidad = await this.uiPort.select('Visibilidad:', [
|
|
297
|
+
{ name: 'Público', value: 'public' },
|
|
298
|
+
{ name: 'Privado', value: 'private' },
|
|
299
|
+
]);
|
|
300
|
+
const confirmar = await this.uiPort.confirm(`¿Quieres crear el repositorio "${nombre}" (${visibilidad}) en GitHub y subir el código actual?`, true);
|
|
301
|
+
if (!confirmar) {
|
|
302
|
+
this.uiPort.warning(' Operación cancelada.');
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const spinGH = this.uiPort.spinner('Creando repositorio en GitHub...');
|
|
306
|
+
let repoCreado;
|
|
307
|
+
try {
|
|
308
|
+
repoCreado = await this.gitHubService.createRepo({
|
|
309
|
+
name: nombre.trim(),
|
|
310
|
+
description: descripcion.trim() || undefined,
|
|
311
|
+
visibility: visibilidad,
|
|
312
|
+
autoInit: false,
|
|
313
|
+
});
|
|
314
|
+
spinGH.stop();
|
|
315
|
+
this.uiPort.success(` ✓ Repositorio creado en GitHub: ${repoCreado.htmlUrl}`);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
spinGH.stop();
|
|
319
|
+
this.uiPort.error(` ✗ Error al crear repositorio en GitHub: ${err.message}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const spinConfig = this.uiPort.spinner('Vinculando repositorio remoto...');
|
|
323
|
+
try {
|
|
324
|
+
await this.gitPort.addRemote('origin', repoCreado.sshUrl || repoCreado.cloneUrl);
|
|
325
|
+
spinConfig.stop();
|
|
326
|
+
this.uiPort.success(` ✓ Remoto "origin" vinculado con éxito.`);
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
spinConfig.stop();
|
|
330
|
+
this.uiPort.error(` ✗ Error al vincular el remoto: ${err.message}`);
|
|
331
|
+
this.uiPort.info(` Puedes agregarlo manualmente con la URL: ${repoCreado.cloneUrl}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const status = await this.gitPort.getStatus();
|
|
335
|
+
const ramaActual = status.current || 'main';
|
|
336
|
+
const realizarPush = await this.uiPort.confirm(`¿Deseas subir la rama actual "${ramaActual}" a GitHub ahora mismo? (push)`, true);
|
|
337
|
+
if (realizarPush) {
|
|
338
|
+
const spinPush = this.uiPort.spinner(`Subiendo rama "${ramaActual}" a GitHub (origin)...`);
|
|
339
|
+
try {
|
|
340
|
+
await this.gitPort.push('origin', ramaActual, ['-u', 'origin', ramaActual]);
|
|
341
|
+
spinPush.stop();
|
|
342
|
+
this.uiPort.success(`\n ✓ ¡Todo listo! Tu código se ha subido a GitHub.`);
|
|
343
|
+
this.uiPort.log(` 🔗 Puedes verlo en: ${c.info(repoCreado.htmlUrl)}\n`);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
spinPush.stop();
|
|
347
|
+
this.uiPort.error(`\n ✗ Error al subir el código: ${err.message}`);
|
|
348
|
+
this.uiPort.info(' Asegúrate de tener commits hechos antes de subir cambios.');
|
|
349
|
+
this.uiPort.info(` Puedes intentar subirlo manualmente ejecutando: git push -u origin ${ramaActual}\n`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.uiPort.info(`\n Perfecto. Puedes subir tu código en cualquier momento usando la opción de "Push" desde el menú principal.\n`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async gestionarRemotosSubMenu() {
|
|
126
357
|
const isRepo = await this.gitPort.isRepository();
|
|
127
358
|
if (!isRepo) {
|
|
128
359
|
this.uiPort.error(' ✗ Este directorio no es un repositorio Git.');
|
|
129
360
|
return;
|
|
130
361
|
}
|
|
362
|
+
const c = this.uiColors;
|
|
363
|
+
let continuar = true;
|
|
364
|
+
while (continuar) {
|
|
365
|
+
this.uiPort.log('');
|
|
366
|
+
this.uiPort.log(c.titulo(' 🌐 Gestión de Enlaces Remotos (Remotes)'));
|
|
367
|
+
this.uiPort.separator();
|
|
368
|
+
const accion = await this.uiPort.select('¿Qué deseas hacer con los remotos?', [
|
|
369
|
+
{ name: '📋 Ver remotos configurados', value: 'ver' },
|
|
370
|
+
{ name: '➕ Agregar un nuevo remoto', value: 'agregar' },
|
|
371
|
+
{ name: '✏️ Cambiar URL de un remoto', value: 'url' },
|
|
372
|
+
{ name: '🏷️ Renombrar un remoto', value: 'renombrar' },
|
|
373
|
+
{ name: '❌ Eliminar un remoto', value: 'eliminar' },
|
|
374
|
+
{ name: '← Volver', value: 'volver' },
|
|
375
|
+
]);
|
|
376
|
+
switch (accion) {
|
|
377
|
+
case 'ver':
|
|
378
|
+
await this.verRemotos();
|
|
379
|
+
break;
|
|
380
|
+
case 'agregar':
|
|
381
|
+
await this.agregarRemoto();
|
|
382
|
+
break;
|
|
383
|
+
case 'url':
|
|
384
|
+
await this.cambiarUrlRemoto();
|
|
385
|
+
break;
|
|
386
|
+
case 'renombrar':
|
|
387
|
+
await this.renombrarRemoto();
|
|
388
|
+
break;
|
|
389
|
+
case 'eliminar':
|
|
390
|
+
await this.eliminarRemoto();
|
|
391
|
+
break;
|
|
392
|
+
case 'volver':
|
|
393
|
+
continuar = false;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
if (continuar && accion !== 'volver') {
|
|
397
|
+
await this.uiPort.pause();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async verRemotos() {
|
|
131
402
|
const spin = this.uiPort.spinner('Cargando remotos...');
|
|
132
403
|
const remotos = await this.gitPort.getRemotes();
|
|
133
404
|
spin.stop();
|
|
@@ -147,11 +418,6 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
147
418
|
this.uiPort.log('');
|
|
148
419
|
}
|
|
149
420
|
async agregarRemoto() {
|
|
150
|
-
const isRepo = await this.gitPort.isRepository();
|
|
151
|
-
if (!isRepo) {
|
|
152
|
-
this.uiPort.error(' ✗ Este directorio no es un repositorio Git.');
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
421
|
const nombre = await this.uiPort.input('Nombre del remoto:', {
|
|
156
422
|
default: 'origin',
|
|
157
423
|
validate: (v) => v.trim().length > 0 || 'Nombre requerido.',
|
|
@@ -171,98 +437,76 @@ export class RepositoryCommand extends BaseCommand {
|
|
|
171
437
|
this.uiPort.error(`\n ✗ Error:\n ${msg}\n`);
|
|
172
438
|
}
|
|
173
439
|
}
|
|
174
|
-
async
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
this.uiPort.error(' ✗ No estás autenticado con GitHub.');
|
|
179
|
-
this.uiPort.info(' Ve al menú "🌐 Gestión GitHub" primero para iniciar sesión.');
|
|
440
|
+
async cambiarUrlRemoto() {
|
|
441
|
+
const remotes = await this.gitPort.getRemotes();
|
|
442
|
+
if (remotes.length === 0) {
|
|
443
|
+
this.uiPort.warning(' No hay remotos configurados.');
|
|
180
444
|
return;
|
|
181
445
|
}
|
|
182
|
-
const
|
|
183
|
-
this.uiPort.
|
|
184
|
-
this.uiPort.
|
|
185
|
-
|
|
186
|
-
// Paso 1: Obtener datos del repositorio
|
|
187
|
-
const nombreCarpeta = await this.uiPort.input('Nombre de la carpeta y del repositorio:', {
|
|
188
|
-
validate: (v) => v.trim().length > 0 || 'El nombre no puede estar vacío.',
|
|
189
|
-
});
|
|
190
|
-
const descripcion = await this.uiPort.input('Descripción del repositorio (opcional):', {
|
|
191
|
-
default: '',
|
|
446
|
+
const choices = remotes.map((r) => ({ name: `${r.name} (${r.refs?.fetch || 'no-url'})`, value: r.name }));
|
|
447
|
+
const name = await this.uiPort.select('Selecciona el remoto a modificar:', choices);
|
|
448
|
+
const url = await this.uiPort.input('Ingresa la nueva URL para el remoto:', {
|
|
449
|
+
validate: (v) => v.trim().length > 0 || 'La URL es requerida.',
|
|
192
450
|
});
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
this.uiPort.log(c.info(` Visibilidad: ${visibilidad}`));
|
|
209
|
-
if (descripcion.trim())
|
|
210
|
-
this.uiPort.log(c.tenue(` Descripción: ${descripcion}`));
|
|
211
|
-
this.uiPort.separator();
|
|
212
|
-
const confirmar = await this.uiPort.confirm('¿Confirmar y continuar?', true);
|
|
213
|
-
if (!confirmar) {
|
|
214
|
-
this.uiPort.warning(' Operación cancelada.');
|
|
451
|
+
const spin = this.uiPort.spinner('Actualizando URL del remoto...');
|
|
452
|
+
try {
|
|
453
|
+
await this.gitPort.setRemoteUrl(name, url.trim());
|
|
454
|
+
spin.stop();
|
|
455
|
+
this.uiPort.success(`\n ✓ La URL del remoto "${name}" ha sido cambiada a: ${url.trim()}\n`);
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
spin.stop();
|
|
459
|
+
this.uiPort.error(`\n ✗ Error al cambiar URL: ${err.message}\n`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async renombrarRemoto() {
|
|
463
|
+
const remotes = await this.gitPort.getRemotes();
|
|
464
|
+
if (remotes.length === 0) {
|
|
465
|
+
this.uiPort.warning(' No hay remotos configurados.');
|
|
215
466
|
return;
|
|
216
467
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
468
|
+
const choices = remotes.map((r) => ({ name: r.name, value: r.name }));
|
|
469
|
+
const oldName = await this.uiPort.select('Selecciona el remoto a renombrar:', choices);
|
|
470
|
+
const newName = await this.uiPort.input('Ingresa el nuevo nombre:', {
|
|
471
|
+
validate: (v) => {
|
|
472
|
+
if (!v.trim())
|
|
473
|
+
return 'El nombre es requerido.';
|
|
474
|
+
if (/\s/.test(v))
|
|
475
|
+
return 'El nombre no puede tener espacios.';
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
const spin = this.uiPort.spinner('Renombrando remoto...');
|
|
220
480
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
visibility: visibilidad,
|
|
225
|
-
autoInit: autoInit,
|
|
226
|
-
gitignoreTemplate: gitignore?.trim(),
|
|
227
|
-
licenseTemplate: licencia?.trim(),
|
|
228
|
-
});
|
|
229
|
-
spinGH.stop();
|
|
230
|
-
this.uiPort.success(c.exito(` ✓ Repo creado en GitHub: ${repoCreado.htmlUrl}`));
|
|
481
|
+
await this.gitPort.renameRemote(oldName, newName.trim());
|
|
482
|
+
spin.stop();
|
|
483
|
+
this.uiPort.success(`\n ✓ Remoto renombrado con éxito: ${oldName} → ${newName.trim()}\n`);
|
|
231
484
|
}
|
|
232
485
|
catch (err) {
|
|
233
|
-
|
|
234
|
-
this.uiPort.error(
|
|
486
|
+
spin.stop();
|
|
487
|
+
this.uiPort.error(`\n ✗ Error al renombrar remoto: ${err.message}\n`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
async eliminarRemoto() {
|
|
491
|
+
const remotes = await this.gitPort.getRemotes();
|
|
492
|
+
if (remotes.length === 0) {
|
|
493
|
+
this.uiPort.warning(' No hay remotos configurados.');
|
|
235
494
|
return;
|
|
236
495
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
496
|
+
const choices = remotes.map((r) => ({ name: `${r.name} (${r.refs?.fetch || 'no-url'})`, value: r.name }));
|
|
497
|
+
const name = await this.uiPort.select('Selecciona el remoto a eliminar:', choices);
|
|
498
|
+
const confirmar = await this.uiPort.confirm(`¿Seguro que deseas eliminar el remoto "${name}"? Se perderá la vinculación remota.`, false);
|
|
499
|
+
if (!confirmar)
|
|
500
|
+
return;
|
|
501
|
+
const spin = this.uiPort.spinner('Eliminando remoto...');
|
|
239
502
|
try {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// Ir a la carpeta
|
|
244
|
-
const gitRepo = simpleGit(destino);
|
|
245
|
-
// Verificar que existan archivos (si usamos autoInit)
|
|
246
|
-
if (autoInit) {
|
|
247
|
-
// Ya tiene contenido, solo confirmar
|
|
248
|
-
spinLocal.stop();
|
|
249
|
-
this.uiPort.success(c.exito(` ✓ Listo! Repositorio en: ${destino}`));
|
|
250
|
-
this.uiPort.log(c.tenue(` URL HTTPS: ${repoCreado.cloneUrl}`));
|
|
251
|
-
this.uiPort.log(c.tenue(` URL SSH: ${repoCreado.sshUrl}`));
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
// Si no usamos autoInit, agregar algún contenido inicial
|
|
255
|
-
spinLocal.stop();
|
|
256
|
-
this.uiPort.info(' Repo creado sin contenido inicial. Puedes agregar archivos y hacer push manualmente.');
|
|
257
|
-
this.uiPort.log(c.tenue(` URL HTTPS: ${repoCreado.cloneUrl}`));
|
|
258
|
-
this.uiPort.log(c.tenue(` URL SSH: ${repoCreado.sshUrl}`));
|
|
259
|
-
}
|
|
260
|
-
this.uiPort.log('');
|
|
503
|
+
await this.gitPort.removeRemote(name);
|
|
504
|
+
spin.stop();
|
|
505
|
+
this.uiPort.success(`\n ✓ Remoto "${name}" eliminado correctamente.\n`);
|
|
261
506
|
}
|
|
262
507
|
catch (err) {
|
|
263
|
-
|
|
264
|
-
this.uiPort.error(
|
|
265
|
-
this.uiPort.warning(' El repositorio sigue existiendo en GitHub.');
|
|
508
|
+
spin.stop();
|
|
509
|
+
this.uiPort.error(`\n ✗ Error al eliminar remoto: ${err.message}\n`);
|
|
266
510
|
}
|
|
267
511
|
}
|
|
268
512
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { BaseGitHubCommand } from './BaseGitHubCommand.js';
|
|
2
|
-
import { simpleGit } from 'simple-git';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
export class GitHubReposCommand extends BaseGitHubCommand {
|
|
5
4
|
async execute() {
|
|
@@ -196,8 +195,7 @@ export class GitHubReposCommand extends BaseGitHubCommand {
|
|
|
196
195
|
const dest = await this.uiPort.input('Carpeta de destino:', { default: defaultDest });
|
|
197
196
|
const spinClone = this.uiPort.spinner('Clonando repositorio...');
|
|
198
197
|
try {
|
|
199
|
-
|
|
200
|
-
await git.clone(cloneUrl, dest.trim());
|
|
198
|
+
await this.gitPort.clone(cloneUrl, dest.trim());
|
|
201
199
|
spinClone.stop();
|
|
202
200
|
const destPath = path.join(process.cwd(), dest.trim());
|
|
203
201
|
this.uiPort.success(` ✓ Repositorio clonado en: ${c.info(destPath)}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { GitHubService } from './GitHubService.js';
|
|
3
|
+
describe('GitHubService', () => {
|
|
4
|
+
let mockGitHubPort;
|
|
5
|
+
let mockStorage;
|
|
6
|
+
let service;
|
|
7
|
+
class MockSecureStorage {
|
|
8
|
+
store = new Map();
|
|
9
|
+
async save(key, value) { this.store.set(key, value); }
|
|
10
|
+
async load(key) { return this.store.get(key) || null; }
|
|
11
|
+
async delete(key) { this.store.delete(key); }
|
|
12
|
+
async exists(key) { return this.store.has(key); }
|
|
13
|
+
}
|
|
14
|
+
class MockGitHubPort {
|
|
15
|
+
authenticatedToken = null;
|
|
16
|
+
validated = false;
|
|
17
|
+
currentUser = { login: 'testuser', name: 'Test User' };
|
|
18
|
+
async authenticate(token) { this.authenticatedToken = token; }
|
|
19
|
+
async validateToken() { this.validated = true; return true; }
|
|
20
|
+
async getCurrentUser() { return this.currentUser; }
|
|
21
|
+
// Stub implementations for the rest to satisfy interface
|
|
22
|
+
async listRepos() { return []; }
|
|
23
|
+
async getRepo() { return {}; }
|
|
24
|
+
async createRepo() { return {}; }
|
|
25
|
+
async archiveRepo() { }
|
|
26
|
+
async unarchiveRepo() { }
|
|
27
|
+
async deleteRepo() { }
|
|
28
|
+
async listWorkflows() { return []; }
|
|
29
|
+
async listWorkflowRuns() { return []; }
|
|
30
|
+
async enableWorkflow() { }
|
|
31
|
+
async disableWorkflow() { }
|
|
32
|
+
async triggerWorkflowDispatch() { }
|
|
33
|
+
}
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockGitHubPort = new MockGitHubPort();
|
|
36
|
+
mockStorage = new MockSecureStorage();
|
|
37
|
+
service = new GitHubService(mockGitHubPort, mockStorage);
|
|
38
|
+
});
|
|
39
|
+
it('debería retornar falso en isAuthenticated si no hay token guardado', async () => {
|
|
40
|
+
const isAuth = await service.isAuthenticated();
|
|
41
|
+
expect(isAuth).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it('debería retornar verdadero en isAuthenticated si hay un token válido guardado', async () => {
|
|
44
|
+
await mockStorage.save('github_pat', 'my-valid-token');
|
|
45
|
+
const isAuth = await service.isAuthenticated();
|
|
46
|
+
expect(isAuth).toBe(true);
|
|
47
|
+
expect(mockGitHubPort.authenticatedToken).toBe('my-valid-token');
|
|
48
|
+
});
|
|
49
|
+
it('debería iniciar sesión correctamente guardando el token y obteniendo el usuario', async () => {
|
|
50
|
+
await service.login('new-token');
|
|
51
|
+
expect(await mockStorage.load('github_pat')).toBe('new-token');
|
|
52
|
+
expect(mockGitHubPort.authenticatedToken).toBe('new-token');
|
|
53
|
+
const user = await service.getCurrentUser();
|
|
54
|
+
expect(user.login).toBe('testuser');
|
|
55
|
+
});
|
|
56
|
+
it('debería cerrar sesión eliminando el token guardado', async () => {
|
|
57
|
+
await mockStorage.save('github_pat', 'token-to-delete');
|
|
58
|
+
await service.logout();
|
|
59
|
+
expect(await mockStorage.exists('github_pat')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,26 @@ import { SimpleGitAdapter } from './adapters/git/SimpleGitAdapter.js';
|
|
|
3
3
|
import { ConsoleUIAdapter } from './adapters/ui/ConsoleUIAdapter.js';
|
|
4
4
|
import { GitHubAPIAdapter } from './adapters/github/GitHubAPIAdapter.js';
|
|
5
5
|
import { FileSecureStorageAdapter } from './adapters/storage/FileSecureStorageAdapter.js';
|
|
6
|
-
import { CommandFactory } from './
|
|
6
|
+
import { CommandFactory } from './commands/CommandFactory.js';
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
function checkGitInstalled() {
|
|
9
|
+
try {
|
|
10
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
7
17
|
async function main() {
|
|
8
|
-
const gitPort = new SimpleGitAdapter();
|
|
9
18
|
const uiPort = new ConsoleUIAdapter();
|
|
19
|
+
if (!checkGitInstalled()) {
|
|
20
|
+
uiPort.error('\n ✗ Error: Git no está instalado o no se encuentra en el PATH del sistema.');
|
|
21
|
+
uiPort.info(' Por favor instala Git y asegúrate de que esté configurado en tu variable de entorno PATH.');
|
|
22
|
+
uiPort.info(' Para más información visita: https://git-scm.com\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const gitPort = new SimpleGitAdapter();
|
|
10
26
|
const gitHubPort = new GitHubAPIAdapter();
|
|
11
27
|
const storage = new FileSecureStorageAdapter();
|
|
12
28
|
const commandFactory = new CommandFactory(gitPort, uiPort, gitHubPort, storage);
|
|
@@ -19,6 +35,7 @@ async function main() {
|
|
|
19
35
|
const opcion = await uiPort.select('¿Qué deseas hacer?', [
|
|
20
36
|
{ name: '📋 Estado del repositorio', value: 'status' },
|
|
21
37
|
{ name: '📜 Ver historial (log)', value: 'log' },
|
|
38
|
+
{ name: '🔍 Ver diferencias (diff)', value: 'diff' },
|
|
22
39
|
{ name: '─────────────────────────────', value: 'sep1', disabled: true },
|
|
23
40
|
{ name: '✅ Commit (add + commit)', value: 'commit' },
|
|
24
41
|
{ name: '↑ Push (subir cambios)', value: 'push' },
|