@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.
@@ -1,6 +1,7 @@
1
1
  import { BaseCommand } from './BaseCommand.js';
2
2
  import path from 'path';
3
- import { simpleGit } from 'simple-git';
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 nuevo repositorio', value: 'init' },
15
- { name: 'Crear y subir a GitHub', value: 'initGitHub' },
16
- { name: 'Ver remotos configurados', value: 'remotos' },
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 'initGitHub':
28
- await this.inicializarYSubirAGitHub();
27
+ case 'publicarGitHub':
28
+ await this.publicarRepositorioEnGitHub();
29
29
  break;
30
- case 'remotos':
31
- await this.verRemotos();
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
- const url = await this.uiPort.input('URL del repositorio (HTTPS o SSH):', {
50
- validate: (v) => v.trim().length > 0 || 'La URL no puede estar vacía.',
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 git.clone(url, destino, opciones);
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
- const git = simpleGit(destino);
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 verRemotos() {
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 inicializarYSubirAGitHub() {
175
- // Primero, verificar que el usuario esté autenticado
176
- const isAuth = await this.gitHubService.isAuthenticated();
177
- if (!isAuth) {
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 c = this.uiColors;
183
- this.uiPort.log('');
184
- this.uiPort.log(c.titulo(' 🚀 Crear repo y subir a GitHub'));
185
- this.uiPort.separator();
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 visibilidad = await this.uiPort.select('Visibilidad:', [
194
- { name: 'Público', value: 'public' },
195
- { name: 'Privado', value: 'private' },
196
- ]);
197
- const autoInit = await this.uiPort.confirm('¿Agregar README.md inicial?', true);
198
- const gitignore = autoInit ? await this.uiPort.input('Plantilla .gitignore (ej: Node, dejar vacío para ninguna):', { default: '' }) : undefined;
199
- const licencia = autoInit ? await this.uiPort.input('Licencia (ej: mit, dejar vacío para ninguna):', { default: '' }) : undefined;
200
- const ramaInicial = await this.uiPort.input('Nombre de la rama inicial:', {
201
- default: 'main',
202
- });
203
- // Calcular destino
204
- const destino = path.join(process.cwd(), nombreCarpeta.trim());
205
- this.uiPort.separator();
206
- this.uiPort.log(c.titulo(' Resumen:'));
207
- this.uiPort.log(c.info(` Carpeta: ${destino}`));
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
- // Paso 2: Crear repo en GitHub
218
- const spinGH = this.uiPort.spinner('Creando repositorio en GitHub...');
219
- let repoCreado;
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
- repoCreado = await this.gitHubService.createRepo({
222
- name: nombreCarpeta.trim(),
223
- description: descripcion.trim() || undefined,
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
- spinGH.stop();
234
- this.uiPort.error(` ✗ Error al crear repo en GitHub: ${err.message}`);
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
- // Paso 3: Clonar o inicializar localmente y subir
238
- const spinLocal = this.uiPort.spinner('Configurando repositorio local...');
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
- const git = simpleGit();
241
- // Clonar el repo que acabamos de crear
242
- await git.clone(repoCreado.cloneUrl, nombreCarpeta.trim());
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
- spinLocal.stop();
264
- this.uiPort.error(` ✗ Error al configurar localmente: ${err.message}`);
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
- const git = simpleGit(process.cwd());
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 './factories/CommandFactory.js';
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' },