@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.
- package/README.md +189 -0
- package/dist/adapters/git/SimpleGitAdapter.js +126 -0
- package/dist/adapters/github/GitHubAPIAdapter.js +243 -0
- package/dist/adapters/storage/FileSecureStorageAdapter.js +67 -0
- package/dist/adapters/ui/ConsoleUIAdapter.js +91 -0
- package/dist/commands/BaseCommand.js +11 -0
- package/dist/commands/BranchesCommand.js +152 -0
- package/dist/commands/Command.js +1 -0
- package/dist/commands/CommitCommand.js +85 -0
- package/dist/commands/IntegrationCommand.js +151 -0
- package/dist/commands/LogCommand.js +42 -0
- package/dist/commands/PullCommand.js +55 -0
- package/dist/commands/PushCommand.js +227 -0
- package/dist/commands/RepositoryCommand.js +268 -0
- package/dist/commands/StashCommand.js +140 -0
- package/dist/commands/StatusCommand.js +66 -0
- package/dist/commands/github/BaseGitHubCommand.js +17 -0
- package/dist/commands/github/GitHubAuthCommand.js +48 -0
- package/dist/commands/github/GitHubMenuCommand.js +57 -0
- package/dist/commands/github/GitHubReposCommand.js +210 -0
- package/dist/commands/github/GitHubWorkflowsCommand.js +243 -0
- package/dist/commit.js +98 -0
- package/dist/core/errors/GitError.js +19 -0
- package/dist/core/errors/GitHubError.js +36 -0
- package/dist/core/ports/GitHubPort.js +1 -0
- package/dist/core/ports/GitPort.js +1 -0
- package/dist/core/ports/SecureStoragePort.js +1 -0
- package/dist/core/ports/UIPort.js +1 -0
- package/dist/core/services/GitHubService.js +95 -0
- package/dist/core/services/GitService.js +44 -0
- package/dist/estado.js +97 -0
- package/dist/factories/CommandFactory.js +51 -0
- package/dist/index.js +71 -0
- package/dist/integracion.js +152 -0
- package/dist/ramas.js +178 -0
- package/dist/remoto.js +129 -0
- package/dist/repositorio.js +159 -0
- package/dist/stash.js +169 -0
- package/dist/utils.js +49 -0
- 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
|
+
}
|