@asaro/git-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +189 -0
  2. package/dist/adapters/git/SimpleGitAdapter.js +126 -0
  3. package/dist/adapters/github/GitHubAPIAdapter.js +243 -0
  4. package/dist/adapters/storage/FileSecureStorageAdapter.js +67 -0
  5. package/dist/adapters/ui/ConsoleUIAdapter.js +91 -0
  6. package/dist/commands/BaseCommand.js +11 -0
  7. package/dist/commands/BranchesCommand.js +152 -0
  8. package/dist/commands/Command.js +1 -0
  9. package/dist/commands/CommitCommand.js +85 -0
  10. package/dist/commands/IntegrationCommand.js +151 -0
  11. package/dist/commands/LogCommand.js +42 -0
  12. package/dist/commands/PullCommand.js +55 -0
  13. package/dist/commands/PushCommand.js +227 -0
  14. package/dist/commands/RepositoryCommand.js +268 -0
  15. package/dist/commands/StashCommand.js +140 -0
  16. package/dist/commands/StatusCommand.js +66 -0
  17. package/dist/commands/github/BaseGitHubCommand.js +17 -0
  18. package/dist/commands/github/GitHubAuthCommand.js +48 -0
  19. package/dist/commands/github/GitHubMenuCommand.js +57 -0
  20. package/dist/commands/github/GitHubReposCommand.js +210 -0
  21. package/dist/commands/github/GitHubWorkflowsCommand.js +243 -0
  22. package/dist/commit.js +98 -0
  23. package/dist/core/errors/GitError.js +19 -0
  24. package/dist/core/errors/GitHubError.js +36 -0
  25. package/dist/core/ports/GitHubPort.js +1 -0
  26. package/dist/core/ports/GitPort.js +1 -0
  27. package/dist/core/ports/SecureStoragePort.js +1 -0
  28. package/dist/core/ports/UIPort.js +1 -0
  29. package/dist/core/services/GitHubService.js +95 -0
  30. package/dist/core/services/GitService.js +44 -0
  31. package/dist/estado.js +97 -0
  32. package/dist/factories/CommandFactory.js +51 -0
  33. package/dist/index.js +71 -0
  34. package/dist/integracion.js +152 -0
  35. package/dist/ramas.js +178 -0
  36. package/dist/remoto.js +129 -0
  37. package/dist/repositorio.js +159 -0
  38. package/dist/stash.js +169 -0
  39. package/dist/utils.js +49 -0
  40. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Git CLI (@asaro/git-cli)
2
+
3
+ Git CLI interactivo en español para gestionar repositorios Git y GitHub desde la terminal.
4
+
5
+ ## Características
6
+
7
+ - 📋 Estado del repositorio, historial de commits
8
+ - ✅ Commit, Push, Pull
9
+ - 🌿 Gestión de ramas
10
+ - 🔀 Merge y Rebase
11
+ - 📦 Stash
12
+ - 🆕 Repositorios (clone, init, remotos)
13
+ - 🌐 Integración con GitHub
14
+ - Autenticación segura
15
+ - Crear y gestionar repositorios
16
+ - GitHub Actions
17
+ - 🔐 Token encriptado guardado localmente
18
+
19
+ ## Instalación
20
+
21
+ ### Requisitos previos
22
+
23
+ - Node.js (v18 o superior)
24
+ - npm (v9 o superior)
25
+ - Git instalado y configurado
26
+
27
+ ### Métodos de instalación
28
+
29
+ #### 1. Instalación global desde npm (la forma más fácil!)
30
+
31
+ Si el paquete está publicado en npm:
32
+
33
+ ```bash
34
+ # Instálalo globalmente en tu PC
35
+ npm install -g @itzasaro/git-cli
36
+
37
+ # Ahora puedes usar el comando 'git-cli' desde cualquier lugar!
38
+ git-cli
39
+ ```
40
+
41
+ #### 2. Instalación desde el repositorio
42
+
43
+ Si quieres instalarlo directamente desde el código fuente:
44
+
45
+ ```bash
46
+ # Clona el repositorio
47
+ git clone https://github.com/iTzAsaro/GitCLI.git
48
+ cd gitcli
49
+
50
+ # Instala dependencias y compila
51
+ npm install
52
+ npm run build
53
+
54
+ # Instálalo globalmente
55
+ npm install -g .
56
+
57
+ # Ahora puedes usar el comando 'git-cli' desde cualquier lugar!
58
+ git-cli
59
+ ```
60
+
61
+ #### 3. Usar desde la carpeta del proyecto
62
+
63
+ Si prefieres no instalarlo globalmente:
64
+
65
+ ```bash
66
+ npm install
67
+ npm run build
68
+ npm start
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Publicar tu paquete en npm
74
+
75
+ Si quieres que otras personas puedan instalarlo con `npm install -g gitcli`, sigue estos pasos:
76
+
77
+ ### Paso 1: Crear una cuenta en npm
78
+
79
+ 1. Ve a [https://www.npmjs.com/signup](https://www.npmjs.com/signup)
80
+ 2. Crea tu cuenta y verifica tu email
81
+
82
+ ### Paso 2: Iniciar sesión en npm desde la terminal
83
+
84
+ ```bash
85
+ npm login
86
+ ```
87
+
88
+ Ingresa tu username, password y email de npm.
89
+
90
+ ### Paso 3: Actualizar datos del paquete
91
+
92
+ Edita `package.json` y cambia:
93
+ - `name`: Asegúrate de que el nombre esté disponible en npm
94
+ - `author`: Tu nombre y email
95
+ - `repository`: La URL de tu repo en GitHub
96
+ - `homepage` y `bugs`: URLs de tu repo
97
+
98
+ ### Paso 4: Publicar!
99
+
100
+ Como es un paquete con scope (`@iTzAsaro/`), necesitas publicar con acceso público:
101
+
102
+ ```bash
103
+ npm publish --access public
104
+ ```
105
+
106
+ ¡Listo! Ahora cualquier persona puede instalar tu paquete con:
107
+
108
+ ```bash
109
+ npm install -g @itzasaro/git-cli
110
+ ```
111
+
112
+ ### Para publicar una nueva versión
113
+
114
+ Siempre que quieras publicar una actualización:
115
+
116
+ 1. Actualiza la versión en `package.json` (usa [Semantic Versioning](https://semver.org/))
117
+ 2. Ejecuta:
118
+ ```bash
119
+ npm publish --access public
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Uso
125
+
126
+ Ejecuta el comando y sigue las instrucciones en la terminal:
127
+
128
+ ```bash
129
+ git-cli
130
+ ```
131
+
132
+ ## Configuración de GitHub
133
+
134
+ 1. Abre GitCLI
135
+ 2. Ve a **"🌐 Gestión GitHub"**
136
+ 3. Selecciona **"Autenticación (Login/Logout)"**
137
+ 4. Ingresa tu Personal Access Token de GitHub
138
+ 5. ¡Listo para usar todas las funcionalidades de GitHub!
139
+
140
+ ### Cómo crear un Personal Access Token:
141
+
142
+ 1. Ve a https://github.com/settings/tokens/new
143
+ 2. Selecciona los scopes:
144
+ - `repo` (para repositorios)
145
+ - `workflow` (para GitHub Actions)
146
+ - `delete_repo` (para borrar repositorios, opcional)
147
+ 3. Genera y copia el token
148
+
149
+ ## Funcionalidades
150
+
151
+ ### Menú principal
152
+
153
+ - **📋 Estado del repositorio** - Ver el estado actual del repo
154
+ - **📜 Ver historial (log)** - Ver commits y cambios
155
+ - **✅ Commit** - Hacer commit de cambios
156
+ - **↑ Push** - Subir cambios a remoto
157
+ - **↓ Pull** - Bajar cambios de remoto
158
+ - **🌿 Ramas** - Gestionar ramas
159
+ - **🔀 Merge / Rebase** - Fusionar cambios
160
+ - **📦 Stash** - Guardar cambios temporalmente
161
+ - **🆕 Repositorio** - Clonar, init o gestionar remotos
162
+ - **🌐 Gestión GitHub** - Gestionar GitHub
163
+ - **🚪 Salir** - Cerrar la aplicación
164
+
165
+ ## Actualización
166
+
167
+ Si instalaste globalmente y quieres actualizar:
168
+
169
+ ```bash
170
+ cd gitcli
171
+ git pull
172
+ npm install
173
+ npm run build
174
+ npm install -g .
175
+ ```
176
+
177
+ ## Desinstalación
178
+
179
+ ```bash
180
+ npm uninstall -g @itzasaro/git-cli
181
+ ```
182
+
183
+ ## Archivos de configuración
184
+
185
+ - El token de GitHub se guarda encriptado en `~/.gitcli/`
186
+
187
+ ## Licencia
188
+
189
+ ISC
@@ -0,0 +1,126 @@
1
+ import { simpleGit } from 'simple-git';
2
+ export class SimpleGitAdapter {
3
+ git;
4
+ constructor(cwd) {
5
+ this.git = simpleGit(cwd || process.cwd());
6
+ }
7
+ async isRepository() {
8
+ try {
9
+ await this.git.revparse(['--git-dir']);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ getStatus() {
17
+ return this.git.status();
18
+ }
19
+ getLog(maxCount = 10) {
20
+ return this.git.log({ maxCount });
21
+ }
22
+ getBranches() {
23
+ return this.git.branch(['-a']);
24
+ }
25
+ getRemotes() {
26
+ return this.git.getRemotes(true);
27
+ }
28
+ async add(files) {
29
+ await this.git.add(files);
30
+ }
31
+ async commit(message) {
32
+ await this.git.commit(message);
33
+ }
34
+ async push(remote, branch, options) {
35
+ if (options && options.length > 0) {
36
+ await this.git.push(options);
37
+ }
38
+ else if (remote && branch) {
39
+ await this.git.push(remote, branch);
40
+ }
41
+ else {
42
+ await this.git.push();
43
+ }
44
+ }
45
+ async pull(remote, branch, options) {
46
+ if (remote && branch) {
47
+ await this.git.pull(remote, branch, options);
48
+ }
49
+ else {
50
+ await this.git.pull(options);
51
+ }
52
+ }
53
+ async checkout(branch) {
54
+ await this.git.checkout(branch);
55
+ }
56
+ async checkoutLocalBranch(branch) {
57
+ await this.git.checkoutLocalBranch(branch);
58
+ }
59
+ async createBranch(branch) {
60
+ await this.git.branch([branch]);
61
+ }
62
+ async deleteBranch(branch, force = false) {
63
+ await this.git.deleteLocalBranch(branch, force);
64
+ }
65
+ async renameBranch(oldName, newName) {
66
+ await this.git.branch(['-m', oldName, newName]);
67
+ }
68
+ async merge(branch, options) {
69
+ if (options) {
70
+ await this.git.merge([branch, ...options]);
71
+ }
72
+ else {
73
+ await this.git.merge([branch]);
74
+ }
75
+ }
76
+ async rebase(branch) {
77
+ await this.git.rebase([branch]);
78
+ }
79
+ async abortMerge() {
80
+ await this.git.merge(['--abort']);
81
+ }
82
+ async abortRebase() {
83
+ await this.git.rebase(['--abort']);
84
+ }
85
+ async stashPush(message, includeUntracked = false) {
86
+ const args = ['push'];
87
+ if (message)
88
+ args.push('-m', message);
89
+ if (includeUntracked)
90
+ args.push('--include-untracked');
91
+ await this.git.stash(args);
92
+ }
93
+ stashList() {
94
+ return this.git.stashList();
95
+ }
96
+ async stashApply(stashId) {
97
+ await this.git.stash(['apply', stashId]);
98
+ }
99
+ async stashPop(stashId) {
100
+ await this.git.stash(['pop', stashId]);
101
+ }
102
+ async stashDrop(stashId) {
103
+ await this.git.stash(['drop', stashId]);
104
+ }
105
+ async stashClear() {
106
+ await this.git.stash(['clear']);
107
+ }
108
+ async clone(url, destination, options) {
109
+ await this.git.clone(url, destination, options);
110
+ }
111
+ async init(destination, initialBranch = 'main') {
112
+ if (destination) {
113
+ const git = simpleGit(destination);
114
+ await git.init({ '--initial-branch': initialBranch });
115
+ }
116
+ else {
117
+ await this.git.init({ '--initial-branch': initialBranch });
118
+ }
119
+ }
120
+ async addRemote(name, url) {
121
+ await this.git.addRemote(name, url);
122
+ }
123
+ getGitInstance() {
124
+ return this.git;
125
+ }
126
+ }
@@ -0,0 +1,243 @@
1
+ import { Octokit } from 'octokit';
2
+ import { GitHubAuthError, GitHubRateLimitError, GitHubPermissionError, GitHubNotFoundError, GitHubError, } from '../../core/errors/GitHubError.js';
3
+ export class GitHubAPIAdapter {
4
+ octokit = null;
5
+ async authenticate(token) {
6
+ this.octokit = new Octokit({ auth: token });
7
+ }
8
+ async validateToken() {
9
+ if (!this.octokit)
10
+ throw new GitHubAuthError('No hay token configurado');
11
+ await this.octokit.rest.users.getAuthenticated();
12
+ return true;
13
+ }
14
+ async getCurrentUser() {
15
+ if (!this.octokit)
16
+ throw new GitHubAuthError('No hay token configurado');
17
+ const { data } = await this.octokit.rest.users.getAuthenticated();
18
+ return { login: data.login, name: data.name };
19
+ }
20
+ async listRepos(filters) {
21
+ if (!this.octokit)
22
+ throw new GitHubAuthError('No hay token configurado');
23
+ const { data } = await this.octokit.rest.repos.listForAuthenticatedUser({
24
+ visibility: filters?.visibility === 'all' ? undefined : filters?.visibility,
25
+ sort: 'updated',
26
+ per_page: 100,
27
+ });
28
+ let repos = data.map((repo) => ({
29
+ id: repo.id,
30
+ name: repo.name,
31
+ fullName: repo.full_name,
32
+ description: repo.description,
33
+ visibility: repo.visibility,
34
+ htmlUrl: repo.html_url,
35
+ cloneUrl: repo.clone_url,
36
+ sshUrl: repo.ssh_url,
37
+ stars: repo.stargazers_count,
38
+ language: repo.language,
39
+ updatedAt: repo.updated_at,
40
+ archived: repo.archived,
41
+ }));
42
+ // Apply client-side filters
43
+ if (filters?.language) {
44
+ repos = repos.filter((r) => r.language?.toLowerCase() === filters.language.toLowerCase());
45
+ }
46
+ if (filters?.since) {
47
+ const sinceDate = new Date(filters.since);
48
+ repos = repos.filter((r) => r.updatedAt && new Date(r.updatedAt) >= sinceDate);
49
+ }
50
+ return repos;
51
+ }
52
+ async getRepo(owner, repo) {
53
+ if (!this.octokit)
54
+ throw new GitHubAuthError('No hay token configurado');
55
+ try {
56
+ const { data } = await this.octokit.rest.repos.get({ owner, repo });
57
+ return {
58
+ id: data.id,
59
+ name: data.name,
60
+ fullName: data.full_name,
61
+ description: data.description,
62
+ visibility: data.visibility,
63
+ htmlUrl: data.html_url,
64
+ cloneUrl: data.clone_url,
65
+ sshUrl: data.ssh_url,
66
+ stars: data.stargazers_count,
67
+ language: data.language,
68
+ updatedAt: data.updated_at,
69
+ archived: data.archived,
70
+ };
71
+ }
72
+ catch (err) {
73
+ this.handleError(err, 'Repositorio');
74
+ }
75
+ }
76
+ async createRepo(options) {
77
+ if (!this.octokit)
78
+ throw new GitHubAuthError('No hay token configurado');
79
+ try {
80
+ const { data } = await this.octokit.rest.repos.createForAuthenticatedUser({
81
+ name: options.name,
82
+ description: options.description,
83
+ private: options.visibility === 'private',
84
+ auto_init: options.autoInit,
85
+ gitignore_template: options.gitignoreTemplate,
86
+ license_template: options.licenseTemplate,
87
+ });
88
+ return {
89
+ id: data.id,
90
+ name: data.name,
91
+ fullName: data.full_name,
92
+ description: data.description,
93
+ visibility: data.visibility,
94
+ htmlUrl: data.html_url,
95
+ cloneUrl: data.clone_url,
96
+ sshUrl: data.ssh_url,
97
+ stars: data.stargazers_count,
98
+ language: data.language,
99
+ updatedAt: data.updated_at,
100
+ archived: data.archived,
101
+ };
102
+ }
103
+ catch (err) {
104
+ this.handleError(err);
105
+ }
106
+ }
107
+ async archiveRepo(owner, repo) {
108
+ if (!this.octokit)
109
+ throw new GitHubAuthError('No hay token configurado');
110
+ try {
111
+ await this.octokit.rest.repos.update({ owner, repo, archived: true });
112
+ }
113
+ catch (err) {
114
+ this.handleError(err, 'Repositorio');
115
+ }
116
+ }
117
+ async unarchiveRepo(owner, repo) {
118
+ if (!this.octokit)
119
+ throw new GitHubAuthError('No hay token configurado');
120
+ try {
121
+ await this.octokit.rest.repos.update({ owner, repo, archived: false });
122
+ }
123
+ catch (err) {
124
+ this.handleError(err, 'Repositorio');
125
+ }
126
+ }
127
+ async deleteRepo(owner, repo) {
128
+ if (!this.octokit)
129
+ throw new GitHubAuthError('No hay token configurado');
130
+ try {
131
+ await this.octokit.rest.repos.delete({ owner, repo });
132
+ }
133
+ catch (err) {
134
+ this.handleError(err, 'Repositorio');
135
+ }
136
+ }
137
+ async listWorkflows(owner, repo) {
138
+ if (!this.octokit)
139
+ throw new GitHubAuthError('No hay token configurado');
140
+ try {
141
+ const { data } = await this.octokit.rest.actions.listRepoWorkflows({ owner, repo });
142
+ return data.workflows.map((w) => ({
143
+ id: w.id,
144
+ name: w.name,
145
+ path: w.path,
146
+ state: w.state,
147
+ createdAt: w.created_at,
148
+ updatedAt: w.updated_at,
149
+ htmlUrl: w.html_url,
150
+ }));
151
+ }
152
+ catch (err) {
153
+ this.handleError(err, 'Workflows');
154
+ }
155
+ }
156
+ async listWorkflowRuns(owner, repo, filters) {
157
+ if (!this.octokit)
158
+ throw new GitHubAuthError('No hay token configurado');
159
+ try {
160
+ const params = { owner, repo, per_page: 50 };
161
+ if (filters?.status)
162
+ params.status = filters.status;
163
+ if (filters?.branch)
164
+ params.branch = filters.branch;
165
+ if (filters?.since)
166
+ params.created = `>=${filters.since}`;
167
+ const { data } = await this.octokit.rest.actions.listWorkflowRunsForRepo(params);
168
+ return data.workflow_runs.map((r) => ({
169
+ id: r.id,
170
+ name: r.name,
171
+ status: r.status,
172
+ conclusion: r.conclusion,
173
+ headBranch: r.head_branch,
174
+ headSha: r.head_sha,
175
+ createdAt: r.created_at,
176
+ updatedAt: r.updated_at,
177
+ htmlUrl: r.html_url,
178
+ actor: r.actor ? { login: r.actor.login } : null,
179
+ }));
180
+ }
181
+ catch (err) {
182
+ this.handleError(err, 'Runs de workflow');
183
+ }
184
+ }
185
+ async enableWorkflow(owner, repo, workflowId) {
186
+ if (!this.octokit)
187
+ throw new GitHubAuthError('No hay token configurado');
188
+ try {
189
+ await this.octokit.rest.actions.enableWorkflow({ owner, repo, workflow_id: workflowId });
190
+ }
191
+ catch (err) {
192
+ this.handleError(err);
193
+ }
194
+ }
195
+ async disableWorkflow(owner, repo, workflowId) {
196
+ if (!this.octokit)
197
+ throw new GitHubAuthError('No hay token configurado');
198
+ try {
199
+ await this.octokit.rest.actions.disableWorkflow({ owner, repo, workflow_id: workflowId });
200
+ }
201
+ catch (err) {
202
+ this.handleError(err);
203
+ }
204
+ }
205
+ async triggerWorkflowDispatch(owner, repo, workflowId, ref, inputs) {
206
+ if (!this.octokit)
207
+ throw new GitHubAuthError('No hay token configurado');
208
+ try {
209
+ await this.octokit.rest.actions.createWorkflowDispatch({
210
+ owner,
211
+ repo,
212
+ workflow_id: workflowId,
213
+ ref,
214
+ inputs,
215
+ });
216
+ }
217
+ catch (err) {
218
+ this.handleError(err);
219
+ }
220
+ }
221
+ handleError(err, resourceName) {
222
+ if (err instanceof Error) {
223
+ // Check if it's an Octokit error
224
+ const octokitErr = err;
225
+ if (octokitErr.status) {
226
+ switch (octokitErr.status) {
227
+ case 401:
228
+ case 403:
229
+ if (octokitErr.message?.includes('rate limit')) {
230
+ const resetTime = octokitErr.response?.headers?.['x-ratelimit-reset'];
231
+ throw new GitHubRateLimitError(resetTime ? parseInt(resetTime) : undefined);
232
+ }
233
+ throw new GitHubPermissionError();
234
+ case 404:
235
+ throw new GitHubNotFoundError(resourceName);
236
+ default:
237
+ throw new GitHubError(`Error de GitHub (${octokitErr.status}): ${octokitErr.message}`);
238
+ }
239
+ }
240
+ }
241
+ throw new GitHubError(err.message || 'Error desconocido de GitHub');
242
+ }
243
+ }
@@ -0,0 +1,67 @@
1
+ import CryptoJS from 'crypto-js';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ export class FileSecureStorageAdapter {
6
+ storageDir;
7
+ secretKey;
8
+ constructor() {
9
+ // Use a directory in user's home for storage
10
+ 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
+ }
19
+ async ensureStorageDir() {
20
+ try {
21
+ await fs.access(this.storageDir);
22
+ }
23
+ catch {
24
+ await fs.mkdir(this.storageDir, { recursive: true });
25
+ }
26
+ }
27
+ getFilePath(key) {
28
+ // Sanitize key for filename
29
+ const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');
30
+ return path.join(this.storageDir, `${safeKey}.enc`);
31
+ }
32
+ async save(key, value) {
33
+ const encrypted = CryptoJS.AES.encrypt(value, this.secretKey).toString();
34
+ const filePath = this.getFilePath(key);
35
+ await fs.writeFile(filePath, encrypted, 'utf8');
36
+ }
37
+ async load(key) {
38
+ const filePath = this.getFilePath(key);
39
+ try {
40
+ const encrypted = await fs.readFile(filePath, 'utf8');
41
+ const bytes = CryptoJS.AES.decrypt(encrypted, this.secretKey);
42
+ return bytes.toString(CryptoJS.enc.Utf8);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async delete(key) {
49
+ const filePath = this.getFilePath(key);
50
+ try {
51
+ await fs.unlink(filePath);
52
+ }
53
+ catch {
54
+ // Ignore if file doesn't exist
55
+ }
56
+ }
57
+ async exists(key) {
58
+ const filePath = this.getFilePath(key);
59
+ try {
60
+ await fs.access(filePath);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ }