@dami_deleon/rikudo 2.0.2 → 2.1.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 +11 -5
- package/dist/index.js +18 -1
- package/dist/mcp/server.js +24 -1
- package/dist/services/customFieldsCache.js +59 -0
- package/dist/services/redmine.js +36 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,6 +90,11 @@ rikudo pr -t 1234
|
|
|
90
90
|
rikudo test
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
* **Ver Ticket:**
|
|
94
|
+
```bash
|
|
95
|
+
rikudo ticket 1234
|
|
96
|
+
```
|
|
97
|
+
|
|
93
98
|
* **Configuración:**
|
|
94
99
|
```bash
|
|
95
100
|
rikudo config
|
|
@@ -112,10 +117,11 @@ El servidor se comunica mediante **stdio**, permitiendo integrarse con cualquier
|
|
|
112
117
|
| Herramienta | Descripción |
|
|
113
118
|
|------------|-------------|
|
|
114
119
|
| `validate_configuration` | Verifica que las variables de entorno necesarias (Redmine, Gitea) estén configuradas correctamente. |
|
|
115
|
-
| `get_ticket_context` | Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad). |
|
|
120
|
+
| `get_ticket_context` | Obtiene el contexto completo de un ticket de Redmine (título, descripción, estado, prioridad, custom fields). |
|
|
116
121
|
| `get_git_diff` | Retorna el diff de los archivos que están en staging (`git add`). |
|
|
117
122
|
| `shift_left_start` | Flujo shift-left completo: checkout a rama objetivo, crea nueva rama, commit vacío y abre Draft PR en Gitea. Soporta parámetro `isWip` para usar prefijo "WIP:" en el título. |
|
|
118
123
|
| `update_pm_redmine` | Agrega un comentario a un ticket de Redmine para notificar al Project Manager. |
|
|
124
|
+
| `update_custom_fields` | Actualiza los campos personalizados (Custom Fields) de un ticket de Redmine. |
|
|
119
125
|
|
|
120
126
|
#### Prompts Disponibles
|
|
121
127
|
|
|
@@ -157,8 +163,8 @@ RIKUDO_REDMINE_API_KEY=tu_redmine_key
|
|
|
157
163
|
# Gitea
|
|
158
164
|
RIKUDO_GITEA_URL=https://gitea.tuempresa.com
|
|
159
165
|
RIKUDO_GITEA_TOKEN=tu_gitea_token
|
|
160
|
-
RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
|
|
161
|
-
RIKUDO_GITEA_REPO=nombre_del_repositorio
|
|
166
|
+
RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion # Opcional
|
|
167
|
+
RIKUDO_GITEA_REPO=nombre_del_repositorio # Opcional
|
|
162
168
|
```
|
|
163
169
|
|
|
164
170
|
#### Formato de Ramas
|
|
@@ -197,8 +203,8 @@ RIKUDO_REDMINE_API_KEY=tu_redmine_key
|
|
|
197
203
|
# --- Gitea (para servidor MCP) ---
|
|
198
204
|
RIKUDO_GITEA_URL=https://gitea.tuempresa.com
|
|
199
205
|
RIKUDO_GITEA_TOKEN=tu_gitea_token
|
|
200
|
-
RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion
|
|
201
|
-
RIKUDO_GITEA_REPO=nombre_del_repositorio
|
|
206
|
+
RIKUDO_GITEA_OWNER=tu_usuario_o_organizacion # Opcional: se obtiene del remote si no se especifica
|
|
207
|
+
RIKUDO_GITEA_REPO=nombre_del_repositorio # Opcional: se obtiene del remote si no se especifica
|
|
202
208
|
```
|
|
203
209
|
|
|
204
210
|
### Plantillas Personalizadas
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import figlet from "figlet";
|
|
6
6
|
import clipboardy from "clipboardy"; // <--- Nueva dependencia
|
|
7
7
|
import { getStagedDiff, getStagedFiles, commitChanges, getRecentCommits, getCommitList, } from "./services/git.js";
|
|
8
|
-
import { getIssueDetails, updateIssueComment, } from "./services/redmine.js";
|
|
8
|
+
import { getIssueDetails, updateIssueComment, printTicketDetails, } from "./services/redmine.js";
|
|
9
9
|
import { showConfigMenu } from "./menus/configMenu.js";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import path from "node:path";
|
|
@@ -63,6 +63,23 @@ program
|
|
|
63
63
|
.command("config")
|
|
64
64
|
.description("Abrir menú de configuración")
|
|
65
65
|
.action(() => showConfigMenu());
|
|
66
|
+
// Ticket
|
|
67
|
+
const ticketCmd = program
|
|
68
|
+
.command("ticket <ticketId>")
|
|
69
|
+
.description("Ver detalles de un ticket de Redmine");
|
|
70
|
+
ticketCmd.action(async (ticketId) => {
|
|
71
|
+
loadEnv();
|
|
72
|
+
if (!ticketId) {
|
|
73
|
+
console.log(chalk.red("❌ Debes especificar un ticket: rikudo ticket <id>"));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const ticket = await getIssueDetails(ticketId);
|
|
77
|
+
if (!ticket) {
|
|
78
|
+
console.log(chalk.red(`❌ No se encontró el ticket #${ticketId}`));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
printTicketDetails(ticket);
|
|
82
|
+
});
|
|
66
83
|
// MCP Server
|
|
67
84
|
program
|
|
68
85
|
.command("mcp")
|
package/dist/mcp/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { simpleGit } from "simple-git";
|
|
5
|
-
import { getIssueDetails, updateIssueComment } from "../services/redmine.js";
|
|
5
|
+
import { getIssueDetails, updateIssueComment, updateIssueCustomFields } from "../services/redmine.js";
|
|
6
6
|
import { getStagedDiff } from "../services/git.js";
|
|
7
7
|
import { createPullRequest } from "../services/gitea.js";
|
|
8
8
|
import { loadEnv } from "../utils/env.js";
|
|
@@ -46,6 +46,7 @@ server.registerTool("get_ticket_context", {
|
|
|
46
46
|
priority: ticket.priority,
|
|
47
47
|
author: ticket.author,
|
|
48
48
|
url: ticket.url,
|
|
49
|
+
customFields: ticket.customFields,
|
|
49
50
|
}, null, 2),
|
|
50
51
|
},
|
|
51
52
|
],
|
|
@@ -190,6 +191,28 @@ server.registerTool("update_pm_redmine", {
|
|
|
190
191
|
return { content: [{ type: "text", text: `Error actualizando Redmine: ${errorMessage}` }], isError: true };
|
|
191
192
|
}
|
|
192
193
|
});
|
|
194
|
+
server.registerTool("update_custom_fields", {
|
|
195
|
+
title: "Update Custom Fields",
|
|
196
|
+
description: "Actualiza los campos personalizados (Custom Fields) de un ticket de Redmine. Usa get_ticket_context primero para ver los campos disponibles y sus IDs.",
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
ticketId: z.string().describe("ID del ticket de Redmine (ej: '1234')"),
|
|
199
|
+
customFields: z.array(z.object({
|
|
200
|
+
id: z.number().describe("ID del custom field"),
|
|
201
|
+
value: z.string().describe("Nuevo valor para el campo"),
|
|
202
|
+
})).describe("Array de custom fields a actualizar"),
|
|
203
|
+
}),
|
|
204
|
+
}, async ({ ticketId, customFields }) => {
|
|
205
|
+
try {
|
|
206
|
+
await updateIssueCustomFields(ticketId, customFields);
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text: `Custom fields actualizados exitosamente en el ticket #${ticketId}` }],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
const errorMessage = error instanceof Error ? error.message : "Error desconocido";
|
|
213
|
+
return { content: [{ type: "text", text: `Error actualizando custom fields: ${errorMessage}` }], isError: true };
|
|
214
|
+
}
|
|
215
|
+
});
|
|
193
216
|
server.registerPrompt("start_feature", {
|
|
194
217
|
title: "Start Feature",
|
|
195
218
|
description: "Inicia el flujo de trabajo para una nueva feature. Obtiene contexto del ticket y prepara el entorno en Gitea.",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { getConfig } from "../utils/config.js";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const CUSTOM_FIELDS_CACHE_FILE = path.resolve(__dirname, "../../.rikudo_custom_fields.json");
|
|
9
|
+
let cachedFields = null;
|
|
10
|
+
export function getCustomFieldsCache() {
|
|
11
|
+
if (cachedFields) {
|
|
12
|
+
return cachedFields;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(CUSTOM_FIELDS_CACHE_FILE)) {
|
|
16
|
+
const data = JSON.parse(fs.readFileSync(CUSTOM_FIELDS_CACHE_FILE, "utf-8"));
|
|
17
|
+
cachedFields = new Map(data.fields.map((f) => [f.id, f]));
|
|
18
|
+
return cachedFields;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error("Error reading custom fields cache:", error);
|
|
23
|
+
}
|
|
24
|
+
return new Map();
|
|
25
|
+
}
|
|
26
|
+
export async function initCustomFieldsCache() {
|
|
27
|
+
const config = getConfig();
|
|
28
|
+
if (!config.REDMINE_URL || !config.REDMINE_API_KEY) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const client = axios.create({
|
|
33
|
+
baseURL: config.REDMINE_URL,
|
|
34
|
+
headers: { "X-Redmine-API-Key": config.REDMINE_API_KEY },
|
|
35
|
+
});
|
|
36
|
+
const response = await client.get("/custom_fields.json");
|
|
37
|
+
const fields = response.data.custom_fields;
|
|
38
|
+
const cache = {
|
|
39
|
+
fields,
|
|
40
|
+
updatedAt: Date.now(),
|
|
41
|
+
};
|
|
42
|
+
fs.writeFileSync(CUSTOM_FIELDS_CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
43
|
+
cachedFields = new Map(fields.map((f) => [f.id, f]));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error("Error initializing custom fields cache:", error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function enrichCustomFields(customFields) {
|
|
50
|
+
const cache = getCustomFieldsCache();
|
|
51
|
+
return customFields.map((field) => {
|
|
52
|
+
const definition = cache.get(field.id);
|
|
53
|
+
return {
|
|
54
|
+
id: field.id,
|
|
55
|
+
name: definition?.name || `Field ${field.id}`,
|
|
56
|
+
value: field.value,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
}
|
package/dist/services/redmine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios, { AxiosError } from "axios";
|
|
2
|
+
import chalk from "chalk";
|
|
2
3
|
import { getConfig } from "../utils/config.js";
|
|
3
4
|
import z from "zod";
|
|
4
5
|
function cleanRedmineMarkdown(text) {
|
|
@@ -112,6 +113,7 @@ const issueSchema = z
|
|
|
112
113
|
updated: issue.updated_on,
|
|
113
114
|
estimatedHours: issue.estimated_hours,
|
|
114
115
|
},
|
|
116
|
+
customFields: issue.custom_fields,
|
|
115
117
|
};
|
|
116
118
|
});
|
|
117
119
|
const getHttpRedmineClient = () => {
|
|
@@ -157,6 +159,12 @@ export const updateIssueComment = async (issueId, comment) => {
|
|
|
157
159
|
console.error("❌ Error actualizando Redmine.");
|
|
158
160
|
}
|
|
159
161
|
};
|
|
162
|
+
export const updateIssueCustomFields = async (ticketId, customFields) => {
|
|
163
|
+
const client = getHttpRedmineClient();
|
|
164
|
+
await client.put(`/issues/${ticketId}.json`, {
|
|
165
|
+
issue: { custom_fields: customFields },
|
|
166
|
+
});
|
|
167
|
+
};
|
|
160
168
|
const userSchema = z
|
|
161
169
|
.object({
|
|
162
170
|
id: z.number(),
|
|
@@ -196,9 +204,37 @@ export const checkRedmineConnection = async () => {
|
|
|
196
204
|
return false;
|
|
197
205
|
}
|
|
198
206
|
};
|
|
207
|
+
export function printTicketDetails(ticket) {
|
|
208
|
+
console.log(chalk.bold.cyan("\n━━━ Ticket #" + ticket.id + " ━━━"));
|
|
209
|
+
console.log(chalk.bold.white(ticket.subject));
|
|
210
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
211
|
+
console.log(chalk.gray("Estado: ") + chalk.green(ticket.status));
|
|
212
|
+
console.log(chalk.gray("Prioridad: ") + chalk.yellow(ticket.priority));
|
|
213
|
+
console.log(chalk.gray("Tracker: ") + ticket.tracker);
|
|
214
|
+
console.log(chalk.gray("Proyecto: ") + ticket.project);
|
|
215
|
+
console.log(chalk.gray("Autor: ") + ticket.author);
|
|
216
|
+
console.log(chalk.gray("URL: ") + chalk.blue.underline(ticket.url));
|
|
217
|
+
if (ticket.time.end) {
|
|
218
|
+
console.log(chalk.gray("Vencimiento: ") + chalk.red(ticket.time.end));
|
|
219
|
+
}
|
|
220
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
221
|
+
console.log(chalk.bold("Descripción:"));
|
|
222
|
+
console.log(chalk.white(ticket.description));
|
|
223
|
+
if (ticket.customFields && ticket.customFields.length > 0) {
|
|
224
|
+
console.log(chalk.dim("─".repeat(40)));
|
|
225
|
+
console.log(chalk.bold("Custom Fields:"));
|
|
226
|
+
ticket.customFields.forEach((field) => {
|
|
227
|
+
const value = field.value && String(field.value).length > 0 ? String(field.value) : chalk.dim("(vacío)");
|
|
228
|
+
console.log(chalk.gray(` ${field.name}: `) + chalk.white(value));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
console.log(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━\n"));
|
|
232
|
+
}
|
|
199
233
|
export const Redmine = {
|
|
200
234
|
getIssueDetails,
|
|
201
235
|
updateIssueComment,
|
|
236
|
+
updateIssueCustomFields,
|
|
202
237
|
getCurrentUser,
|
|
203
238
|
checkRedmineConnection,
|
|
239
|
+
printTicketDetails,
|
|
204
240
|
};
|