@korl3one/ccode 2.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.
@@ -0,0 +1,737 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import * as path from 'path';
6
+ import * as fs from 'fs';
7
+ import { showLogo, showWelcome, showHeader, showStep, showSuccess, showError, showWarning, showInfo, showFileTree, showProgressBar, c, } from './brand.js';
8
+ import { FileUtils } from '../utils/files.js';
9
+ import { ContextEngine } from '../core/context.js';
10
+ import { TaskEngine } from '../core/tasks.js';
11
+ import { PromptBuilder } from '../core/prompt-builder.js';
12
+ import { AIManager } from '../ai/manager.js';
13
+ import { FileWatcher, displayChanges } from './watcher.js';
14
+ // ─── Estado global de sesión ────────────────────────────────────────
15
+ const watcher = new FileWatcher();
16
+ // ─── Helpers ────────────────────────────────────────────────────────
17
+ async function requireInit() {
18
+ const ccodeDir = path.join(process.cwd(), '.ccode');
19
+ if (!(await FileUtils.exists(ccodeDir))) {
20
+ showError('Proyecto no inicializado.');
21
+ showInfo('Ejecuta: ccode init');
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ async function requireAI() {
27
+ const config = await AIManager.loadConfig();
28
+ if (!config) {
29
+ showError('Proveedor de IA no configurado.');
30
+ showInfo('Ejecuta: ccode connect');
31
+ return null;
32
+ }
33
+ return config;
34
+ }
35
+ async function promptAIConfig() {
36
+ const { provider } = await inquirer.prompt([{
37
+ type: 'select',
38
+ name: 'provider',
39
+ message: 'Proveedor de IA:',
40
+ choices: [
41
+ { name: ' Claude (Anthropic) — Recomendado', value: 'claude' },
42
+ { name: ' Ollama (Local)', value: 'ollama' },
43
+ ],
44
+ }]);
45
+ const config = { provider };
46
+ if (provider === 'claude') {
47
+ const { apiKey } = await inquirer.prompt([{
48
+ type: 'password',
49
+ name: 'apiKey',
50
+ message: 'API Key de Anthropic:',
51
+ mask: '*',
52
+ validate: (v) => v.length > 10 || 'Ingresa una API Key válida',
53
+ }]);
54
+ config.apiKey = apiKey;
55
+ const { model } = await inquirer.prompt([{
56
+ type: 'select',
57
+ name: 'model',
58
+ message: 'Modelo:',
59
+ choices: [
60
+ { name: 'Claude Sonnet 4 (recomendado)', value: 'claude-sonnet-4-20250514' },
61
+ { name: 'Claude Haiku 3.5 (rápido)', value: 'claude-haiku-4-5-20251001' },
62
+ { name: 'Claude Opus 4 (máxima calidad)', value: 'claude-opus-4-20250514' },
63
+ ],
64
+ }]);
65
+ config.model = model;
66
+ }
67
+ else {
68
+ const { model } = await inquirer.prompt([{
69
+ type: 'input',
70
+ name: 'model',
71
+ message: 'Modelo de Ollama:',
72
+ default: 'llama3',
73
+ }]);
74
+ config.model = model;
75
+ }
76
+ return config;
77
+ }
78
+ function listProjectFiles(dir, prefix = '') {
79
+ const results = [];
80
+ const ignore = ['node_modules', '.ccode', '.git', 'dist', '.next', '__pycache__', '.venv'];
81
+ try {
82
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ if (ignore.includes(entry.name) || entry.name.startsWith('.'))
85
+ continue;
86
+ const fullPath = path.join(prefix, entry.name);
87
+ if (entry.isDirectory()) {
88
+ results.push(`${fullPath}/`);
89
+ results.push(...listProjectFiles(path.join(dir, entry.name), fullPath));
90
+ }
91
+ else {
92
+ results.push(fullPath);
93
+ }
94
+ if (results.length > 100)
95
+ break;
96
+ }
97
+ }
98
+ catch { /* ignore */ }
99
+ return results;
100
+ }
101
+ // ─── SESIÓN PERSISTENTE ─────────────────────────────────────────────
102
+ async function startSession() {
103
+ // Iniciar watcher
104
+ watcher.start(process.cwd());
105
+ let running = true;
106
+ while (running) {
107
+ // Cargar estado actual
108
+ const taskEngine = new TaskEngine();
109
+ await taskEngine.load();
110
+ const stats = taskEngine.getStats();
111
+ const hasConfig = await AIManager.loadConfig();
112
+ const contextEngine = new ContextEngine();
113
+ const isLoaded = await contextEngine.load();
114
+ const state = isLoaded ? contextEngine.getState() : null;
115
+ const currentTask = state?.current_task_id
116
+ ? taskEngine.getTaskById(state.current_task_id)
117
+ : null;
118
+ // Mostrar cambios detectados
119
+ const changedFiles = watcher.flush();
120
+ if (changedFiles.length > 0) {
121
+ displayChanges(changedFiles);
122
+ }
123
+ // Mostrar estado actual compacto
124
+ if (stats.total > 0) {
125
+ console.log(c.dim(` ${showProgressBar(stats.completed, stats.total)}`));
126
+ }
127
+ if (currentTask) {
128
+ console.log(c.accent(` ▶ Tarea activa: ${currentTask.id} — ${currentTask.title}`));
129
+ }
130
+ console.log('');
131
+ // Construir menú contextual
132
+ const choices = [];
133
+ // Si hay cambios recientes y tarea activa → sugerir verificación primero
134
+ if (changedFiles.length > 0 && currentTask) {
135
+ choices.push({ name: c.accent(' 🔍 Verificar progreso (se detectaron cambios)'), value: 'verify' });
136
+ }
137
+ if (currentTask) {
138
+ choices.push({ name: ' ✅ Marcar tarea como completada', value: 'complete' }, { name: ' 🔍 Verificar progreso con IA', value: 'verify' });
139
+ }
140
+ else if (stats.pending > 0) {
141
+ choices.push({ name: ` ▶ Iniciar siguiente tarea (${stats.pending} pendientes)`, value: 'next' });
142
+ }
143
+ if (!hasConfig) {
144
+ choices.push({ name: ' 🔌 Conectar proveedor de IA', value: 'connect' });
145
+ }
146
+ choices.push({ name: ' 📋 Generar / actualizar plan de tareas', value: 'plan' }, { name: ` 📊 Ver estado completo`, value: 'status' }, { name: ' 📄 Ver contexto generado', value: 'context' });
147
+ if (hasConfig) {
148
+ choices.push({ name: ' 🔌 Reconfigurar IA', value: 'connect' });
149
+ }
150
+ choices.push({ name: ' 🚪 Salir', value: 'exit' });
151
+ // Eliminar duplicados por value
152
+ const seen = new Set();
153
+ const uniqueChoices = choices.filter(ch => {
154
+ if (seen.has(ch.value))
155
+ return false;
156
+ seen.add(ch.value);
157
+ return true;
158
+ });
159
+ const { action } = await inquirer.prompt([{
160
+ type: 'select',
161
+ name: 'action',
162
+ message: '¿Qué hacemos?',
163
+ choices: uniqueChoices,
164
+ }]);
165
+ if (action === 'exit') {
166
+ running = false;
167
+ continue;
168
+ }
169
+ const handlers = {
170
+ init: handleInit,
171
+ connect: handleConnect,
172
+ plan: handlePlan,
173
+ next: handleNext,
174
+ verify: handleVerify,
175
+ complete: handleComplete,
176
+ status: handleStatus,
177
+ context: handleContext,
178
+ };
179
+ if (handlers[action]) {
180
+ try {
181
+ await handlers[action]();
182
+ }
183
+ catch (err) {
184
+ showError(err instanceof Error ? err.message : String(err));
185
+ showInfo('La sesión sigue activa. Puedes intentar otra acción.');
186
+ }
187
+ }
188
+ // Pequeña pausa visual entre acciones
189
+ console.log('');
190
+ console.log(c.dim(' ─────────────────────────────────────────────'));
191
+ console.log(c.dim(' CCODE sigue observando tu proyecto...'));
192
+ console.log('');
193
+ }
194
+ watcher.stop();
195
+ console.log(c.dim('\n Sesión finalizada. Hasta pronto!\n'));
196
+ }
197
+ // ─── INIT ───────────────────────────────────────────────────────────
198
+ async function handleInit() {
199
+ showLogo();
200
+ console.log(c.accent(' Asistente de Inicialización de Proyecto'));
201
+ console.log(c.dim(' CCODE genera el contexto de trabajo, no el código.\n'));
202
+ const ccodeDir = path.join(process.cwd(), '.ccode');
203
+ if (await FileUtils.exists(ccodeDir)) {
204
+ showWarning('Este proyecto ya ha sido inicializado.');
205
+ showInfo('Usa "plan" para regenerar el plan de tareas.');
206
+ return;
207
+ }
208
+ // ─ Paso 1
209
+ showStep(1, 4, 'Tu Proyecto');
210
+ console.log(c.dim(' Cuéntanos sobre tu proyecto. CCODE adaptará la'));
211
+ console.log(c.dim(' complejidad del contexto automáticamente.\n'));
212
+ const projectAnswers = await inquirer.prompt([
213
+ {
214
+ type: 'input',
215
+ name: 'name',
216
+ message: 'Nombre del proyecto:',
217
+ default: path.basename(process.cwd()),
218
+ validate: (v) => v.trim().length > 0 || 'Requerido',
219
+ },
220
+ {
221
+ type: 'input',
222
+ name: 'description',
223
+ message: 'Describe tu proyecto (qué es y qué problema resuelve):',
224
+ validate: (v) => v.trim().length > 10 || 'Sé más descriptivo',
225
+ },
226
+ {
227
+ type: 'input',
228
+ name: 'features',
229
+ message: 'Funcionalidades principales (separadas por coma):',
230
+ validate: (v) => v.trim().length > 0 || 'Indica al menos una',
231
+ },
232
+ ]);
233
+ // ─ Paso 2
234
+ showStep(2, 4, 'Stack Técnico');
235
+ const techAnswers = await inquirer.prompt([
236
+ {
237
+ type: 'input',
238
+ name: 'techStack',
239
+ message: 'Stack tecnológico (ej: React, Node.js, PostgreSQL):',
240
+ validate: (v) => v.trim().length > 0 || 'Requerido',
241
+ },
242
+ {
243
+ type: 'select',
244
+ name: 'projectType',
245
+ message: 'Tipo de proyecto:',
246
+ choices: ['Web App', 'Mobile App', 'API / Backend', 'CLI', 'Librería / SDK', 'Full Stack', 'Otro'],
247
+ },
248
+ ]);
249
+ // ─ Paso 3
250
+ showStep(3, 4, 'Conexión con IA');
251
+ console.log(c.dim(' Necesitamos IA para generar el contexto.\n'));
252
+ const aiConfig = await promptAIConfig();
253
+ // ─ Paso 4
254
+ showStep(4, 4, 'Generando Contexto');
255
+ const spinner = ora({ text: 'Conectando con IA...', color: 'cyan', spinner: 'dots' }).start();
256
+ try {
257
+ await FileUtils.ensureDir(ccodeDir);
258
+ await AIManager.saveConfig(aiConfig);
259
+ spinner.text = 'Analizando proyecto y generando contexto...';
260
+ const provider = AIManager.getProvider(aiConfig);
261
+ const metaPrompt = PromptBuilder.getContextGenerationPrompt(projectAnswers.name, projectAnswers.description, projectAnswers.features, techAnswers.techStack, techAnswers.projectType);
262
+ const response = await provider.generate(metaPrompt);
263
+ spinner.text = 'Procesando...';
264
+ const generated = PromptBuilder.parseJSON(response);
265
+ spinner.text = 'Creando estructura...';
266
+ const contextData = {
267
+ name: generated.project_name || projectAnswers.name,
268
+ version: '1.0.0',
269
+ architecture_file: 'architecture.md',
270
+ rules_file: 'rules.md',
271
+ tasks_file: 'tasks.json',
272
+ };
273
+ await FileUtils.writeJson(path.join(ccodeDir, 'context.json'), contextData);
274
+ await FileUtils.writeFile(path.join(ccodeDir, 'project.md'), generated.project);
275
+ await FileUtils.writeFile(path.join(ccodeDir, 'architecture.md'), generated.architecture);
276
+ await FileUtils.writeFile(path.join(ccodeDir, 'rules.md'), generated.rules);
277
+ const tasks = (generated.tasks || []).map((t, i) => ({
278
+ id: `TASK-${String(i + 1).padStart(3, '0')}`,
279
+ title: t.title,
280
+ description: t.description,
281
+ status: 'pending',
282
+ priority: (['high', 'medium', 'low'].includes(t.priority) ? t.priority : 'medium'),
283
+ module: t.module || 'general',
284
+ }));
285
+ await FileUtils.writeJson(path.join(ccodeDir, 'tasks.json'), { tasks });
286
+ await FileUtils.writeJson(path.join(ccodeDir, 'state.json'), {
287
+ current_task_id: null, workflow_stage: 'planned',
288
+ });
289
+ await FileUtils.writeFile(path.join(ccodeDir, 'memory.md'), `# Memoria del Proyecto\n\n## Decisiones\n\n### Inicialización — ${new Date().toISOString().split('T')[0]}\n- Proyecto **${contextData.name}** creado con CCODE\n- Complejidad: ${generated.complexity || 'auto'}\n- Stack: ${techAnswers.techStack}\n- Tipo: ${techAnswers.projectType}\n- ${tasks.length} tareas generadas\n`);
290
+ await FileUtils.writeFile(path.join(ccodeDir, 'onboarding.md'), `# Guía de Onboarding\n\nCCODE mantiene el contexto del proyecto en .ccode/\nNo genera código — genera documentación, arquitectura y tareas.\n\n## Flujo\n1. CCODE te muestra la siguiente tarea\n2. Tú desarrollas con tu herramienta favorita\n3. CCODE detecta cambios y verifica progreso\n4. Cuando se cumple una tarea, la marca automáticamente\n`);
291
+ await FileUtils.writeFile(path.join(ccodeDir, 'user-prompt.md'), `# Prompt Original\n\n## Descripción\n${projectAnswers.description}\n\n## Funcionalidades\n${projectAnswers.features}\n\n## Stack\n${techAnswers.techStack}\n\n## Tipo\n${techAnswers.projectType}\n`);
292
+ spinner.succeed(c.success('Contexto generado'));
293
+ showHeader('Proyecto Listo', `${contextData.name} — ${generated.complexity || 'auto'}`);
294
+ console.log(c.dim(' Archivos de contexto en .ccode/:\n'));
295
+ showFileTree([
296
+ { name: 'project.md ', desc: 'Documentación' },
297
+ { name: 'architecture.md', desc: 'Arquitectura' },
298
+ { name: 'rules.md ', desc: 'Reglas' },
299
+ { name: 'tasks.json ', desc: `${tasks.length} tareas` },
300
+ { name: 'memory.md ', desc: 'Historial' },
301
+ { name: 'config.json ', desc: `IA: ${aiConfig.provider}` },
302
+ ]);
303
+ console.log('');
304
+ showSuccess('CCODE se queda activo observando tu proyecto.');
305
+ showInfo('Abre otra terminal y empieza a desarrollar.');
306
+ showInfo('CCODE detectará los cambios automáticamente.');
307
+ }
308
+ catch (error) {
309
+ spinner.fail('Error durante la generación');
310
+ const errMsg = error instanceof Error ? error.message : String(error);
311
+ showError(errMsg);
312
+ showWarning('Creando estructura básica...');
313
+ if (!(await FileUtils.exists(ccodeDir)))
314
+ await FileUtils.ensureDir(ccodeDir);
315
+ await FileUtils.writeJson(path.join(ccodeDir, 'context.json'), {
316
+ name: projectAnswers.name, version: '1.0.0',
317
+ architecture_file: 'architecture.md', rules_file: 'rules.md', tasks_file: 'tasks.json',
318
+ });
319
+ await FileUtils.writeJson(path.join(ccodeDir, 'tasks.json'), { tasks: [] });
320
+ await FileUtils.writeJson(path.join(ccodeDir, 'state.json'), { current_task_id: null, workflow_stage: 'created' });
321
+ await FileUtils.writeFile(path.join(ccodeDir, 'project.md'), `# ${projectAnswers.name}\n\n${projectAnswers.description}\n`);
322
+ await FileUtils.writeFile(path.join(ccodeDir, 'architecture.md'), `# Arquitectura\n\nStack: ${techAnswers.techStack}\n`);
323
+ await FileUtils.writeFile(path.join(ccodeDir, 'rules.md'), '# Reglas\n\n(Pendiente)\n');
324
+ await FileUtils.writeFile(path.join(ccodeDir, 'memory.md'), '# Memoria\n');
325
+ await FileUtils.writeFile(path.join(ccodeDir, 'onboarding.md'), '# Onboarding\n');
326
+ showSuccess('Estructura básica creada.');
327
+ }
328
+ // ─── NO salir → entrar a sesión persistente ───
329
+ // (se ejecuta después del init, la sesión queda activa)
330
+ }
331
+ // ─── CONNECT ────────────────────────────────────────────────────────
332
+ async function handleConnect() {
333
+ if (!(await requireInit()))
334
+ return;
335
+ showHeader('Configuración de IA');
336
+ const existingConfig = await AIManager.loadConfig();
337
+ if (existingConfig) {
338
+ showInfo(`Actual: ${existingConfig.provider} (${existingConfig.model || 'default'})`);
339
+ const { reconfigure } = await inquirer.prompt([{
340
+ type: 'confirm', name: 'reconfigure',
341
+ message: '¿Reconfigurar?', default: false,
342
+ }]);
343
+ if (!reconfigure)
344
+ return;
345
+ }
346
+ const config = await promptAIConfig();
347
+ const spinner = ora({ text: 'Verificando conexión...', color: 'cyan', spinner: 'dots' }).start();
348
+ const success = await AIManager.testConnection(config);
349
+ if (success) {
350
+ spinner.succeed(c.success('Conexión verificada'));
351
+ await AIManager.saveConfig(config);
352
+ showSuccess('Configuración guardada.');
353
+ }
354
+ else {
355
+ spinner.fail(c.error('No se pudo conectar'));
356
+ showError('Verifica credenciales o que Ollama esté corriendo.');
357
+ }
358
+ }
359
+ // ─── PLAN ───────────────────────────────────────────────────────────
360
+ async function handlePlan() {
361
+ if (!(await requireInit()))
362
+ return;
363
+ const config = await requireAI();
364
+ if (!config)
365
+ return;
366
+ showHeader('Plan de Tareas');
367
+ const taskEngine = new TaskEngine();
368
+ await taskEngine.load();
369
+ const currentTasks = taskEngine.getTasks();
370
+ const stats = taskEngine.getStats();
371
+ if (currentTasks.length > 0) {
372
+ showInfo(`Tareas: ${stats.completed} completadas, ${stats.pending} pendientes`);
373
+ const { action } = await inquirer.prompt([{
374
+ type: 'select', name: 'action',
375
+ message: '¿Qué hacer?',
376
+ choices: [
377
+ { name: ' Regenerar (mantiene completadas)', value: 'regenerate' },
378
+ { name: ' Cancelar', value: 'cancel' },
379
+ ],
380
+ }]);
381
+ if (action === 'cancel')
382
+ return;
383
+ }
384
+ const { extraContext } = await inquirer.prompt([{
385
+ type: 'input', name: 'extraContext',
386
+ message: 'Requisitos adicionales (Enter para omitir):',
387
+ }]);
388
+ const spinner = ora({ text: 'Generando tareas...', color: 'cyan', spinner: 'dots' }).start();
389
+ try {
390
+ const ccodePath = path.join(process.cwd(), '.ccode');
391
+ const projectMd = await FileUtils.readFileSafe(path.join(ccodePath, 'project.md'));
392
+ const archMd = await FileUtils.readFileSafe(path.join(ccodePath, 'architecture.md'));
393
+ const rulesMd = await FileUtils.readFileSafe(path.join(ccodePath, 'rules.md'));
394
+ const provider = AIManager.getProvider(config);
395
+ const planPrompt = PromptBuilder.getPlanRegenerationPrompt(projectMd, archMd, rulesMd, currentTasks, extraContext);
396
+ const response = await provider.generate(planPrompt);
397
+ const generated = PromptBuilder.parseJSON(response);
398
+ const completedTasks = currentTasks.filter(t => t.status === 'completed');
399
+ const newTasks = generated.tasks.map((t, i) => ({
400
+ id: `TASK-${String(completedTasks.length + i + 1).padStart(3, '0')}`,
401
+ title: t.title, description: t.description,
402
+ status: 'pending',
403
+ priority: (['high', 'medium', 'low'].includes(t.priority) ? t.priority : 'medium'),
404
+ module: t.module || 'general',
405
+ }));
406
+ await taskEngine.setTasks([...completedTasks, ...newTasks]);
407
+ spinner.succeed(c.success(`${newTasks.length} tareas generadas`));
408
+ newTasks.forEach(task => {
409
+ const pColor = task.priority === 'high' ? c.error : task.priority === 'medium' ? c.warning : c.dim;
410
+ console.log(` ${c.dim('○')} ${c.bold(task.id)} ${pColor(`[${task.priority.toUpperCase()}]`)} ${task.title}`);
411
+ });
412
+ console.log('');
413
+ }
414
+ catch (error) {
415
+ spinner.fail('Error');
416
+ showError(error instanceof Error ? error.message : String(error));
417
+ }
418
+ }
419
+ // ─── NEXT ───────────────────────────────────────────────────────────
420
+ async function handleNext() {
421
+ if (!(await requireInit()))
422
+ return;
423
+ const contextEngine = new ContextEngine();
424
+ await contextEngine.load();
425
+ const state = contextEngine.getState();
426
+ const taskEngine = new TaskEngine();
427
+ await taskEngine.load();
428
+ if (state.current_task_id) {
429
+ const current = taskEngine.getTaskById(state.current_task_id);
430
+ if (current && current.status === 'in_progress') {
431
+ showHeader('Tarea en Progreso');
432
+ printTaskDetail(current);
433
+ showInfo('Ve a desarrollar — CCODE detectará los cambios.');
434
+ return;
435
+ }
436
+ }
437
+ const nextTask = taskEngine.getNextTask();
438
+ if (!nextTask) {
439
+ const stats = taskEngine.getStats();
440
+ if (stats.total === 0) {
441
+ showWarning('No hay tareas.');
442
+ }
443
+ else {
444
+ showSuccess(`Todas completadas (${stats.completed}/${stats.total})`);
445
+ }
446
+ return;
447
+ }
448
+ showHeader('Siguiente Tarea');
449
+ printTaskDetail(nextTask);
450
+ const { startTask } = await inquirer.prompt([{
451
+ type: 'confirm', name: 'startTask',
452
+ message: '¿Iniciar esta tarea?', default: true,
453
+ }]);
454
+ if (startTask) {
455
+ await taskEngine.updateTaskStatus(nextTask.id, 'in_progress');
456
+ await contextEngine.updateState({ current_task_id: nextTask.id, workflow_stage: 'in_progress' });
457
+ showSuccess(`${nextTask.id} en progreso.`);
458
+ showInfo('Abre otra terminal y desarrolla. CCODE vigila los cambios.');
459
+ }
460
+ }
461
+ function printTaskDetail(task) {
462
+ const priorityColors = {
463
+ critical: c.error, high: c.warning, medium: c.primary, low: c.dim,
464
+ };
465
+ const pColor = priorityColors[task.priority] || c.white;
466
+ console.log(` ${c.bold(task.id)} ${pColor(`[${task.priority.toUpperCase()}]`)} ${c.dim(task.module)}`);
467
+ console.log('');
468
+ console.log(c.white(` ${task.title}`));
469
+ console.log('');
470
+ console.log(c.dim(' Criterios de aceptación:'));
471
+ task.description.split('\n').forEach(line => console.log(c.dim(` ${line}`)));
472
+ console.log('');
473
+ }
474
+ // ─── VERIFY ─────────────────────────────────────────────────────────
475
+ async function handleVerify() {
476
+ if (!(await requireInit()))
477
+ return;
478
+ const config = await requireAI();
479
+ if (!config)
480
+ return;
481
+ showHeader('Verificando Progreso');
482
+ const taskEngine = new TaskEngine();
483
+ await taskEngine.load();
484
+ const tasks = taskEngine.getTasks();
485
+ if (tasks.length === 0) {
486
+ showWarning('No hay tareas.');
487
+ return;
488
+ }
489
+ const spinner = ora({ text: 'Analizando proyecto...', color: 'cyan', spinner: 'dots' }).start();
490
+ try {
491
+ const ccodePath = path.join(process.cwd(), '.ccode');
492
+ const projectMd = await FileUtils.readFileSafe(path.join(ccodePath, 'project.md'));
493
+ const archMd = await FileUtils.readFileSafe(path.join(ccodePath, 'architecture.md'));
494
+ const projectFiles = listProjectFiles(process.cwd()).join('\n');
495
+ const provider = AIManager.getProvider(config);
496
+ const verifyPrompt = PromptBuilder.getVerificationPrompt(projectMd, archMd, tasks, projectFiles);
497
+ spinner.text = 'La IA verifica las tareas...';
498
+ const response = await provider.generate(verifyPrompt);
499
+ spinner.succeed(c.success('Verificación completada'));
500
+ try {
501
+ const result = PromptBuilder.parseJSON(response);
502
+ console.log('');
503
+ for (const v of result.verification) {
504
+ const task = taskEngine.getTaskById(v.task_id);
505
+ if (!task)
506
+ continue;
507
+ const icon = v.status === 'completed' ? c.success('✓')
508
+ : v.status === 'in_progress' ? c.warning('◐')
509
+ : v.status === 'blocked' ? c.error('✗') : c.dim('○');
510
+ console.log(` ${icon} ${c.bold(v.task_id)} ${task.title}`);
511
+ console.log(c.dim(` ${v.evidence}`));
512
+ if (v.missing)
513
+ console.log(c.warning(` Falta: ${v.missing}`));
514
+ console.log('');
515
+ }
516
+ console.log(c.primary(' ─ Resumen ─'));
517
+ console.log(c.white(` ${result.summary}`));
518
+ if (result.next_recommended) {
519
+ console.log(c.primary(`\n → ${result.next_recommended}`));
520
+ }
521
+ console.log('');
522
+ const { updateStates } = await inquirer.prompt([{
523
+ type: 'confirm', name: 'updateStates',
524
+ message: '¿Actualizar estados según la verificación?', default: true,
525
+ }]);
526
+ if (updateStates) {
527
+ const contextEngine = new ContextEngine();
528
+ await contextEngine.load();
529
+ for (const v of result.verification) {
530
+ const task = taskEngine.getTaskById(v.task_id);
531
+ if (!task)
532
+ continue;
533
+ if (v.status === 'completed' && task.status !== 'completed') {
534
+ await taskEngine.updateTaskStatus(v.task_id, 'completed');
535
+ // Registrar en memoria
536
+ const memoryPath = path.join(process.cwd(), '.ccode/memory.md');
537
+ const entry = `\n### Auto-completada: [${v.task_id}] ${task.title}\n- **Fecha:** ${new Date().toISOString().split('T')[0]}\n- **Verificado por IA:** ${v.evidence}\n`;
538
+ const mem = await FileUtils.readFileSafe(memoryPath, '# Memoria\n');
539
+ await FileUtils.writeFile(memoryPath, mem + entry);
540
+ // Si era la tarea activa, limpiar
541
+ if (contextEngine.getState().current_task_id === v.task_id) {
542
+ await contextEngine.updateState({ current_task_id: null, workflow_stage: 'idle' });
543
+ }
544
+ showSuccess(`${v.task_id} marcada como completada automáticamente.`);
545
+ }
546
+ }
547
+ }
548
+ }
549
+ catch {
550
+ console.log('');
551
+ response.split('\n').forEach(line => console.log(` ${line}`));
552
+ console.log('');
553
+ }
554
+ }
555
+ catch (error) {
556
+ spinner.fail('Error');
557
+ showError(error instanceof Error ? error.message : String(error));
558
+ }
559
+ }
560
+ // ─── COMPLETE ───────────────────────────────────────────────────────
561
+ async function handleComplete() {
562
+ if (!(await requireInit()))
563
+ return;
564
+ const contextEngine = new ContextEngine();
565
+ await contextEngine.load();
566
+ const state = contextEngine.getState();
567
+ const taskEngine = new TaskEngine();
568
+ await taskEngine.load();
569
+ let taskToComplete;
570
+ if (state.current_task_id) {
571
+ taskToComplete = taskEngine.getTaskById(state.current_task_id);
572
+ }
573
+ if (!taskToComplete) {
574
+ const inProgress = taskEngine.getTasks().filter(t => t.status === 'in_progress');
575
+ if (inProgress.length === 0) {
576
+ showWarning('No hay tareas activas.');
577
+ return;
578
+ }
579
+ if (inProgress.length === 1) {
580
+ taskToComplete = inProgress[0];
581
+ }
582
+ else {
583
+ const { selectedId } = await inquirer.prompt([{
584
+ type: 'select', name: 'selectedId',
585
+ message: '¿Cuál completar?',
586
+ choices: inProgress.map(t => ({ name: ` ${t.id} — ${t.title}`, value: t.id })),
587
+ }]);
588
+ taskToComplete = taskEngine.getTaskById(selectedId);
589
+ }
590
+ }
591
+ if (!taskToComplete)
592
+ return;
593
+ showHeader('Completar', `${taskToComplete.id} — ${taskToComplete.title}`);
594
+ console.log(c.dim(' Criterios:'));
595
+ taskToComplete.description.split('\n').forEach(line => console.log(c.dim(` ${line}`)));
596
+ console.log('');
597
+ const { confirmComplete } = await inquirer.prompt([{
598
+ type: 'confirm', name: 'confirmComplete',
599
+ message: '¿Criterios cumplidos?', default: true,
600
+ }]);
601
+ if (!confirmComplete) {
602
+ showInfo('Usa "verificar" para que la IA revise.');
603
+ return;
604
+ }
605
+ await taskEngine.updateTaskStatus(taskToComplete.id, 'completed');
606
+ await contextEngine.updateState({ current_task_id: null, workflow_stage: 'idle' });
607
+ const memoryPath = path.join(process.cwd(), '.ccode/memory.md');
608
+ const entry = `\n### Completada: [${taskToComplete.id}] ${taskToComplete.title}\n- **Fecha:** ${new Date().toISOString().split('T')[0]}\n`;
609
+ const mem = await FileUtils.readFileSafe(memoryPath, '# Memoria\n');
610
+ await FileUtils.writeFile(memoryPath, mem + entry);
611
+ showSuccess(`${taskToComplete.id} completada.`);
612
+ const stats = taskEngine.getStats();
613
+ console.log(c.primary(` ${showProgressBar(stats.completed, stats.total)}`));
614
+ }
615
+ // ─── STATUS ─────────────────────────────────────────────────────────
616
+ async function handleStatus() {
617
+ if (!(await requireInit()))
618
+ return;
619
+ const contextEngine = new ContextEngine();
620
+ await contextEngine.load();
621
+ const context = contextEngine.getContext();
622
+ const state = contextEngine.getState();
623
+ const taskEngine = new TaskEngine();
624
+ await taskEngine.load();
625
+ const stats = taskEngine.getStats();
626
+ const config = await AIManager.loadConfig();
627
+ showHeader('Estado', `${context.name} v${context.version}`);
628
+ if (config)
629
+ console.log(c.dim(` IA: ${config.provider} (${config.model || 'default'})`));
630
+ else
631
+ console.log(c.warning(' IA: No configurado'));
632
+ console.log('');
633
+ if (stats.total > 0) {
634
+ console.log(c.primary(` ${showProgressBar(stats.completed, stats.total)}`));
635
+ console.log('');
636
+ console.log(c.success(` ● Completadas: ${stats.completed}`));
637
+ if (stats.in_progress > 0)
638
+ console.log(c.warning(` ◐ En progreso: ${stats.in_progress}`));
639
+ console.log(c.white(` ○ Pendientes: ${stats.pending}`));
640
+ if (stats.failed > 0)
641
+ console.log(c.error(` ✗ Fallidas: ${stats.failed}`));
642
+ console.log('');
643
+ taskEngine.getTasks().forEach(task => {
644
+ const icon = task.status === 'completed' ? c.success('✓')
645
+ : task.status === 'in_progress' ? c.warning('◐')
646
+ : task.status === 'failed' ? c.error('✗') : c.dim('○');
647
+ const color = task.status === 'completed' ? c.dim : c.white;
648
+ console.log(` ${icon} ${c.dim(task.id)} ${color(task.title)}`);
649
+ });
650
+ }
651
+ else {
652
+ console.log(c.dim(' Sin tareas.'));
653
+ }
654
+ console.log('');
655
+ }
656
+ // ─── CONTEXT ────────────────────────────────────────────────────────
657
+ async function handleContext() {
658
+ if (!(await requireInit()))
659
+ return;
660
+ showHeader('Contexto del Proyecto');
661
+ const { file } = await inquirer.prompt([{
662
+ type: 'select', name: 'file',
663
+ message: '¿Qué ver?',
664
+ choices: [
665
+ { name: ' 📄 Documentación (project.md)', value: 'project.md' },
666
+ { name: ' 🏗 Arquitectura', value: 'architecture.md' },
667
+ { name: ' 📏 Reglas', value: 'rules.md' },
668
+ { name: ' 🧠 Memoria', value: 'memory.md' },
669
+ { name: ' 📝 Prompt original', value: 'user-prompt.md' },
670
+ { name: ' 📋 Contexto completo', value: '__full__' },
671
+ ],
672
+ }]);
673
+ const ccodePath = path.join(process.cwd(), '.ccode');
674
+ if (file === '__full__') {
675
+ const pb = new PromptBuilder();
676
+ const full = await pb.buildContextPrompt();
677
+ console.log('');
678
+ full.split('\n').forEach(line => console.log(` ${line}`));
679
+ }
680
+ else {
681
+ const content = await FileUtils.readFileSafe(path.join(ccodePath, file), '(Vacío)');
682
+ console.log('');
683
+ content.split('\n').forEach(line => console.log(` ${line}`));
684
+ }
685
+ console.log('');
686
+ }
687
+ // ─── CLI Setup ──────────────────────────────────────────────────────
688
+ async function main() {
689
+ const program = new Command();
690
+ program
691
+ .name('ccode')
692
+ .description('CCODE: Contexto Persistente para Desarrollo con IA')
693
+ .version('2.0.0');
694
+ // Comandos individuales (para uso rápido sin sesión)
695
+ program.command('init').description('Inicializa el contexto del proyecto').action(async () => {
696
+ await handleInit();
697
+ // Después de init → entrar en sesión
698
+ await startSession();
699
+ });
700
+ program.command('connect').description('Configura el proveedor de IA').action(handleConnect);
701
+ program.command('plan').description('Genera o regenera tareas').action(handlePlan);
702
+ program.command('next').description('Siguiente tarea').action(handleNext);
703
+ program.command('verify').description('Verifica progreso con IA').action(handleVerify);
704
+ program.command('complete').description('Completa una tarea').action(handleComplete);
705
+ program.command('status').description('Estado del proyecto').action(handleStatus);
706
+ program.command('context').description('Ver contexto generado').action(handleContext);
707
+ if (process.argv.length <= 2) {
708
+ // Sin argumentos → sesión interactiva
709
+ const isInitialized = await FileUtils.exists(path.join(process.cwd(), '.ccode'));
710
+ if (!isInitialized) {
711
+ showWelcome();
712
+ showInfo('Este directorio no tiene un proyecto CCODE.');
713
+ console.log('');
714
+ const { doInit } = await inquirer.prompt([{
715
+ type: 'confirm', name: 'doInit',
716
+ message: '¿Inicializar proyecto aquí?', default: true,
717
+ }]);
718
+ if (doInit) {
719
+ await handleInit();
720
+ await startSession();
721
+ }
722
+ }
723
+ else {
724
+ showLogo();
725
+ showSuccess('Proyecto detectado. Iniciando sesión...');
726
+ await startSession();
727
+ }
728
+ }
729
+ else {
730
+ await program.parseAsync(process.argv);
731
+ }
732
+ }
733
+ main().catch((error) => {
734
+ watcher.stop();
735
+ showError(`Error: ${error instanceof Error ? error.message : error}`);
736
+ process.exit(1);
737
+ });