openprompt-lang 1.2.7 → 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.
@@ -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
+ }
@@ -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
+ }