leganux-commit-describer 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Angel Erick Cruz Olivera
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # lnxcommit — Leganux Commit Describer
2
+
3
+ > CLI que analiza los cambios de tus commits de Git y genera una descripción legible en español usando IA (DeepSeek).
4
+
5
+ ---
6
+
7
+ ## Requisitos
8
+
9
+ - Node.js >= 18
10
+ - Una API key de [DeepSeek](https://platform.deepseek.com/)
11
+ - El directorio donde lo uses debe ser un repositorio Git
12
+
13
+ ---
14
+
15
+ ## Instalación
16
+
17
+ ### Opción A — Global desde npm
18
+
19
+ ```bash
20
+ npm install -g leganux-commit-describer
21
+ ```
22
+
23
+ ### Opción B — Clonar y enlazar localmente
24
+
25
+ ```bash
26
+ git clone https://github.com/leganux/leganux-commit-describer.git
27
+ cd leganux-commit-describer
28
+ npm install
29
+ npm link
30
+ ```
31
+
32
+ Tras la instalación el binario `lnxcommit` quedará disponible en tu `PATH`.
33
+
34
+ ---
35
+
36
+ ## Comandos
37
+
38
+ ### `lnxcommit configure`
39
+
40
+ Guarda tu API key de DeepSeek en `~/.lnx_commit_describer/config.json`.
41
+ Solo necesitas ejecutarlo **una vez** (o cuando quieras cambiar la key).
42
+
43
+ ```bash
44
+ lnxcommit configure
45
+ ```
46
+
47
+ **Flujo interactivo:**
48
+
49
+ ```
50
+ 🔧 Configuración de lnxcommit
51
+
52
+ API key actual: (no configurada)
53
+
54
+ ? Ingresa tu API key de DeepSeek: ****************************
55
+
56
+ ✅ API key guardada en: /Users/<tu-usuario>/.lnx_commit_describer/config.json
57
+ ```
58
+
59
+ ---
60
+
61
+ ### `lnxcommit describe`
62
+
63
+ Analiza el repositorio Git del directorio actual (o del indicado con `--path`) y
64
+ muestra la descripción de cambios directamente en la consola.
65
+
66
+ ```bash
67
+ # En el directorio del repo
68
+ lnxcommit describe
69
+
70
+ # Apuntando a otro repositorio
71
+ lnxcommit describe --path /ruta/al/repo
72
+ ```
73
+
74
+ **Salida de ejemplo:**
75
+
76
+ ```
77
+ 🔍 Analizando cambios en: /Users/leganux/mi-proyecto
78
+
79
+ :sparkles: Título: Migración de script a librería npm con CLI interactivo
80
+
81
+ - :sparkles: Se agrega el archivo `bin/lnxcommit.js` que expone el CLI con commander
82
+ - :recycle: Se refactoriza `describe-changes.js` separando la lógica en `lib/core.js`
83
+ - :memo: Se actualiza `README.md` con instrucciones completas de uso
84
+ - :package: Se crea `package.json` definiendo el binario `lnxcommit`
85
+
86
+ Resumen: Se convirtió el script original en una herramienta de línea de comandos instalable...
87
+ ```
88
+
89
+ #### Opciones
90
+
91
+ | Opción | Descripción |
92
+ |--------|-------------|
93
+ | `--path <ruta>` | Ruta al repositorio git (por defecto: directorio actual) |
94
+
95
+ ---
96
+
97
+ ### `lnxcommit describeandexport`
98
+
99
+ Igual que `describe`, pero además exporta el resultado a un archivo `.txt`.
100
+
101
+ ```bash
102
+ # Exporta a commit-description.txt en el directorio actual
103
+ lnxcommit describeandexport
104
+
105
+ # Con ruta y nombre de archivo personalizado
106
+ lnxcommit describeandexport --path /ruta/al/repo --output ./changelog/cambios.txt
107
+ ```
108
+
109
+ **Salida de ejemplo:**
110
+
111
+ ```
112
+ 🔍 Analizando cambios en: /Users/leganux/mi-proyecto
113
+
114
+ - :sparkles: ...
115
+ - :bug: ...
116
+
117
+ 📄 Descripción exportada a: /Users/leganux/mi-proyecto/commit-description.txt
118
+ ```
119
+
120
+ El archivo generado tiene el siguiente formato:
121
+
122
+ ```
123
+ # Descripción de cambios
124
+ Generado: 2026-05-06 14:00:00
125
+ Directorio: /Users/leganux/mi-proyecto
126
+
127
+ - :sparkles: Título: ...
128
+ - :recycle: ...
129
+ ...
130
+ ```
131
+
132
+ #### Opciones
133
+
134
+ | Opción | Descripción |
135
+ |--------|-------------|
136
+ | `--path <ruta>` | Ruta al repositorio git (por defecto: directorio actual) |
137
+ | `--output <archivo>` | Archivo de salida (por defecto: `commit-description.txt` en el repo) |
138
+
139
+ ---
140
+
141
+ ## Configuración almacenada
142
+
143
+ La API key se guarda de forma local en tu máquina en:
144
+
145
+ ```
146
+ ~/.lnx_commit_describer/config.json
147
+ ```
148
+
149
+ Ejemplo del contenido:
150
+
151
+ ```json
152
+ {
153
+ "apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
154
+ }
155
+ ```
156
+
157
+ Este archivo es **solo tuyo** y nunca se sube al repositorio (está en `.gitignore`).
158
+
159
+ ---
160
+
161
+ ## Cómo funciona internamente
162
+
163
+ 1. Detecta la rama base de comparación (`origin/main`, `origin/master` o `HEAD~1`).
164
+ 2. Obtiene `git diff --name-status` y el patch unificado contra esa base.
165
+ 3. Construye un prompt en español y lo envía a la API de DeepSeek (`deepseek-chat`).
166
+ 4. Formatea la respuesta como viñetas con gitmoji y la muestra / guarda.
167
+
168
+ ---
169
+
170
+ ## Estructura del proyecto
171
+
172
+ ```
173
+ leganux-commit-describer/
174
+ ├── bin/
175
+ │ └── lnxcommit.js # Punto de entrada del CLI (commander)
176
+ ├── lib/
177
+ │ └── core.js # Lógica reutilizable (git, AI, config)
178
+ ├── describe-changes.js # Script original (standalone)
179
+ ├── package.json
180
+ └── README.md
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Licencia
186
+
187
+ MIT © [leganux](https://github.com/leganux)
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { program } = require('commander');
6
+ const readline = require('readline');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const {
11
+ readConfig,
12
+ writeConfig,
13
+ CONFIG_FILE,
14
+ CONFIG_DIR,
15
+ isGitRepo,
16
+ describeChanges,
17
+ } = require('../lib/core');
18
+
19
+ const { version } = require('../package.json');
20
+
21
+ // ─── Helper: leer input oculto desde terminal ─────────────────────────────────
22
+ function askSecret(prompt) {
23
+ return new Promise((resolve) => {
24
+ const rl = readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout,
27
+ });
28
+
29
+ // Silencia la salida para ocultar lo que se escribe
30
+ process.stdout.write(prompt);
31
+ let input = '';
32
+
33
+ // Desactivar echo en terminales que lo soporten
34
+ if (process.stdin.isTTY) {
35
+ process.stdin.setRawMode(true);
36
+ process.stdin.resume();
37
+ process.stdin.setEncoding('utf8');
38
+
39
+ process.stdin.on('data', function handler(char) {
40
+ if (char === '\n' || char === '\r' || char === '\u0004') {
41
+ process.stdin.setRawMode(false);
42
+ process.stdin.removeListener('data', handler);
43
+ process.stdout.write('\n');
44
+ rl.close();
45
+ resolve(input);
46
+ } else if (char === '\u0003') {
47
+ // Ctrl+C
48
+ process.stdout.write('\n');
49
+ process.exit(0);
50
+ } else if (char === '\u007f') {
51
+ // Backspace
52
+ input = input.slice(0, -1);
53
+ } else {
54
+ input += char;
55
+ }
56
+ });
57
+ } else {
58
+ // Sin TTY (p.ej. pipe): leer directamente
59
+ rl.question('', (answer) => {
60
+ rl.close();
61
+ resolve(answer);
62
+ });
63
+ }
64
+ });
65
+ }
66
+
67
+ program
68
+ .name('lnxcommit')
69
+ .description('CLI para describir cambios de commits con IA (DeepSeek)')
70
+ .version(version);
71
+
72
+ // ─── configure ────────────────────────────────────────────────────────────────
73
+ program
74
+ .command('configure')
75
+ .description('Configura la API key de DeepSeek y la guarda en ~/.lnx_commit_describer/config.json')
76
+ .action(async () => {
77
+ console.log('🔧 Configuración de lnxcommit\n');
78
+
79
+ const existing = readConfig();
80
+ const maskedKey = existing.apiKey
81
+ ? `${existing.apiKey.slice(0, 6)}${'*'.repeat(Math.max(0, existing.apiKey.length - 10))}${existing.apiKey.slice(-4)}`
82
+ : '(no configurada)';
83
+
84
+ console.log(`API key actual: ${maskedKey}\n`);
85
+
86
+ const apiKey = await askSecret('Ingresa tu API key de DeepSeek: ');
87
+ if (!apiKey || !apiKey.trim()) {
88
+ console.error('❌ La API key no puede estar vacía.');
89
+ process.exit(1);
90
+ }
91
+
92
+ const newConfig = { ...existing, apiKey: apiKey.trim() };
93
+ writeConfig(newConfig);
94
+
95
+ console.log(`\n✅ API key guardada en: ${CONFIG_FILE}`);
96
+ });
97
+
98
+ // ─── describe ─────────────────────────────────────────────────────────────────
99
+ program
100
+ .command('describe')
101
+ .description('Analiza los cambios del repositorio git en el directorio actual y muestra la descripción en consola')
102
+ .option('--path <ruta>', 'Ruta del repositorio git (por defecto: directorio actual)')
103
+ .action(async (opts) => {
104
+ const cwd = opts.path ? path.resolve(opts.path) : process.cwd();
105
+
106
+ console.log(`🔍 Analizando cambios en: ${cwd}\n`);
107
+
108
+ try {
109
+ const output = await describeChanges(cwd);
110
+ console.log(output);
111
+ } catch (err) {
112
+ const detail = err?.response?.data || err.message;
113
+ console.error('❌ Error:', detail);
114
+ process.exit(1);
115
+ }
116
+ });
117
+
118
+ // ─── describeandexport ────────────────────────────────────────────────────────
119
+ program
120
+ .command('describeandexport')
121
+ .description('Analiza los cambios del repositorio git y exporta la descripción a un archivo .txt')
122
+ .option('--path <ruta>', 'Ruta del repositorio git (por defecto: directorio actual)')
123
+ .option('--output <archivo>', 'Nombre del archivo de salida (por defecto: commit-description.txt)')
124
+ .action(async (opts) => {
125
+ const cwd = opts.path ? path.resolve(opts.path) : process.cwd();
126
+ const outputFile = opts.output
127
+ ? path.resolve(opts.output)
128
+ : path.join(cwd, 'commit-description.txt');
129
+
130
+ console.log(`🔍 Analizando cambios en: ${cwd}\n`);
131
+
132
+ try {
133
+ const output = await describeChanges(cwd);
134
+
135
+ const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
136
+ const fileContent = `# Descripción de cambios\nGenerado: ${timestamp}\nDirectorio: ${cwd}\n\n${output}\n`;
137
+
138
+ fs.writeFileSync(outputFile, fileContent, 'utf8');
139
+
140
+ console.log(output);
141
+ console.log(`\n📄 Descripción exportada a: ${outputFile}`);
142
+ } catch (err) {
143
+ const detail = err?.response?.data || err.message;
144
+ console.error('❌ Error:', detail);
145
+ process.exit(1);
146
+ }
147
+ });
148
+
149
+ program.parse(process.argv);
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('dotenv').config();
4
+
5
+ const axios = require('axios');
6
+ const { execSync } = require('child_process');
7
+
8
+ function run(command) {
9
+ return execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
10
+ }
11
+
12
+ function runSafe(command) {
13
+ try {
14
+ return run(command);
15
+ } catch (error) {
16
+ return '';
17
+ }
18
+ }
19
+
20
+ function getBaseRef() {
21
+ const checkMain = runSafe('git show-ref --verify --quiet refs/remotes/origin/main; echo $?');
22
+ if (checkMain.endsWith('0')) return 'origin/main';
23
+
24
+ const checkMaster = runSafe('git show-ref --verify --quiet refs/remotes/origin/master; echo $?');
25
+ if (checkMaster.endsWith('0')) return 'origin/master';
26
+
27
+ const hasPreviousCommit = runSafe('git rev-parse --verify HEAD~1');
28
+ if (hasPreviousCommit) return 'HEAD~1';
29
+
30
+ return 'HEAD';
31
+ }
32
+
33
+ function getGitChanges(baseRef) {
34
+ const branch = runSafe('git rev-parse --abbrev-ref HEAD') || 'unknown-branch';
35
+ const changedFiles = runSafe(`git diff --name-status ${baseRef}...HEAD`);
36
+ const patch = runSafe(`git diff --unified=1 ${baseRef}...HEAD`);
37
+ const status = runSafe('git status --short');
38
+
39
+ return {
40
+ branch,
41
+ baseRef,
42
+ changedFiles,
43
+ patch: patch.slice(0, 20000),
44
+ status,
45
+ };
46
+ }
47
+
48
+ function buildPrompt({ branch, baseRef, changedFiles, patch, status }) {
49
+ return `Analiza estos cambios de Git y crea una descripción humana en español.
50
+
51
+ Reglas de salida:
52
+ 0) aniade un titulo para todos los cambios
53
+ 1) Responde SOLO con viñetas, un renglón por cada cambio relevante.
54
+ 2) Cada viñeta debe iniciar con gitmoji (ejemplo: :sparkles:, :bug:, :recycle:, :art:, :zap:).
55
+ 3) Formato exacto: "- :gitmoji: descripción corta".
56
+ 4) Máximo 1 línea por punto, sin párrafos, sin encabezados, sin texto extra.
57
+ 5) Si hay varios fixes, sepáralos en puntos distintos.
58
+ 6) Si no hay cambios relevantes, responde: "- :white_check_mark: No se detectaron cambios relevantes".
59
+ 7) Haz referencia detallada a los botones, Ids, funciones, clases, rutas de archivos, ramas; describiendo con base a sus nombres de esas clases o funciones que hacen internamente o que funcionalidad resuelven.
60
+ 8) Y al final pon un resumen corto de 4 o 5 renglones en lenguaje no tecnico de lo que implico esos cambios.
61
+ 9) Dame el titulo del commit en una sola frase con gitmoji al inicio.
62
+
63
+ Contexto:
64
+ - Rama: ${branch}
65
+ - Base de comparación: ${baseRef}
66
+
67
+ Archivos cambiados:
68
+ ${changedFiles || '(sin archivos en diff commit)'}
69
+
70
+ Estado de working tree:
71
+ ${status || '(limpio)'}
72
+
73
+ Patch (resumido):
74
+ ${patch || '(sin patch disponible)'}
75
+ `;
76
+ }
77
+
78
+ async function requestDeepseek(prompt, apiKey) {
79
+ const response = await axios.post(
80
+ 'https://api.deepseek.com/v1/chat/completions',
81
+ {
82
+ model: 'deepseek-chat',
83
+ temperature: 0.2,
84
+ max_tokens: 700,
85
+ messages: [
86
+ {
87
+ role: 'system',
88
+ content:
89
+ 'Eres un generador de changelogs técnicos cortos. Cumple formato solicitado exactamente.',
90
+ },
91
+ {
92
+ role: 'user',
93
+ content: prompt,
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ headers: {
99
+ Authorization: `Bearer ${apiKey}`,
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ timeout: 60000,
103
+ }
104
+ );
105
+
106
+ return response?.data?.choices?.[0]?.message?.content?.trim() || '';
107
+ }
108
+
109
+ function normalizeOutput(text) {
110
+ if (!text) return '- :white_check_mark: No se detectaron cambios relevantes';
111
+
112
+ const lines = text
113
+ .split('\n')
114
+ .map((line) => line.trim())
115
+ .filter(Boolean)
116
+ .map((line) => {
117
+ const clean = line.replace(/^[-*]\s*/, '').trim();
118
+ return clean.startsWith(':') ? `- ${clean}` : `- :wrench: ${clean}`;
119
+ });
120
+
121
+ return lines.length
122
+ ? lines.join('\n')
123
+ : '- :white_check_mark: No se detectaron cambios relevantes';
124
+ }
125
+
126
+ async function main() {
127
+ const isDryRun = process.argv.includes('--dry-run');
128
+ const apiKey = process.env.AI_API_KEY;
129
+ if (!apiKey && !isDryRun) {
130
+ console.error('Error: falta AI_API_KEY en el .env');
131
+ process.exit(1);
132
+ }
133
+
134
+ const baseRef = getBaseRef();
135
+ const changes = getGitChanges(baseRef);
136
+ const prompt = buildPrompt(changes);
137
+
138
+ if (isDryRun) {
139
+ console.log('DRY RUN - Resumen local de cambios detectados');
140
+ console.log(`Base: ${baseRef}`);
141
+ console.log(changes.changedFiles || 'Sin cambios detectados en diff contra base');
142
+ return;
143
+ }
144
+
145
+ try {
146
+ const aiText = await requestDeepseek(prompt, apiKey);
147
+ const output = normalizeOutput(aiText);
148
+ console.log(output);
149
+ } catch (error) {
150
+ const detail = error?.response?.data || error.message;
151
+ console.error('Error al generar descripción con IA:', detail);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ main();
package/lib/core.js ADDED
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const { execSync } = require('child_process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+
9
+ // ─── Config helpers ───────────────────────────────────────────────────────────
10
+
11
+ const CONFIG_DIR = path.join(os.homedir(), '.lnx_commit_describer');
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
+
14
+ function readConfig() {
15
+ if (!fs.existsSync(CONFIG_FILE)) return {};
16
+ try {
17
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+
23
+ function writeConfig(data) {
24
+ if (!fs.existsSync(CONFIG_DIR)) {
25
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
26
+ }
27
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), 'utf8');
28
+ }
29
+
30
+ function getApiKey() {
31
+ const config = readConfig();
32
+ return config.apiKey || null;
33
+ }
34
+
35
+ // ─── Git helpers ──────────────────────────────────────────────────────────────
36
+
37
+ function run(command, cwd) {
38
+ return execSync(command, {
39
+ encoding: 'utf8',
40
+ stdio: ['pipe', 'pipe', 'pipe'],
41
+ cwd: cwd || process.cwd(),
42
+ }).trim();
43
+ }
44
+
45
+ function runSafe(command, cwd) {
46
+ try {
47
+ return run(command, cwd);
48
+ } catch {
49
+ return '';
50
+ }
51
+ }
52
+
53
+ function isGitRepo(cwd) {
54
+ const result = runSafe('git rev-parse --is-inside-work-tree', cwd);
55
+ return result === 'true';
56
+ }
57
+
58
+ function getBaseRef(cwd) {
59
+ const checkMain = runSafe(
60
+ 'git show-ref --verify --quiet refs/remotes/origin/main; echo $?',
61
+ cwd
62
+ );
63
+ if (checkMain.endsWith('0')) return 'origin/main';
64
+
65
+ const checkMaster = runSafe(
66
+ 'git show-ref --verify --quiet refs/remotes/origin/master; echo $?',
67
+ cwd
68
+ );
69
+ if (checkMaster.endsWith('0')) return 'origin/master';
70
+
71
+ const hasPreviousCommit = runSafe('git rev-parse --verify HEAD~1', cwd);
72
+ if (hasPreviousCommit) return 'HEAD~1';
73
+
74
+ return 'HEAD';
75
+ }
76
+
77
+ function getGitChanges(baseRef, cwd) {
78
+ const branch =
79
+ runSafe('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown-branch';
80
+ const changedFiles = runSafe(`git diff --name-status ${baseRef}...HEAD`, cwd);
81
+ const patch = runSafe(`git diff --unified=1 ${baseRef}...HEAD`, cwd);
82
+ const status = runSafe('git status --short', cwd);
83
+
84
+ return {
85
+ branch,
86
+ baseRef,
87
+ changedFiles,
88
+ patch: patch.slice(0, 20000),
89
+ status,
90
+ };
91
+ }
92
+
93
+ // ─── AI helpers ───────────────────────────────────────────────────────────────
94
+
95
+ function buildPrompt({ branch, baseRef, changedFiles, patch, status }) {
96
+ return `Analiza estos cambios de Git y crea una descripción humana en español.
97
+
98
+ Reglas de salida:
99
+ 0) aniade un titulo para todos los cambios
100
+ 1) Responde SOLO con viñetas, un renglón por cada cambio relevante.
101
+ 2) Cada viñeta debe iniciar con gitmoji (ejemplo: :sparkles:, :bug:, :recycle:, :art:, :zap:).
102
+ 3) Formato exacto: "- :gitmoji: descripción corta".
103
+ 4) Máximo 1 línea por punto, sin párrafos, sin encabezados, sin texto extra.
104
+ 5) Si hay varios fixes, sepáralos en puntos distintos.
105
+ 6) Si no hay cambios relevantes, responde: "- :white_check_mark: No se detectaron cambios relevantes".
106
+ 7) Haz referencia detallada a los botones, Ids, funciones, clases, rutas de archivos, ramas; describiendo con base a sus nombres de esas clases o funciones que hacen internamente o que funcionalidad resuelven.
107
+ 8) Y al final pon un resumen corto de 4 o 5 renglones en lenguaje no tecnico de lo que implico esos cambios.
108
+ 9) Dame el titulo del commit en una sola frase con gitmoji al inicio.
109
+
110
+ Contexto:
111
+ - Rama: ${branch}
112
+ - Base de comparación: ${baseRef}
113
+
114
+ Archivos cambiados:
115
+ ${changedFiles || '(sin archivos en diff commit)'}
116
+
117
+ Estado de working tree:
118
+ ${status || '(limpio)'}
119
+
120
+ Patch (resumido):
121
+ ${patch || '(sin patch disponible)'}
122
+ `;
123
+ }
124
+
125
+ async function requestDeepseek(prompt, apiKey) {
126
+ const response = await axios.post(
127
+ 'https://api.deepseek.com/v1/chat/completions',
128
+ {
129
+ model: 'deepseek-chat',
130
+ temperature: 0.2,
131
+ max_tokens: 700,
132
+ messages: [
133
+ {
134
+ role: 'system',
135
+ content:
136
+ 'Eres un generador de changelogs técnicos cortos. Cumple formato solicitado exactamente.',
137
+ },
138
+ {
139
+ role: 'user',
140
+ content: prompt,
141
+ },
142
+ ],
143
+ },
144
+ {
145
+ headers: {
146
+ Authorization: `Bearer ${apiKey}`,
147
+ 'Content-Type': 'application/json',
148
+ },
149
+ timeout: 60000,
150
+ }
151
+ );
152
+
153
+ return response?.data?.choices?.[0]?.message?.content?.trim() || '';
154
+ }
155
+
156
+ function normalizeOutput(text) {
157
+ if (!text) return '- :white_check_mark: No se detectaron cambios relevantes';
158
+
159
+ const lines = text
160
+ .split('\n')
161
+ .map((line) => line.trim())
162
+ .filter(Boolean)
163
+ .map((line) => {
164
+ const clean = line.replace(/^[-*]\s*/, '').trim();
165
+ return clean.startsWith(':') ? `- ${clean}` : `- :wrench: ${clean}`;
166
+ });
167
+
168
+ return lines.length
169
+ ? lines.join('\n')
170
+ : '- :white_check_mark: No se detectaron cambios relevantes';
171
+ }
172
+
173
+ // ─── Main describe logic ──────────────────────────────────────────────────────
174
+
175
+ async function describeChanges(cwd) {
176
+ const apiKey = getApiKey();
177
+ if (!apiKey) {
178
+ throw new Error(
179
+ 'No se encontró la API key. Ejecuta primero: lnxcommit configure'
180
+ );
181
+ }
182
+
183
+ if (!isGitRepo(cwd)) {
184
+ throw new Error(
185
+ `El directorio "${cwd}" no es un repositorio git válido.`
186
+ );
187
+ }
188
+
189
+ const baseRef = getBaseRef(cwd);
190
+ const changes = getGitChanges(baseRef, cwd);
191
+ const prompt = buildPrompt(changes);
192
+ const aiText = await requestDeepseek(prompt, apiKey);
193
+ return normalizeOutput(aiText);
194
+ }
195
+
196
+ module.exports = {
197
+ readConfig,
198
+ writeConfig,
199
+ getApiKey,
200
+ CONFIG_FILE,
201
+ CONFIG_DIR,
202
+ isGitRepo,
203
+ describeChanges,
204
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "leganux-commit-describer",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to describe git commit changes using DeepSeek AI",
5
+ "main": "lib/core.js",
6
+ "bin": {
7
+ "lnxcommit": "./bin/lnxcommit.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/lnxcommit.js"
11
+ },
12
+ "keywords": [
13
+ "git",
14
+ "commit",
15
+ "changelog",
16
+ "ai",
17
+ "deepseek",
18
+ "cli"
19
+ ],
20
+ "author": "leganux",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "axios": "^1.6.0",
24
+ "commander": "^12.0.0"
25
+ }
26
+ }