openprompt-lang 1.2.6 → 1.3.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 +62 -8
- package/docs/EMBEDDINGS.md +214 -0
- package/docs/FRAMEWORK.md +52 -0
- package/docs/ONBOARDING_WORKFLOW.md +151 -0
- package/docs/OPL-ERRORES.md +504 -0
- package/docs/OPL_ACADEMIC_ISSUES.md +158 -0
- package/docs/WEB_SCRAPER_PLAN.md +454 -0
- package/package.json +7 -1
- package/scripts/postinstall.js +37 -0
- package/src/cli/commands-knowledge.js +1 -0
- package/src/cli/commands-opl.js +79 -1
- package/src/cli/commands-work.js +3 -1
- package/src/cli/commands-workflow.js +125 -6
- package/src/commands/init-core.js +188 -12
- package/src/commands/init-existing.js +13 -6
- package/src/commands/init-helpers.js +20 -14
- package/src/commands/knowledge-ops.js +52 -0
- package/src/commands/opl-embeddings.js +556 -0
- package/src/commands/opl-help.js +26 -2
- package/src/commands/opl-search.js +106 -2
- package/src/commands/opl-webscrape.js +390 -0
- package/src/commands/work-context.js +17 -0
- package/src/commands/workflow/close/index.js +2 -1
- package/src/commands/workflow/delivery/index.js +4 -0
- package/src/commands/workflow/discovery/index.js +4 -0
- package/src/commands/workflow/epic-cli.js +192 -0
- package/src/commands/workflow/select.js +146 -0
- package/src/commands/workflow/specification/index.js +4 -0
- package/src/commands/workflow/sprint-cli.js +174 -0
- package/src/core/engine/sandbox.js +7 -3
- package/src/core/webscrape/analyzer.js +481 -0
- package/src/core/webscrape/deep-scraper.js +1027 -0
- package/src/core/workflow/epic-manager.js +845 -0
- package/src/core/workflow/gates.js +180 -1
- package/src/core/workflow/selector.js +707 -0
- package/src/embeddings/chunker.js +450 -0
- package/src/embeddings/embedder.js +431 -0
- package/src/embeddings/index-pipeline.js +320 -0
- package/src/embeddings/vector-store.js +505 -0
- package/src/mcp-plan-server.js +12 -5
- package/src/mcp-shared-state.js +25 -0
- package/src/mcp-refactor/mcp-server.js +0 -171
- package/src/mcp-server-backup.js +0 -1913
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @kind(module)
|
|
2
|
+
// @contract(in: args:object -> out: void, sideEffect: gestiona épicas vía CLI)
|
|
3
|
+
// @limit(lines: 180)
|
|
4
|
+
// @deps(@external: [chalk, ../../core/workflow/epic-manager])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Comandos CLI para gestión de épicas.
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* npx openPrompt-Lang epic create --title "Sistema de Reportes" --desc "..."
|
|
11
|
+
* npx openPrompt-Lang epic list
|
|
12
|
+
* npx openPrompt-Lang epic show EPIC-001
|
|
13
|
+
* npx openPrompt-Lang epic close EPIC-001
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from "chalk"
|
|
17
|
+
import {
|
|
18
|
+
createEpic,
|
|
19
|
+
listEpics,
|
|
20
|
+
updateEpic,
|
|
21
|
+
addTicketsToEpic,
|
|
22
|
+
generateStatusReport,
|
|
23
|
+
} from "../../core/workflow/epic-manager.js"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Crea una nueva épica desde CLI.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {string} options.title - Título de la épica
|
|
30
|
+
* @param {string} options.desc - Descripción detallada
|
|
31
|
+
* @param {string} [options.domain] - Dominio
|
|
32
|
+
* @param {boolean} [options.noAutoTickets] - No generar tickets automáticamente
|
|
33
|
+
*/
|
|
34
|
+
export async function epicCreate(options) {
|
|
35
|
+
if (!options.title) {
|
|
36
|
+
console.log(chalk.red('\n❌ El título es requerido. Usa: --title "..."'))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.cyan(`\n📦 Creando épica: ${options.title}`))
|
|
41
|
+
console.log(chalk.gray(" Analizando descripción para generar tickets...\n"))
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const epic = createEpic(options.title, options.desc || "", {
|
|
45
|
+
autoGenerateTickets: !options.noAutoTickets,
|
|
46
|
+
domain: options.domain,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
console.log(chalk.green(`✅ Épica creada: ${epic.id}`))
|
|
50
|
+
console.log(chalk.cyan(` Título: ${epic.title}`))
|
|
51
|
+
console.log(chalk.cyan(` Tickets generados: ${epic.tickets.length}`))
|
|
52
|
+
console.log(chalk.cyan(` Días estimados: ${epic.metrics?.estimatedDays || "?"}`))
|
|
53
|
+
console.log(chalk.cyan(` Estado: ${epic.status}`))
|
|
54
|
+
console.log("")
|
|
55
|
+
|
|
56
|
+
if (epic.tickets.length > 0) {
|
|
57
|
+
console.log(chalk.gray(" Tickets creados:"))
|
|
58
|
+
for (const ticket of epic.tickets) {
|
|
59
|
+
console.log(chalk.gray(` • ${ticket.id}: ${ticket.title}`))
|
|
60
|
+
}
|
|
61
|
+
console.log("")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(chalk.cyan(" Siguientes pasos:"))
|
|
65
|
+
console.log(chalk.gray(" 1. Revisa los tickets creados"))
|
|
66
|
+
console.log(chalk.gray(" 2. Asigna a un sprint: npx openPrompt-Lang sprint create"))
|
|
67
|
+
console.log(chalk.gray(" 3. Comienza con: npx openPrompt-Lang workflow select"))
|
|
68
|
+
console.log("")
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lista las épicas.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} options
|
|
78
|
+
* @param {string} [options.status] - Filtrar por estado
|
|
79
|
+
* @param {string} [options.domain] - Filtrar por dominio
|
|
80
|
+
*/
|
|
81
|
+
export async function epicList(options) {
|
|
82
|
+
try {
|
|
83
|
+
const epics = listEpics({
|
|
84
|
+
status: options.status,
|
|
85
|
+
domain: options.domain,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (epics.length === 0) {
|
|
89
|
+
console.log(chalk.yellow("\n📭 No hay épicas.\n"))
|
|
90
|
+
console.log(chalk.gray(" Crea una con:"))
|
|
91
|
+
console.log(chalk.cyan(' npx openPrompt-Lang epic create --title "..." --desc "..."\n'))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(chalk.cyan(`\n📋 Épicas (${epics.length})\n`))
|
|
96
|
+
console.log(
|
|
97
|
+
chalk.gray(
|
|
98
|
+
` ${"ID".padEnd(12)}${"Título".padEnd(35)}${"Estado".padEnd(12)}${"Tickets".padEnd(10)}Progreso`
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
console.log(chalk.gray(` ${"─".repeat(75)}`))
|
|
102
|
+
|
|
103
|
+
for (const epic of epics) {
|
|
104
|
+
const statusColor =
|
|
105
|
+
{ planning: chalk.blue, active: chalk.green, done: chalk.gray, cancelled: chalk.red }[
|
|
106
|
+
epic.status
|
|
107
|
+
] || chalk.white
|
|
108
|
+
|
|
109
|
+
const total = epic.metrics?.totalTickets || 0
|
|
110
|
+
const done = epic.metrics?.doneTickets || 0
|
|
111
|
+
const progress = total > 0 ? `${done}/${total}` : "0/0"
|
|
112
|
+
const title = epic.title.length > 32 ? `${epic.title.slice(0, 29)}...` : epic.title
|
|
113
|
+
|
|
114
|
+
console.log(
|
|
115
|
+
` ${epic.id.padEnd(12)}${title.padEnd(35)}${statusColor(epic.status.padEnd(12))}${progress.padEnd(10)}${total > 0 ? `${Math.round((done / total) * 100)}%` : "—"}`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
console.log("")
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Muestra detalle de una épica.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} epicId
|
|
128
|
+
*/
|
|
129
|
+
export async function epicShow(epicId) {
|
|
130
|
+
try {
|
|
131
|
+
const epics = listEpics()
|
|
132
|
+
const epic = epics.find((e) => e.id === epicId)
|
|
133
|
+
|
|
134
|
+
if (!epic) {
|
|
135
|
+
console.log(chalk.red(`\n❌ Épica no encontrada: ${epicId}\n`))
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(chalk.cyan(`\n📦 ${epic.id}: ${epic.title}\n`))
|
|
140
|
+
console.log(chalk.gray(` Estado: ${epic.status}`))
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.gray(
|
|
143
|
+
` Creada: ${epic.created ? new Date(epic.created).toLocaleDateString() : "—"}`
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
console.log(chalk.gray(` Dominio: ${epic.domain || "shared"}`))
|
|
147
|
+
console.log(chalk.gray(` Días est.: ${epic.metrics?.estimatedDays || "?"}`))
|
|
148
|
+
console.log("")
|
|
149
|
+
console.log(chalk.gray(` ${epic.description || "Sin descripción"}`))
|
|
150
|
+
console.log("")
|
|
151
|
+
|
|
152
|
+
if (epic.tickets && epic.tickets.length > 0) {
|
|
153
|
+
console.log(chalk.cyan(` Tickets (${epic.tickets.length}):`))
|
|
154
|
+
for (const ticket of epic.tickets) {
|
|
155
|
+
const statusIcon = ticket.status === "done" || ticket.status === "fixed" ? "✅" : "📌"
|
|
156
|
+
console.log(` ${statusIcon} ${ticket.id}: ${ticket.title} (${ticket.status})`)
|
|
157
|
+
}
|
|
158
|
+
console.log("")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (epic.sprints && epic.sprints.length > 0) {
|
|
162
|
+
console.log(chalk.cyan(` Sprints: ${epic.sprints.join(", ")}`))
|
|
163
|
+
console.log("")
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Cierra una épica.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} epicId
|
|
174
|
+
* @param {object} options
|
|
175
|
+
* @param {string} [options.status="done"]
|
|
176
|
+
*/
|
|
177
|
+
export async function epicClose(epicId, options = {}) {
|
|
178
|
+
try {
|
|
179
|
+
const newStatus = options.status || "done"
|
|
180
|
+
const epic = updateEpic(epicId, { status: newStatus })
|
|
181
|
+
console.log(chalk.green(`\n✅ Épica ${epicId} marcada como "${newStatus}"\n`))
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Muestra el reporte de estado del proyecto.
|
|
189
|
+
*/
|
|
190
|
+
export async function epicStatus() {
|
|
191
|
+
console.log(generateStatusReport())
|
|
192
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// @kind(module)
|
|
2
|
+
// @contract(in: description:string, options:object -> out: void, sideEffect: muestra workflow seleccionado)
|
|
3
|
+
// @limit(lines: 150)
|
|
4
|
+
// @deps(@external: [chalk, ../../core/workflow/selector, ../../core/workflow/epic-manager])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI para selección inteligente de workflows.
|
|
8
|
+
*
|
|
9
|
+
* Analiza la descripción de la tarea y muestra el workflow más adecuado,
|
|
10
|
+
* incluyendo pasos, detección de conflictos y requerimientos de gates.
|
|
11
|
+
*
|
|
12
|
+
* Uso:
|
|
13
|
+
* npx openPrompt-Lang workflow select "Implementar un sistema de reportes"
|
|
14
|
+
* npx openPrompt-Lang workflow select "Arreglar bug en login" --strict
|
|
15
|
+
* npx openPrompt-Lang workflow select --list
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import chalk from "chalk"
|
|
19
|
+
import {
|
|
20
|
+
selectWorkflow,
|
|
21
|
+
listWorkflows,
|
|
22
|
+
formatWorkflowSummary,
|
|
23
|
+
} from "../../core/workflow/selector.js"
|
|
24
|
+
import { detectPlanAdjustments, generateStatusReport } from "../../core/workflow/epic-manager.js"
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ejecuta el comando de selección de workflow.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} description - Descripción de la tarea
|
|
30
|
+
* @param {object} [options]
|
|
31
|
+
* @param {boolean} [options.list] - Listar workflows disponibles
|
|
32
|
+
* @param {boolean} [options.strict] - Modo estricto: no permite omisión de gates
|
|
33
|
+
* @param {boolean} [options.json] - Salida en JSON
|
|
34
|
+
* @param {boolean} [options.status] - Mostrar reporte de estado del proyecto
|
|
35
|
+
*/
|
|
36
|
+
export async function workflowSelect(description, options = {}) {
|
|
37
|
+
if (options.list) {
|
|
38
|
+
await listAvailableWorkflows()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.status) {
|
|
43
|
+
console.log(generateStatusReport())
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!description) {
|
|
48
|
+
console.log(chalk.yellow("\n⚠️ Describe la tarea para seleccionar el workflow:"))
|
|
49
|
+
console.log(chalk.cyan(' npx openPrompt-Lang workflow select "Implementar login con OAuth"'))
|
|
50
|
+
console.log(chalk.gray("\n O usa --list para ver los workflows disponibles."))
|
|
51
|
+
console.log(chalk.gray(" O usa --status para ver el estado del proyecto.\n"))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Seleccionar workflow
|
|
56
|
+
console.log(chalk.cyan("\n🔍 Analizando tarea..."))
|
|
57
|
+
console.log(chalk.gray(` "${description}"\n`))
|
|
58
|
+
|
|
59
|
+
const match = selectWorkflow(description)
|
|
60
|
+
|
|
61
|
+
// Detectar ajustes de plan si hay épicas
|
|
62
|
+
let adjustments = []
|
|
63
|
+
try {
|
|
64
|
+
adjustments = detectPlanAdjustments(match.label || description, description)
|
|
65
|
+
} catch {
|
|
66
|
+
// El módulo de épicas puede no estar disponible
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Mostrar resultado
|
|
70
|
+
if (options.json) {
|
|
71
|
+
console.log(JSON.stringify(match, null, 2))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(formatWorkflowSummary(match))
|
|
76
|
+
|
|
77
|
+
// Mostrar alertas de ajustes de plan
|
|
78
|
+
if (adjustments.length > 0) {
|
|
79
|
+
console.log(chalk.yellow("\n⚠️ Ajustes de plan detectados:\n"))
|
|
80
|
+
for (const adj of adjustments) {
|
|
81
|
+
const color =
|
|
82
|
+
adj.overlapRatio > 60 ? chalk.red : adj.overlapRatio > 40 ? chalk.yellow : chalk.cyan
|
|
83
|
+
console.log(color(` ${adj.epicId}: "${adj.title}" — ${adj.overlapRatio}% superposición`))
|
|
84
|
+
console.log(chalk.gray(` → ${adj.suggestion}`))
|
|
85
|
+
console.log("")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Mostrar próximos pasos
|
|
90
|
+
console.log(chalk.cyan(" Próximos pasos:"))
|
|
91
|
+
const firstSteps = match.steps.slice(0, 3)
|
|
92
|
+
for (const step of firstSteps) {
|
|
93
|
+
console.log(chalk.gray(` ${step.step}. ${step.action} — ${step.purpose}`))
|
|
94
|
+
}
|
|
95
|
+
console.log(chalk.gray(` ... y ${Math.max(0, match.steps.length - 3)} paso(s) más`))
|
|
96
|
+
|
|
97
|
+
// Advertencia de gates si modo estricto
|
|
98
|
+
if (options.strict) {
|
|
99
|
+
const gates = []
|
|
100
|
+
if (match.requiresTicket) gates.push("ticket_created")
|
|
101
|
+
if (match.requiresDocs) gates.push("docs_updated")
|
|
102
|
+
if (match.requiresPlan) gates.push("plan_approved")
|
|
103
|
+
|
|
104
|
+
if (gates.length > 0) {
|
|
105
|
+
console.log(chalk.yellow(`\n 🚧 Gates requeridos (strict mode): ${gates.join(", ")}`))
|
|
106
|
+
console.log(chalk.gray(" Estos gates deben cumplirse antes de cerrar la tarea.\n"))
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log("")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Registrar workflow seleccionado en sesión si existe
|
|
113
|
+
try {
|
|
114
|
+
const { join } = await import("path")
|
|
115
|
+
const { existsSync, readFileSync, writeFileSync } = await import("fs")
|
|
116
|
+
const sessionPath = join(process.cwd(), ".opencode", "work-context", "SESSION.json")
|
|
117
|
+
if (existsSync(sessionPath)) {
|
|
118
|
+
const session = JSON.parse(readFileSync(sessionPath, "utf-8"))
|
|
119
|
+
session.selectedWorkflow = match.workflowId
|
|
120
|
+
session.selectedWorkflowLabel = match.label
|
|
121
|
+
session.selectedWorkflowSteps = match.steps.length
|
|
122
|
+
writeFileSync(sessionPath, JSON.stringify(session, null, 2), "utf-8")
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Ignorar si no hay sesión
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Lista todos los workflows disponibles.
|
|
131
|
+
*/
|
|
132
|
+
async function listAvailableWorkflows() {
|
|
133
|
+
const workflows = listWorkflows()
|
|
134
|
+
|
|
135
|
+
console.log(chalk.cyan("\n📋 Workflows Disponibles\n"))
|
|
136
|
+
console.log(chalk.gray(` ${"ID".padEnd(20)}${"Nombre".padEnd(30)}${"Triggers".padEnd(10)}Pasos`))
|
|
137
|
+
console.log(chalk.gray(` ${"─".repeat(70)}`))
|
|
138
|
+
|
|
139
|
+
for (const wf of workflows) {
|
|
140
|
+
console.log(
|
|
141
|
+
` ${wf.id.padEnd(20)}${chalk.white(wf.label.padEnd(30))}${String(wf.triggerCount).padEnd(10)}${wf.stepCount}`
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(chalk.gray("\n Usa: npx openPrompt-Lang workflow select <descripción>\n"))
|
|
146
|
+
}
|
|
@@ -8,6 +8,7 @@ import { ensureSchema } from "../../../persistence/sqlite/schema.js"
|
|
|
8
8
|
import { runMigrations } from "../../../persistence/sqlite/migrations.js"
|
|
9
9
|
import { getMetadata, setMetadata } from "../../../persistence/sqlite/queries.js"
|
|
10
10
|
import { executeTransition, canTransition } from "../../../core/workflow/transitions.js"
|
|
11
|
+
import { syncWorkflowPhase } from "../../../mcp-shared-state.js"
|
|
11
12
|
import { getWizardResponses } from "../../../wizard/orchestrator.js"
|
|
12
13
|
import { generateDocs } from "../../../docgen/generator.js"
|
|
13
14
|
import {
|
|
@@ -103,6 +104,9 @@ export async function specification(options = {}) {
|
|
|
103
104
|
executeTransition(ctx.currentPhase, "SPECIFICATION", ctx)
|
|
104
105
|
setMetadata(db, "workflow_phase", "SPECIFICATION")
|
|
105
106
|
|
|
107
|
+
// Sincronizar con MCP shared state: SPECIFICATION → modo plan
|
|
108
|
+
syncWorkflowPhase("SPECIFICATION")
|
|
109
|
+
|
|
106
110
|
conn.close()
|
|
107
111
|
|
|
108
112
|
console.log(chalk.green(`\n✅ Specification completada para: ${projectId}`))
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// @kind(module)
|
|
2
|
+
// @contract(in: args:object -> out: void, sideEffect: gestiona sprints vía CLI)
|
|
3
|
+
// @limit(lines: 160)
|
|
4
|
+
// @deps(@external: [chalk, ../../core/workflow/epic-manager])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Comandos CLI para gestión de sprints.
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* npx openPrompt-Lang sprint create "Sprint 1: Core" --goal "Implementar auth"
|
|
11
|
+
* npx openPrompt-Lang sprint plan SPRINT-001 --capacity 8
|
|
12
|
+
* npx openPrompt-Lang sprint list
|
|
13
|
+
* npx openPrompt-Lang sprint close SPRINT-001
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from "chalk"
|
|
17
|
+
import {
|
|
18
|
+
createSprint,
|
|
19
|
+
listSprints,
|
|
20
|
+
updateSprint,
|
|
21
|
+
planSprint,
|
|
22
|
+
} from "../../core/workflow/epic-manager.js"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Crea un nuevo sprint.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} name - Nombre del sprint
|
|
28
|
+
* @param {object} options
|
|
29
|
+
* @param {string} options.goal - Objetivo del sprint
|
|
30
|
+
* @param {number} [options.duration] - Duración en días (default: 14)
|
|
31
|
+
* @param {string} [options.startDate] - Fecha inicio
|
|
32
|
+
*/
|
|
33
|
+
export async function sprintCreate(name, options = {}) {
|
|
34
|
+
if (!name) {
|
|
35
|
+
console.log(chalk.red("\n❌ El nombre del sprint es requerido."))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!options.goal) {
|
|
40
|
+
console.log(chalk.red('\n❌ El objetivo del sprint es requerido. Usa: --goal "..."'))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const sprint = createSprint(name, options.goal, {
|
|
46
|
+
startDate: options.startDate,
|
|
47
|
+
durationDays: options.duration || 14,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
console.log(chalk.green(`\n✅ Sprint creado: ${sprint.id}`))
|
|
51
|
+
console.log(chalk.cyan(` Nombre: ${sprint.name}`))
|
|
52
|
+
console.log(chalk.cyan(` Objetivo: ${sprint.goal}`))
|
|
53
|
+
console.log(chalk.cyan(` Duración: ${sprint.startDate} → ${sprint.endDate}`))
|
|
54
|
+
console.log("")
|
|
55
|
+
|
|
56
|
+
console.log(chalk.cyan(" Siguientes pasos:"))
|
|
57
|
+
console.log(chalk.gray(` 1. Planificar: npx openPrompt-Lang sprint plan ${sprint.id}`))
|
|
58
|
+
console.log(chalk.gray(" 2. Asignar épicas existentes"))
|
|
59
|
+
console.log("")
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Lista los sprints.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} options
|
|
69
|
+
* @param {string} [options.status] - Filtrar por estado
|
|
70
|
+
*/
|
|
71
|
+
export async function sprintList(options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const sprints = listSprints({ status: options.status })
|
|
74
|
+
|
|
75
|
+
if (sprints.length === 0) {
|
|
76
|
+
console.log(chalk.yellow("\n📭 No hay sprints.\n"))
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.gray(' Crea uno con: npx openPrompt-Lang sprint create "Sprint 1" --goal "..."\n')
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(chalk.cyan(`\n📋 Sprints (${sprints.length})\n`))
|
|
84
|
+
console.log(
|
|
85
|
+
chalk.gray(
|
|
86
|
+
` ${"ID".padEnd(14)}${"Nombre".padEnd(30)}${"Estado".padEnd(12)}${"Rango".padEnd(24)}Progreso`
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
console.log(chalk.gray(` ${"─".repeat(85)}`))
|
|
90
|
+
|
|
91
|
+
for (const sprint of sprints) {
|
|
92
|
+
const statusColor =
|
|
93
|
+
{ planning: chalk.blue, active: chalk.green, completed: chalk.gray }[sprint.status] ||
|
|
94
|
+
chalk.white
|
|
95
|
+
|
|
96
|
+
const total = sprint.metrics?.totalTickets || 0
|
|
97
|
+
const done = sprint.metrics?.doneTickets || 0
|
|
98
|
+
const range = `${sprint.startDate || "?"} → ${sprint.endDate || "?"}`
|
|
99
|
+
const name = sprint.name.length > 27 ? `${sprint.name.slice(0, 24)}...` : sprint.name
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
` ${sprint.id.padEnd(14)}${name.padEnd(30)}${statusColor(sprint.status.padEnd(12))}${range.padEnd(24)}${total > 0 ? `${done}/${total}` : "—"}`
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
console.log("")
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Planifica un sprint automáticamente.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} sprintId
|
|
115
|
+
* @param {object} options
|
|
116
|
+
* @param {number} [options.capacity=10]
|
|
117
|
+
*/
|
|
118
|
+
export async function sprintPlan(sprintId, options = {}) {
|
|
119
|
+
if (!sprintId) {
|
|
120
|
+
console.log(chalk.red("\n❌ ID del sprint requerido."))
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
console.log(chalk.cyan(`\n📋 Planificando ${sprintId}...\n`))
|
|
126
|
+
|
|
127
|
+
const sprint = planSprint(sprintId, {
|
|
128
|
+
capacity: options.capacity || 10,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
console.log(chalk.green(`✅ Sprint planificado: ${sprint.name}`))
|
|
132
|
+
console.log(chalk.cyan(` Tickets asignados: ${sprint.ticketIds.length}`))
|
|
133
|
+
console.log(chalk.cyan(` Épicas cubiertas: ${sprint.epicIds.length}`))
|
|
134
|
+
console.log("")
|
|
135
|
+
|
|
136
|
+
if (sprint.ticketIds.length > 0) {
|
|
137
|
+
console.log(chalk.gray(" Tickets en este sprint:"))
|
|
138
|
+
for (const ticketId of sprint.ticketIds) {
|
|
139
|
+
console.log(chalk.gray(` • ${ticketId}`))
|
|
140
|
+
}
|
|
141
|
+
console.log("")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Activar sprint
|
|
145
|
+
updateSprint(sprintId, { status: "active" })
|
|
146
|
+
console.log(chalk.green(` ✅ Sprint activado. ¡A trabajar!\n`))
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Cierra un sprint.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} sprintId
|
|
156
|
+
*/
|
|
157
|
+
export async function sprintClose(sprintId) {
|
|
158
|
+
if (!sprintId) {
|
|
159
|
+
console.log(chalk.red("\n❌ ID del sprint requerido."))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const sprint = updateSprint(sprintId, { status: "completed" })
|
|
165
|
+
console.log(chalk.green(`\n✅ Sprint ${sprintId} completado.`))
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.cyan(
|
|
168
|
+
` ${sprint.metrics?.doneTickets || 0}/${sprint.metrics?.totalTickets || 0} tickets completados.\n`
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.log(chalk.red(`\n❌ Error: ${err.message}\n`))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -89,10 +89,12 @@ async function executeLocalFast(payload, cwd, startAt, startTime) {
|
|
|
89
89
|
const timeout = constraints.timeoutMs || 30000
|
|
90
90
|
let isTimeout = false
|
|
91
91
|
|
|
92
|
+
// shell: false — args ya se dividen manualmente (bin + args).
|
|
93
|
+
// Esto previene command injection si el comando contiene caracteres especiales.
|
|
92
94
|
const child = spawn(bin, args, {
|
|
93
95
|
cwd,
|
|
94
|
-
shell:
|
|
95
|
-
env: process.env,
|
|
96
|
+
shell: false,
|
|
97
|
+
env: process.env,
|
|
96
98
|
})
|
|
97
99
|
|
|
98
100
|
const timeoutId = setTimeout(() => {
|
|
@@ -210,8 +212,10 @@ async function executeDockerEphemeral(payload, cwd, startAt, startTime) {
|
|
|
210
212
|
let stderrRaw = ""
|
|
211
213
|
let isTimeout = false
|
|
212
214
|
|
|
215
|
+
// shell: false — dockerArgs ya se parsean manualmente como array.
|
|
216
|
+
// Los comandos internos de docker se ejecutan via "sh -c '<cmd>'".
|
|
213
217
|
const child = spawn("docker", dockerArgs, {
|
|
214
|
-
shell:
|
|
218
|
+
shell: false,
|
|
215
219
|
})
|
|
216
220
|
|
|
217
221
|
const timeoutId = setTimeout(() => {
|