roadmap-kit 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.
Files changed (39) hide show
  1. package/INSTALL.md +358 -0
  2. package/LICENSE +21 -0
  3. package/README.md +503 -0
  4. package/cli.js +548 -0
  5. package/dashboard/dist/assets/index-BzYzLB7u.css +1 -0
  6. package/dashboard/dist/assets/index-DIonhzlK.js +506 -0
  7. package/dashboard/dist/index.html +18 -0
  8. package/dashboard/dist/roadmap.json +268 -0
  9. package/dashboard/index.html +17 -0
  10. package/dashboard/package-lock.json +4172 -0
  11. package/dashboard/package.json +37 -0
  12. package/dashboard/postcss.config.js +6 -0
  13. package/dashboard/public/roadmap.json +268 -0
  14. package/dashboard/server.js +1366 -0
  15. package/dashboard/src/App.jsx +6979 -0
  16. package/dashboard/src/components/CircularProgress.jsx +55 -0
  17. package/dashboard/src/components/ProgressBar.jsx +33 -0
  18. package/dashboard/src/components/ProjectSettings.jsx +420 -0
  19. package/dashboard/src/components/SharedResources.jsx +239 -0
  20. package/dashboard/src/components/TaskList.jsx +273 -0
  21. package/dashboard/src/components/TechnicalDebt.jsx +170 -0
  22. package/dashboard/src/components/ui/accordion.jsx +46 -0
  23. package/dashboard/src/components/ui/badge.jsx +38 -0
  24. package/dashboard/src/components/ui/card.jsx +60 -0
  25. package/dashboard/src/components/ui/progress.jsx +22 -0
  26. package/dashboard/src/components/ui/tabs.jsx +47 -0
  27. package/dashboard/src/index.css +440 -0
  28. package/dashboard/src/lib/utils.js +6 -0
  29. package/dashboard/src/main.jsx +10 -0
  30. package/dashboard/tailwind.config.js +142 -0
  31. package/dashboard/vite.config.js +18 -0
  32. package/docker/Dockerfile +35 -0
  33. package/docker/docker-compose.yml +30 -0
  34. package/docker/entrypoint.sh +31 -0
  35. package/package.json +68 -0
  36. package/scanner.js +351 -0
  37. package/setup.sh +354 -0
  38. package/templates/clinerules.template +130 -0
  39. package/templates/roadmap.template.json +30 -0
@@ -0,0 +1,1366 @@
1
+ import express from 'express';
2
+ import { createServer as createViteServer } from 'vite';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import crypto from 'crypto';
7
+ import Anthropic from '@anthropic-ai/sdk';
8
+ import { spawn } from 'child_process';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const ROADMAP_KIT_PATH = path.join(__dirname, '..');
12
+ const ROADMAP_PATH = path.join(ROADMAP_KIT_PATH, 'roadmap.json');
13
+ const AUTH_PATH = path.join(ROADMAP_KIT_PATH, 'auth.json');
14
+ const VERSIONS_PATH = path.join(ROADMAP_KIT_PATH, 'versions.json');
15
+ const PROJECT_ROOT = path.join(ROADMAP_KIT_PATH, '..'); // Parent of roadmap-kit
16
+ const MAX_VERSIONS = 10;
17
+
18
+ // ============ VERSION CONTROL ============
19
+ async function loadVersions() {
20
+ try {
21
+ const data = await fs.readFile(VERSIONS_PATH, 'utf-8');
22
+ return JSON.parse(data);
23
+ } catch (err) {
24
+ return { versions: [] };
25
+ }
26
+ }
27
+
28
+ async function saveVersion(roadmap, userId, userName, description = 'Manual save') {
29
+ try {
30
+ const versionsData = await loadVersions();
31
+
32
+ // Create new version entry
33
+ const version = {
34
+ id: `v-${Date.now()}`,
35
+ timestamp: new Date().toISOString(),
36
+ userId,
37
+ userName,
38
+ description,
39
+ snapshot: roadmap
40
+ };
41
+
42
+ // Add to beginning of array
43
+ versionsData.versions.unshift(version);
44
+
45
+ // Keep only last MAX_VERSIONS
46
+ if (versionsData.versions.length > MAX_VERSIONS) {
47
+ versionsData.versions = versionsData.versions.slice(0, MAX_VERSIONS);
48
+ }
49
+
50
+ await fs.writeFile(VERSIONS_PATH, JSON.stringify(versionsData, null, 2), 'utf-8');
51
+ return version;
52
+ } catch (err) {
53
+ console.error('Error saving version:', err);
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // ============ PASSWORD HASHING (using crypto.scrypt - no external deps) ============
59
+ function hashPassword(password) {
60
+ return new Promise((resolve, reject) => {
61
+ const salt = crypto.randomBytes(16).toString('hex');
62
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
63
+ if (err) reject(err);
64
+ resolve(`${salt}:${derivedKey.toString('hex')}`);
65
+ });
66
+ });
67
+ }
68
+
69
+ function verifyPassword(password, hash) {
70
+ return new Promise((resolve, reject) => {
71
+ const [salt, key] = hash.split(':');
72
+ crypto.scrypt(password, salt, 64, (err, derivedKey) => {
73
+ if (err) reject(err);
74
+ resolve(key === derivedKey.toString('hex'));
75
+ });
76
+ });
77
+ }
78
+
79
+ // ============ AUTH.JSON MANAGEMENT ============
80
+ // Note: authConfig is re-read from file on each request to support hot-reload
81
+
82
+ async function loadAuthConfig() {
83
+ try {
84
+ const data = await fs.readFile(AUTH_PATH, 'utf-8');
85
+ return JSON.parse(data);
86
+ } catch (err) {
87
+ if (err.code === 'ENOENT') {
88
+ // Create default auth.json
89
+ return await createDefaultAuthConfig();
90
+ }
91
+ throw err;
92
+ }
93
+ }
94
+
95
+ async function saveAuthConfig(config) {
96
+ await fs.writeFile(AUTH_PATH, JSON.stringify(config, null, 2), 'utf-8');
97
+ }
98
+
99
+ async function createDefaultAuthConfig() {
100
+ // Use environment variables or defaults
101
+ const adminEmail = process.env.ROADMAP_ADMIN_EMAIL || 'admin@localhost';
102
+ const adminPassword = process.env.ROADMAP_ADMIN_PASSWORD || 'Admin123!';
103
+ const adminName = process.env.ROADMAP_ADMIN_NAME || 'Admin';
104
+
105
+ const hashedPassword = await hashPassword(adminPassword);
106
+
107
+ const config = {
108
+ settings: {
109
+ requireAuth: true,
110
+ sessionDuration: 86400000, // 24h in ms
111
+ allowRegistration: false
112
+ },
113
+ users: [
114
+ {
115
+ id: `user-${Date.now()}`,
116
+ name: adminName,
117
+ email: adminEmail,
118
+ password: hashedPassword,
119
+ role: 'admin',
120
+ createdAt: new Date().toISOString(),
121
+ lastLogin: null
122
+ }
123
+ ]
124
+ };
125
+
126
+ await fs.writeFile(AUTH_PATH, JSON.stringify(config, null, 2), 'utf-8');
127
+
128
+ if (process.env.ROADMAP_ADMIN_EMAIL) {
129
+ console.log(` ➜ Auth: Created auth.json (user: ${adminEmail})`);
130
+ } else {
131
+ console.log(` ➜ Auth: Created default auth.json (user: ${adminEmail} / pass: Admin123!)`);
132
+ console.log(' ➜ Tip: Run setup.sh to configure your own credentials');
133
+ }
134
+
135
+ return config;
136
+ }
137
+
138
+ function findUserByEmail(authConfig, email) {
139
+ if (!authConfig?.users) return null;
140
+ return authConfig.users.find(u => u.email.toLowerCase() === email.toLowerCase());
141
+ }
142
+
143
+ function findUserById(authConfig, id) {
144
+ if (!authConfig?.users) return null;
145
+ return authConfig.users.find(u => u.id === id);
146
+ }
147
+
148
+ // ============ SESSION MANAGEMENT ============
149
+ const sessions = new Map();
150
+
151
+ function generateSessionId() {
152
+ return crypto.randomBytes(32).toString('hex');
153
+ }
154
+
155
+ function getSession(sessionId) {
156
+ const session = sessions.get(sessionId);
157
+ if (!session) return null;
158
+ if (Date.now() > session.expiresAt) {
159
+ sessions.delete(sessionId);
160
+ return null;
161
+ }
162
+ return session;
163
+ }
164
+
165
+ function createSession(authConfig, user) {
166
+ const sessionId = generateSessionId();
167
+ const duration = authConfig?.settings?.sessionDuration || 86400000;
168
+ sessions.set(sessionId, {
169
+ userId: user.id,
170
+ email: user.email,
171
+ name: user.name,
172
+ role: user.role,
173
+ createdAt: Date.now(),
174
+ expiresAt: Date.now() + duration
175
+ });
176
+ return { sessionId, duration };
177
+ }
178
+
179
+ // ============ AUTHENTICATION MIDDLEWARE ============
180
+ async function authMiddleware(req, res, next) {
181
+ try {
182
+ const authConfig = await loadAuthConfig();
183
+
184
+ // Check if auth is enabled
185
+ if (!authConfig?.settings?.requireAuth) {
186
+ req.user = { role: 'admin', name: 'Anonymous' }; // Grant full access when auth disabled
187
+ req.authConfig = authConfig;
188
+ return next();
189
+ }
190
+
191
+ // Check session cookie
192
+ const cookies = req.headers.cookie || '';
193
+ const sessionMatch = cookies.match(/roadmap_session=([^;]+)/);
194
+ const sessionId = sessionMatch ? sessionMatch[1] : null;
195
+
196
+ const session = sessionId ? getSession(sessionId) : null;
197
+ if (session) {
198
+ req.user = session;
199
+ req.authConfig = authConfig;
200
+ return next();
201
+ }
202
+
203
+ // Not authenticated
204
+ res.status(401).json({ error: 'No autenticado', requiresAuth: true });
205
+ } catch (err) {
206
+ console.error('Auth middleware error:', err);
207
+ res.status(500).json({ error: 'Error de autenticación' });
208
+ }
209
+ }
210
+
211
+ // Admin-only middleware
212
+ function adminMiddleware(req, res, next) {
213
+ if (req.user?.role !== 'admin') {
214
+ return res.status(403).json({ error: 'Acceso denegado. Solo administradores.' });
215
+ }
216
+ next();
217
+ }
218
+
219
+ async function createServer() {
220
+ // Ensure auth.json exists (creates default if not exists)
221
+ await loadAuthConfig();
222
+
223
+ const app = express();
224
+
225
+ // Trust proxy (for nginx/reverse proxy)
226
+ app.set('trust proxy', 1);
227
+
228
+ app.use(express.json({ limit: '10mb' }));
229
+
230
+ // Security headers
231
+ app.use((req, res, next) => {
232
+ res.setHeader('X-Content-Type-Options', 'nosniff');
233
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
234
+ res.setHeader('X-XSS-Protection', '1; mode=block');
235
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
236
+ next();
237
+ });
238
+
239
+ // API: Check auth status
240
+ app.get('/api/auth/status', async (req, res) => {
241
+ try {
242
+ const authConfig = await loadAuthConfig();
243
+ const authEnabled = authConfig?.settings?.requireAuth ?? true;
244
+
245
+ if (!authEnabled) {
246
+ return res.json({ authenticated: true, authEnabled: false, user: { role: 'admin', name: 'Anonymous' } });
247
+ }
248
+
249
+ const cookies = req.headers.cookie || '';
250
+ const sessionMatch = cookies.match(/roadmap_session=([^;]+)/);
251
+ const sessionId = sessionMatch ? sessionMatch[1] : null;
252
+ const session = sessionId ? getSession(sessionId) : null;
253
+
254
+ res.json({
255
+ authenticated: !!session,
256
+ authEnabled: true,
257
+ user: session ? { name: session.name, email: session.email, role: session.role } : null
258
+ });
259
+ } catch (err) {
260
+ res.status(500).json({ error: err.message });
261
+ }
262
+ });
263
+
264
+ // API: Login
265
+ app.post('/api/auth/login', async (req, res) => {
266
+ try {
267
+ const authConfig = await loadAuthConfig();
268
+ const { email, password } = req.body;
269
+
270
+ if (!email || !password) {
271
+ return res.status(400).json({ error: 'Email y contraseña requeridos' });
272
+ }
273
+
274
+ const user = findUserByEmail(authConfig, email);
275
+ if (!user) {
276
+ return res.status(401).json({ error: 'Credenciales inválidas' });
277
+ }
278
+
279
+ const validPassword = await verifyPassword(password, user.password);
280
+ if (!validPassword) {
281
+ return res.status(401).json({ error: 'Credenciales inválidas' });
282
+ }
283
+
284
+ // Update last login
285
+ user.lastLogin = new Date().toISOString();
286
+ await saveAuthConfig(authConfig);
287
+
288
+ const { sessionId, duration } = createSession(authConfig, user);
289
+ res.setHeader('Set-Cookie', `roadmap_session=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${duration / 1000}`);
290
+ res.json({
291
+ success: true,
292
+ message: 'Login exitoso',
293
+ user: { name: user.name, email: user.email, role: user.role }
294
+ });
295
+ } catch (err) {
296
+ console.error('Login error:', err);
297
+ res.status(500).json({ error: 'Error interno del servidor' });
298
+ }
299
+ });
300
+
301
+ // API: Logout
302
+ app.post('/api/auth/logout', (req, res) => {
303
+ const cookies = req.headers.cookie || '';
304
+ const sessionMatch = cookies.match(/roadmap_session=([^;]+)/);
305
+ if (sessionMatch) {
306
+ sessions.delete(sessionMatch[1]);
307
+ }
308
+ res.setHeader('Set-Cookie', 'roadmap_session=; Path=/; HttpOnly; Max-Age=0');
309
+ res.json({ success: true });
310
+ });
311
+
312
+ // API: Get current user
313
+ app.get('/api/auth/me', authMiddleware, (req, res) => {
314
+ res.json({ user: req.user });
315
+ });
316
+
317
+ // ============ USER MANAGEMENT (Admin only) ============
318
+
319
+ // API: List all users
320
+ app.get('/api/users', authMiddleware, adminMiddleware, (req, res) => {
321
+ const users = (req.authConfig?.users || []).map(u => ({
322
+ id: u.id,
323
+ name: u.name,
324
+ email: u.email,
325
+ role: u.role,
326
+ createdAt: u.createdAt,
327
+ lastLogin: u.lastLogin
328
+ }));
329
+ res.json({ users });
330
+ });
331
+
332
+ // API: Create user
333
+ app.post('/api/users', authMiddleware, adminMiddleware, async (req, res) => {
334
+ try {
335
+ const { name, email, password, role } = req.body;
336
+
337
+ if (!name || !email || !password) {
338
+ return res.status(400).json({ error: 'Nombre, email y contraseña requeridos' });
339
+ }
340
+
341
+ // Check if email already exists
342
+ if (findUserByEmail(req.authConfig, email)) {
343
+ return res.status(409).json({ error: 'El email ya está registrado' });
344
+ }
345
+
346
+ const hashedPassword = await hashPassword(password);
347
+ const newUser = {
348
+ id: `user-${Date.now()}`,
349
+ name,
350
+ email: email.toLowerCase(),
351
+ password: hashedPassword,
352
+ role: role || 'member',
353
+ createdAt: new Date().toISOString(),
354
+ lastLogin: null
355
+ };
356
+
357
+ req.authConfig.users.push(newUser);
358
+ await saveAuthConfig(req.authConfig);
359
+
360
+ res.json({
361
+ success: true,
362
+ user: { id: newUser.id, name: newUser.name, email: newUser.email, role: newUser.role }
363
+ });
364
+ } catch (err) {
365
+ console.error('Create user error:', err);
366
+ res.status(500).json({ error: 'Error al crear usuario' });
367
+ }
368
+ });
369
+
370
+ // API: Update user
371
+ app.put('/api/users/:id', authMiddleware, adminMiddleware, async (req, res) => {
372
+ try {
373
+ const { id } = req.params;
374
+ const { name, email, password, role } = req.body;
375
+
376
+ const user = findUserById(req.authConfig, id);
377
+ if (!user) {
378
+ return res.status(404).json({ error: 'Usuario no encontrado' });
379
+ }
380
+
381
+ // Check email uniqueness if changing
382
+ if (email && email.toLowerCase() !== user.email.toLowerCase()) {
383
+ if (findUserByEmail(req.authConfig, email)) {
384
+ return res.status(409).json({ error: 'El email ya está registrado' });
385
+ }
386
+ user.email = email.toLowerCase();
387
+ }
388
+
389
+ if (name) user.name = name;
390
+ if (role) user.role = role;
391
+ if (password) {
392
+ user.password = await hashPassword(password);
393
+ }
394
+
395
+ await saveAuthConfig(req.authConfig);
396
+
397
+ res.json({
398
+ success: true,
399
+ user: { id: user.id, name: user.name, email: user.email, role: user.role }
400
+ });
401
+ } catch (err) {
402
+ console.error('Update user error:', err);
403
+ res.status(500).json({ error: 'Error al actualizar usuario' });
404
+ }
405
+ });
406
+
407
+ // API: Delete user
408
+ app.delete('/api/users/:id', authMiddleware, adminMiddleware, async (req, res) => {
409
+ try {
410
+ const { id } = req.params;
411
+
412
+ const userIndex = req.authConfig.users.findIndex(u => u.id === id);
413
+ if (userIndex === -1) {
414
+ return res.status(404).json({ error: 'Usuario no encontrado' });
415
+ }
416
+
417
+ // Prevent deleting the last admin
418
+ const user = req.authConfig.users[userIndex];
419
+ if (user.role === 'admin') {
420
+ const adminCount = req.authConfig.users.filter(u => u.role === 'admin').length;
421
+ if (adminCount <= 1) {
422
+ return res.status(400).json({ error: 'No se puede eliminar el último administrador' });
423
+ }
424
+ }
425
+
426
+ req.authConfig.users.splice(userIndex, 1);
427
+ await saveAuthConfig(req.authConfig);
428
+
429
+ res.json({ success: true });
430
+ } catch (err) {
431
+ console.error('Delete user error:', err);
432
+ res.status(500).json({ error: 'Error al eliminar usuario' });
433
+ }
434
+ });
435
+
436
+ // API: Update own profile (any authenticated user)
437
+ app.put('/api/auth/profile', authMiddleware, async (req, res) => {
438
+ try {
439
+ const { name, email, currentPassword, newPassword } = req.body;
440
+ const userId = req.user.userId;
441
+
442
+ const user = findUserById(req.authConfig, userId);
443
+ if (!user) {
444
+ return res.status(404).json({ error: 'Usuario no encontrado' });
445
+ }
446
+
447
+ // If changing password, verify current password
448
+ if (newPassword) {
449
+ if (!currentPassword) {
450
+ return res.status(400).json({ error: 'Se requiere la contraseña actual para cambiar la contraseña' });
451
+ }
452
+ const validPassword = await verifyPassword(currentPassword, user.password);
453
+ if (!validPassword) {
454
+ return res.status(401).json({ error: 'Contraseña actual incorrecta' });
455
+ }
456
+ user.password = await hashPassword(newPassword);
457
+ }
458
+
459
+ // If changing email, check uniqueness
460
+ if (email && email.toLowerCase() !== user.email.toLowerCase()) {
461
+ if (findUserByEmail(req.authConfig, email)) {
462
+ return res.status(409).json({ error: 'El email ya está en uso' });
463
+ }
464
+ user.email = email.toLowerCase();
465
+ }
466
+
467
+ if (name) user.name = name;
468
+
469
+ await saveAuthConfig(req.authConfig);
470
+
471
+ // Update session with new info
472
+ const session = getSession(req.headers.cookie?.match(/roadmap_session=([^;]+)/)?.[1]);
473
+ if (session) {
474
+ session.name = user.name;
475
+ session.email = user.email;
476
+ }
477
+
478
+ res.json({
479
+ success: true,
480
+ user: { id: user.id, name: user.name, email: user.email, role: user.role }
481
+ });
482
+ } catch (err) {
483
+ console.error('Update profile error:', err);
484
+ res.status(500).json({ error: 'Error al actualizar perfil' });
485
+ }
486
+ });
487
+
488
+ // API: Get auth settings (Admin only)
489
+ app.get('/api/auth/settings', authMiddleware, adminMiddleware, (req, res) => {
490
+ res.json({
491
+ settings: req.authConfig?.settings || {},
492
+ userCount: req.authConfig?.users?.length || 0
493
+ });
494
+ });
495
+
496
+ // API: Update auth settings (Admin only)
497
+ app.put('/api/auth/settings', authMiddleware, adminMiddleware, async (req, res) => {
498
+ try {
499
+ const { requireAuth, sessionDuration, allowRegistration } = req.body;
500
+
501
+ if (typeof requireAuth === 'boolean') req.authConfig.settings.requireAuth = requireAuth;
502
+ if (typeof sessionDuration === 'number') req.authConfig.settings.sessionDuration = sessionDuration;
503
+ if (typeof allowRegistration === 'boolean') req.authConfig.settings.allowRegistration = allowRegistration;
504
+
505
+ await saveAuthConfig(req.authConfig);
506
+
507
+ res.json({ success: true, settings: req.authConfig.settings });
508
+ } catch (err) {
509
+ console.error('Update settings error:', err);
510
+ res.status(500).json({ error: 'Error al actualizar configuración' });
511
+ }
512
+ });
513
+
514
+ // API: Get roadmap (no auth required for reading)
515
+ app.get('/roadmap.json', async (req, res) => {
516
+ try {
517
+ const data = await fs.readFile(ROADMAP_PATH, 'utf-8');
518
+ res.json(JSON.parse(data));
519
+ } catch (err) {
520
+ if (err.code === 'ENOENT') {
521
+ res.status(404).json({ error: 'roadmap.json not found' });
522
+ } else {
523
+ res.status(500).json({ error: err.message });
524
+ }
525
+ }
526
+ });
527
+
528
+ // API: Save roadmap (requires auth)
529
+ app.post('/api/save-roadmap', authMiddleware, async (req, res) => {
530
+ try {
531
+ const { roadmap, description } = req.body.roadmap ? req.body : { roadmap: req.body, description: 'Manual save' };
532
+
533
+ // Basic validation
534
+ if (!roadmap || typeof roadmap !== 'object') {
535
+ return res.status(400).json({ error: 'Datos invalidos' });
536
+ }
537
+
538
+ // Save current version BEFORE overwriting
539
+ try {
540
+ const currentRoadmap = JSON.parse(await fs.readFile(ROADMAP_PATH, 'utf-8'));
541
+ await saveVersion(currentRoadmap, req.user?.id, req.user?.name || 'Unknown', description || 'Manual save');
542
+ } catch (versionErr) {
543
+ // First save, no previous version
544
+ console.log('No previous version to save (first save)');
545
+ }
546
+
547
+ // Sanitize: Remove any script tags or dangerous content from strings
548
+ const sanitizedRoadmap = JSON.parse(
549
+ JSON.stringify(roadmap).replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
550
+ );
551
+
552
+ // Update last_sync timestamp and last editor
553
+ if (sanitizedRoadmap.project_info) {
554
+ sanitizedRoadmap.project_info.last_sync = new Date().toISOString();
555
+ sanitizedRoadmap.project_info.last_edited_by = req.user?.name || 'Unknown';
556
+ }
557
+
558
+ await fs.writeFile(ROADMAP_PATH, JSON.stringify(sanitizedRoadmap, null, 2), 'utf-8');
559
+ res.json({ success: true, message: 'Roadmap guardado correctamente' });
560
+ } catch (err) {
561
+ console.error('Error saving roadmap:', err);
562
+ res.status(500).json({ error: err.message });
563
+ }
564
+ });
565
+
566
+ // API: Get version history
567
+ app.get('/api/versions', authMiddleware, async (req, res) => {
568
+ try {
569
+ const versionsData = await loadVersions();
570
+ // Return versions without full snapshots (just metadata)
571
+ const versions = versionsData.versions.map(v => ({
572
+ id: v.id,
573
+ timestamp: v.timestamp,
574
+ userId: v.userId,
575
+ userName: v.userName,
576
+ description: v.description,
577
+ featuresCount: v.snapshot?.features?.length || 0,
578
+ tasksCount: v.snapshot?.features?.reduce((sum, f) => sum + (f.tasks?.length || 0), 0) || 0
579
+ }));
580
+ res.json({ versions });
581
+ } catch (err) {
582
+ res.status(500).json({ error: err.message });
583
+ }
584
+ });
585
+
586
+ // API: Get specific version
587
+ app.get('/api/versions/:id', authMiddleware, async (req, res) => {
588
+ try {
589
+ const versionsData = await loadVersions();
590
+ const version = versionsData.versions.find(v => v.id === req.params.id);
591
+ if (!version) {
592
+ return res.status(404).json({ error: 'Version not found' });
593
+ }
594
+ res.json({ version });
595
+ } catch (err) {
596
+ res.status(500).json({ error: err.message });
597
+ }
598
+ });
599
+
600
+ // API: Restore a version
601
+ app.post('/api/versions/:id/restore', authMiddleware, async (req, res) => {
602
+ try {
603
+ const versionsData = await loadVersions();
604
+ const version = versionsData.versions.find(v => v.id === req.params.id);
605
+ if (!version) {
606
+ return res.status(404).json({ error: 'Version not found' });
607
+ }
608
+
609
+ // Save current as a new version before restoring
610
+ try {
611
+ const currentRoadmap = JSON.parse(await fs.readFile(ROADMAP_PATH, 'utf-8'));
612
+ await saveVersion(currentRoadmap, req.user?.id, req.user?.name || 'Unknown', `Before restore to ${version.id}`);
613
+ } catch (err) {
614
+ // Ignore if no current roadmap
615
+ }
616
+
617
+ // Restore the version
618
+ const restoredRoadmap = { ...version.snapshot };
619
+ restoredRoadmap.project_info.last_sync = new Date().toISOString();
620
+ restoredRoadmap.project_info.last_edited_by = req.user?.name || 'Unknown';
621
+ restoredRoadmap.project_info.restored_from = version.id;
622
+
623
+ await fs.writeFile(ROADMAP_PATH, JSON.stringify(restoredRoadmap, null, 2), 'utf-8');
624
+ res.json({ success: true, roadmap: restoredRoadmap });
625
+ } catch (err) {
626
+ res.status(500).json({ error: err.message });
627
+ }
628
+ });
629
+
630
+ // API: Get users for task assignment (without passwords)
631
+ app.get('/api/team-members', authMiddleware, async (req, res) => {
632
+ try {
633
+ const users = (req.authConfig?.users || []).map(u => ({
634
+ id: u.id,
635
+ name: u.name,
636
+ email: u.email,
637
+ role: u.role
638
+ }));
639
+ res.json({ members: users });
640
+ } catch (err) {
641
+ res.status(500).json({ error: err.message });
642
+ }
643
+ });
644
+
645
+ // API: Deploy files to project root (requires auth)
646
+ app.post('/api/deploy', authMiddleware, async (req, res) => {
647
+ try {
648
+ const { clinerules, cursorrules, roadmap } = req.body;
649
+
650
+ const deployedFiles = [];
651
+
652
+ // Write .clinerules to project root
653
+ if (clinerules) {
654
+ const sanitizedClinerules = clinerules.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
655
+ await fs.writeFile(path.join(PROJECT_ROOT, '.clinerules'), sanitizedClinerules, 'utf-8');
656
+ deployedFiles.push('.clinerules');
657
+ }
658
+
659
+ // Write .cursorrules to project root (optional)
660
+ if (cursorrules) {
661
+ const sanitizedCursorrules = cursorrules.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
662
+ await fs.writeFile(path.join(PROJECT_ROOT, '.cursorrules'), sanitizedCursorrules, 'utf-8');
663
+ deployedFiles.push('.cursorrules');
664
+ }
665
+
666
+ // Write roadmap.json to roadmap-kit folder
667
+ if (roadmap) {
668
+ const sanitizedRoadmap = JSON.parse(
669
+ JSON.stringify(roadmap).replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
670
+ );
671
+ await fs.writeFile(ROADMAP_PATH, JSON.stringify(sanitizedRoadmap, null, 2), 'utf-8');
672
+ deployedFiles.push('roadmap-kit/roadmap.json');
673
+ }
674
+
675
+ res.json({
676
+ success: true,
677
+ message: `Archivos desplegados: ${deployedFiles.join(', ')}`,
678
+ projectRoot: PROJECT_ROOT,
679
+ files: deployedFiles
680
+ });
681
+ } catch (err) {
682
+ console.error('Error deploying:', err);
683
+ res.status(500).json({ error: err.message });
684
+ }
685
+ });
686
+
687
+ // API: Get project info (paths, etc.)
688
+ app.get('/api/project-info', (req, res) => {
689
+ res.json({
690
+ projectRoot: PROJECT_ROOT,
691
+ roadmapKitPath: ROADMAP_KIT_PATH,
692
+ roadmapPath: ROADMAP_PATH
693
+ });
694
+ });
695
+
696
+ // System prompt for roadmap generation
697
+ const ROADMAP_SYSTEM_PROMPT = `Eres un experto en arquitectura de software y gestión de proyectos. Tu tarea es analizar la descripción de un proyecto y generar la configuración completa para ROADMAP-KIT.
698
+
699
+ ROADMAP-KIT es un sistema que ayuda a las IAs a mantener contexto sobre proyectos de desarrollo. Genera dos archivos:
700
+
701
+ 1. **roadmap.json** - Contiene toda la información del proyecto:
702
+ - project_info: nombre, versión, descripción, propósito, stack, arquitectura, convenciones
703
+ - shared_resources: componentes UI, utilidades, tablas de BD que la IA debe reutilizar
704
+ - features: funcionalidades divididas en tareas específicas
705
+
706
+ 2. **.clinerules** - Reglas que la IA debe seguir al trabajar en el proyecto
707
+
708
+ INSTRUCCIONES IMPORTANTES:
709
+
710
+ 1. **Analiza la descripción** y extrae:
711
+ - Tecnologías mencionadas → stack
712
+ - Funcionalidades requeridas → features
713
+ - Cada funcionalidad → dividir en 3-7 tareas específicas
714
+ - Patrones de arquitectura → architecture, conventions
715
+
716
+ 2. **Para cada FEATURE**:
717
+ - Crea un ID único en kebab-case (ej: "user-auth", "product-crud")
718
+ - Nombre descriptivo
719
+ - Descripción detallada
720
+ - Prioridad: high (core), medium (importante), low (nice-to-have)
721
+ - 3-7 tareas específicas y accionables
722
+
723
+ 3. **Para cada TASK**:
724
+ - ID único: {feature-id}-{task-name} en kebab-case
725
+ - Nombre corto y claro
726
+ - Descripción con instrucciones específicas de implementación
727
+ - Prioridad
728
+ - status: "pending" (todas empiezan así)
729
+
730
+ 4. **Para shared_resources**:
731
+ - Identifica componentes UI reutilizables que se necesitarán
732
+ - Identifica utilidades/helpers comunes
733
+ - Identifica tablas de base de datos necesarias
734
+
735
+ 5. **Para conventions**:
736
+ - naming: cómo nombrar variables, componentes, archivos
737
+ - file_structure: organización de carpetas
738
+ - database: convenciones de BD
739
+ - styling: sistema de estilos
740
+ - error_handling: cómo manejar errores
741
+
742
+ RESPONDE ÚNICAMENTE con un JSON válido con esta estructura exacta:
743
+ {
744
+ "roadmap": {
745
+ "project_info": {
746
+ "name": "string",
747
+ "version": "1.0.0",
748
+ "description": "string",
749
+ "purpose": "string",
750
+ "stack": ["string"],
751
+ "architecture": "string",
752
+ "total_progress": 0,
753
+ "last_sync": "ISO date string",
754
+ "conventions": {
755
+ "naming": {
756
+ "variables": "string",
757
+ "components": "string",
758
+ "files": "string",
759
+ "constants": "string"
760
+ },
761
+ "file_structure": "string",
762
+ "database": "string",
763
+ "styling": "string",
764
+ "error_handling": "string"
765
+ },
766
+ "shared_resources": {
767
+ "ui_components": [
768
+ {
769
+ "path": "string",
770
+ "description": "string",
771
+ "usage": "string"
772
+ }
773
+ ],
774
+ "utilities": [
775
+ {
776
+ "path": "string",
777
+ "description": "string",
778
+ "exports": ["string"],
779
+ "usage": "string",
780
+ "warning": "string or null"
781
+ }
782
+ ],
783
+ "database_tables": [
784
+ {
785
+ "name": "string",
786
+ "fields": ["string"],
787
+ "description": "string"
788
+ }
789
+ ]
790
+ }
791
+ },
792
+ "features": [
793
+ {
794
+ "id": "string",
795
+ "name": "string",
796
+ "description": "string",
797
+ "status": "pending",
798
+ "progress": 0,
799
+ "priority": "high|medium|low",
800
+ "tasks": [
801
+ {
802
+ "id": "string",
803
+ "name": "string",
804
+ "description": "string detallada con instrucciones",
805
+ "status": "pending",
806
+ "priority": "high|medium|low",
807
+ "ai_notes": "",
808
+ "affected_files": [],
809
+ "reused_resources": [],
810
+ "git": {
811
+ "branch": null,
812
+ "pr_number": null,
813
+ "pr_url": null,
814
+ "last_commit": null,
815
+ "commits": []
816
+ },
817
+ "metrics": {
818
+ "lines_added": 0,
819
+ "lines_removed": 0,
820
+ "files_created": 0,
821
+ "files_modified": 0,
822
+ "complexity_score": 0
823
+ },
824
+ "technical_debt": [],
825
+ "started_at": null,
826
+ "completed_at": null
827
+ }
828
+ ]
829
+ }
830
+ ]
831
+ },
832
+ "clinerules": "string con el contenido del archivo .clinerules"
833
+ }
834
+
835
+ NO incluyas explicaciones, solo el JSON. Asegúrate de que sea JSON válido.`;
836
+
837
+ // Helper function to run Claude Code CLI
838
+ function runClaudeCode(prompt) {
839
+ return new Promise((resolve, reject) => {
840
+ console.log('Running Claude Code CLI...');
841
+
842
+ // Expand PATH to include common user binary locations
843
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
844
+ const expandedPath = [
845
+ `${homedir}/.local/bin`,
846
+ `${homedir}/.npm-global/bin`,
847
+ `${homedir}/bin`,
848
+ '/usr/local/bin',
849
+ process.env.PATH
850
+ ].filter(Boolean).join(':');
851
+
852
+ const claude = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
853
+ cwd: PROJECT_ROOT,
854
+ env: { ...process.env, PATH: expandedPath },
855
+ shell: true
856
+ });
857
+
858
+ let stdout = '';
859
+ let stderr = '';
860
+
861
+ claude.stdout.on('data', (data) => {
862
+ stdout += data.toString();
863
+ });
864
+
865
+ claude.stderr.on('data', (data) => {
866
+ stderr += data.toString();
867
+ });
868
+
869
+ claude.on('close', (code) => {
870
+ if (code === 0) {
871
+ resolve(stdout);
872
+ } else {
873
+ reject(new Error(stderr || `Claude Code exited with code ${code}`));
874
+ }
875
+ });
876
+
877
+ claude.on('error', (err) => {
878
+ reject(new Error(`Failed to start Claude Code: ${err.message}. Make sure 'claude' CLI is installed and authenticated.`));
879
+ });
880
+
881
+ // Timeout after 5 minutes
882
+ setTimeout(() => {
883
+ claude.kill();
884
+ reject(new Error('Claude Code timeout after 5 minutes'));
885
+ }, 300000);
886
+ });
887
+ }
888
+
889
+ // API: Generate roadmap with Claude AI (supports both API key and Claude Code CLI)
890
+ app.post('/api/generate', async (req, res) => {
891
+ try {
892
+ const { apiKey, projectDescription, projectFiles, existingConfig, useClaudeCode } = req.body;
893
+
894
+ if (!projectDescription) {
895
+ return res.status(400).json({ error: 'Descripcion del proyecto requerida' });
896
+ }
897
+
898
+ let userMessage = `# Descripción del Proyecto\n\n${projectDescription}`;
899
+
900
+ if (projectFiles) {
901
+ userMessage += `\n\n# Archivos del Proyecto\n\n${projectFiles}`;
902
+ }
903
+
904
+ if (existingConfig) {
905
+ userMessage += `\n\n# Configuración Existente (para referencia)\n\n${JSON.stringify(existingConfig, null, 2)}`;
906
+ }
907
+
908
+ let responseText;
909
+
910
+ // Mode 1: Use Claude Code CLI (subscription-based)
911
+ if (useClaudeCode) {
912
+ console.log('Using Claude Code CLI (subscription mode)...');
913
+
914
+ const fullPrompt = `${ROADMAP_SYSTEM_PROMPT}\n\n---\n\n${userMessage}`;
915
+ responseText = await runClaudeCode(fullPrompt);
916
+ }
917
+ // Mode 2: Use Anthropic API (API key)
918
+ else {
919
+ if (!apiKey) {
920
+ return res.status(400).json({ error: 'API Key requerida (o usa el modo Claude Code)' });
921
+ }
922
+
923
+ console.log('Using Anthropic API...');
924
+ const anthropic = new Anthropic({ apiKey });
925
+
926
+ const message = await anthropic.messages.create({
927
+ model: 'claude-sonnet-4-20250514',
928
+ max_tokens: 8000,
929
+ messages: [
930
+ {
931
+ role: 'user',
932
+ content: userMessage
933
+ }
934
+ ],
935
+ system: ROADMAP_SYSTEM_PROMPT
936
+ });
937
+
938
+ responseText = message.content[0].text;
939
+ }
940
+
941
+ // Parse the JSON response
942
+ let parsed;
943
+ try {
944
+ // Try to extract JSON if there's extra text
945
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
946
+ if (jsonMatch) {
947
+ parsed = JSON.parse(jsonMatch[0]);
948
+ } else {
949
+ throw new Error('No JSON found in response');
950
+ }
951
+ } catch (parseErr) {
952
+ console.error('JSON parse error:', parseErr);
953
+ return res.status(500).json({
954
+ error: 'Error parsing AI response',
955
+ raw: responseText.substring(0, 1000)
956
+ });
957
+ }
958
+
959
+ // Validate the response structure
960
+ if (!parsed.roadmap || !parsed.clinerules) {
961
+ return res.status(500).json({
962
+ error: 'Invalid response structure from AI',
963
+ raw: responseText.substring(0, 1000)
964
+ });
965
+ }
966
+
967
+ // Add timestamp
968
+ if (parsed.roadmap.project_info) {
969
+ parsed.roadmap.project_info.last_sync = new Date().toISOString();
970
+ }
971
+
972
+ res.json({
973
+ success: true,
974
+ roadmap: parsed.roadmap,
975
+ clinerules: parsed.clinerules,
976
+ mode: useClaudeCode ? 'claude-code' : 'api'
977
+ });
978
+
979
+ } catch (err) {
980
+ console.error('AI Generation error:', err);
981
+
982
+ if (err.status === 401) {
983
+ return res.status(401).json({ error: 'API Key inválida' });
984
+ }
985
+ if (err.status === 429) {
986
+ return res.status(429).json({ error: 'Rate limit excedido. Intenta de nuevo en unos minutos.' });
987
+ }
988
+
989
+ res.status(500).json({ error: err.message || 'Error generating with AI' });
990
+ }
991
+ });
992
+
993
+ // API: Check if Claude Code CLI is available
994
+ app.get('/api/claude-code-status', async (req, res) => {
995
+ try {
996
+ // Expand PATH to include common user binary locations
997
+ const homedir = process.env.HOME || process.env.USERPROFILE || '';
998
+ const expandedPath = [
999
+ `${homedir}/.local/bin`,
1000
+ `${homedir}/.npm-global/bin`,
1001
+ `${homedir}/bin`,
1002
+ '/usr/local/bin',
1003
+ process.env.PATH
1004
+ ].filter(Boolean).join(':');
1005
+
1006
+ const claude = spawn('claude', ['--version'], {
1007
+ shell: true,
1008
+ env: { ...process.env, PATH: expandedPath }
1009
+ });
1010
+
1011
+ let version = '';
1012
+ let responded = false;
1013
+
1014
+ claude.stdout.on('data', (data) => {
1015
+ version += data.toString();
1016
+ });
1017
+
1018
+ claude.on('close', (code) => {
1019
+ if (responded) return;
1020
+ responded = true;
1021
+ if (code === 0) {
1022
+ res.json({ available: true, version: version.trim() });
1023
+ } else {
1024
+ res.json({ available: false, error: 'Claude Code not found or not authenticated' });
1025
+ }
1026
+ });
1027
+
1028
+ claude.on('error', () => {
1029
+ if (responded) return;
1030
+ responded = true;
1031
+ res.json({ available: false, error: 'Claude Code CLI not installed' });
1032
+ });
1033
+
1034
+ // Timeout
1035
+ setTimeout(() => {
1036
+ if (responded) return;
1037
+ responded = true;
1038
+ claude.kill();
1039
+ res.json({ available: false, error: 'Timeout checking Claude Code' });
1040
+ }, 5000);
1041
+
1042
+ } catch (err) {
1043
+ res.json({ available: false, error: err.message });
1044
+ }
1045
+ });
1046
+
1047
+ // API: Save API key to .env
1048
+ app.post('/api/save-api-key', authMiddleware, async (req, res) => {
1049
+ try {
1050
+ const { apiKey } = req.body;
1051
+ const envPath = path.join(ROADMAP_KIT_PATH, '.env');
1052
+
1053
+ let envContent = '';
1054
+ try {
1055
+ envContent = await fs.readFile(envPath, 'utf-8');
1056
+ } catch (e) {
1057
+ // File doesn't exist, create new
1058
+ }
1059
+
1060
+ // Update or add ANTHROPIC_API_KEY
1061
+ if (envContent.includes('ANTHROPIC_API_KEY=')) {
1062
+ envContent = envContent.replace(/ANTHROPIC_API_KEY=.*/g, `ANTHROPIC_API_KEY=${apiKey}`);
1063
+ } else {
1064
+ envContent += `\nANTHROPIC_API_KEY=${apiKey}`;
1065
+ }
1066
+
1067
+ await fs.writeFile(envPath, envContent.trim() + '\n', 'utf-8');
1068
+
1069
+ // Update process.env
1070
+ process.env.ANTHROPIC_API_KEY = apiKey;
1071
+
1072
+ res.json({ success: true });
1073
+ } catch (err) {
1074
+ res.status(500).json({ error: err.message });
1075
+ }
1076
+ });
1077
+
1078
+ // API: Get saved API key (masked)
1079
+ app.get('/api/api-key-status', (req, res) => {
1080
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1081
+ res.json({
1082
+ hasKey: !!apiKey,
1083
+ maskedKey: apiKey ? `${apiKey.substring(0, 10)}...${apiKey.substring(apiKey.length - 4)}` : null
1084
+ });
1085
+ });
1086
+
1087
+ // API: Scan project structure
1088
+ app.get('/api/scan-project', async (req, res) => {
1089
+ try {
1090
+ const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'venv', '.venv', 'coverage', '.cache'];
1091
+ const ignoreFiles = ['.DS_Store', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
1092
+
1093
+ const projectFiles = [];
1094
+ const fileTypes = {};
1095
+ let totalFiles = 0;
1096
+ let totalSize = 0;
1097
+
1098
+ async function scanDir(dir, depth = 0) {
1099
+ if (depth > 5) return; // Max depth
1100
+ try {
1101
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1102
+ for (const entry of entries) {
1103
+ if (ignoreDirs.includes(entry.name)) continue;
1104
+ if (ignoreFiles.includes(entry.name)) continue;
1105
+ if (entry.name.startsWith('.') && depth > 0) continue;
1106
+
1107
+ const fullPath = path.join(dir, entry.name);
1108
+ const relativePath = path.relative(PROJECT_ROOT, fullPath);
1109
+
1110
+ if (entry.isDirectory()) {
1111
+ await scanDir(fullPath, depth + 1);
1112
+ } else {
1113
+ try {
1114
+ const stat = await fs.stat(fullPath);
1115
+ const ext = path.extname(entry.name).toLowerCase() || 'no-ext';
1116
+ fileTypes[ext] = (fileTypes[ext] || 0) + 1;
1117
+ totalFiles++;
1118
+ totalSize += stat.size;
1119
+
1120
+ // Only include code files with preview
1121
+ const codeExts = ['.js', '.jsx', '.ts', '.tsx', '.py', '.go', '.rs', '.java', '.vue', '.svelte', '.css', '.scss', '.html', '.json', '.md', '.yaml', '.yml'];
1122
+ if (codeExts.includes(ext) && stat.size < 50000) { // Skip large files
1123
+ projectFiles.push({
1124
+ path: relativePath,
1125
+ name: entry.name,
1126
+ ext,
1127
+ size: stat.size,
1128
+ modified: stat.mtime
1129
+ });
1130
+ }
1131
+ } catch (e) {
1132
+ // Skip files we can't stat
1133
+ }
1134
+ }
1135
+ }
1136
+ } catch (e) {
1137
+ // Skip dirs we can't read
1138
+ }
1139
+ }
1140
+
1141
+ await scanDir(PROJECT_ROOT);
1142
+
1143
+ // Detect tech stack
1144
+ const techStack = [];
1145
+ const packageJsonPath = path.join(PROJECT_ROOT, 'package.json');
1146
+ try {
1147
+ const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
1148
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1149
+ if (allDeps.react) techStack.push('React');
1150
+ if (allDeps.next) techStack.push('Next.js');
1151
+ if (allDeps.vue) techStack.push('Vue');
1152
+ if (allDeps.svelte) techStack.push('Svelte');
1153
+ if (allDeps.express) techStack.push('Express');
1154
+ if (allDeps.fastify) techStack.push('Fastify');
1155
+ if (allDeps.prisma || allDeps['@prisma/client']) techStack.push('Prisma');
1156
+ if (allDeps.mongoose) techStack.push('MongoDB');
1157
+ if (allDeps.pg || allDeps.postgres) techStack.push('PostgreSQL');
1158
+ if (allDeps.typescript) techStack.push('TypeScript');
1159
+ if (allDeps.tailwindcss) techStack.push('TailwindCSS');
1160
+ } catch (e) {}
1161
+
1162
+ // Check for Python
1163
+ if (fileTypes['.py']) techStack.push('Python');
1164
+ // Check for Go
1165
+ if (fileTypes['.go']) techStack.push('Go');
1166
+
1167
+ // Get key file previews (first 50 lines of important files)
1168
+ const keyFiles = [];
1169
+ const importantFiles = ['README.md', 'package.json', 'src/App.jsx', 'src/App.tsx', 'src/index.js', 'src/main.py', 'main.go'];
1170
+ for (const filename of importantFiles) {
1171
+ const filePath = path.join(PROJECT_ROOT, filename);
1172
+ try {
1173
+ const content = await fs.readFile(filePath, 'utf-8');
1174
+ const lines = content.split('\n').slice(0, 50);
1175
+ keyFiles.push({ path: filename, preview: lines.join('\n') });
1176
+ } catch (e) {}
1177
+ }
1178
+
1179
+ res.json({
1180
+ projectRoot: PROJECT_ROOT,
1181
+ totalFiles,
1182
+ totalSize,
1183
+ fileTypes,
1184
+ techStack,
1185
+ keyFiles,
1186
+ files: projectFiles.slice(0, 200) // Limit to 200 files
1187
+ });
1188
+ } catch (err) {
1189
+ console.error('Scan error:', err);
1190
+ res.status(500).json({ error: err.message });
1191
+ }
1192
+ });
1193
+
1194
+ // API: Analyze project with Claude (enhanced)
1195
+ app.post('/api/analyze-project', async (req, res) => {
1196
+ try {
1197
+ const { requirements, projectScan, existingRoadmap, useClaudeCode } = req.body;
1198
+
1199
+ if (!requirements?.trim()) {
1200
+ return res.status(400).json({ error: 'Requirements are required' });
1201
+ }
1202
+
1203
+ // Build comprehensive prompt
1204
+ const analysisPrompt = `Eres un experto arquitecto de software. Tu tarea es analizar un proyecto existente y generar un roadmap estructurado basado en los requerimientos proporcionados.
1205
+
1206
+ ## PROYECTO ACTUAL
1207
+
1208
+ ${projectScan ? `### Estructura de archivos
1209
+ - Total archivos: ${projectScan.totalFiles}
1210
+ - Stack detectado: ${projectScan.techStack?.join(', ') || 'No detectado'}
1211
+ - Tipos de archivo: ${JSON.stringify(projectScan.fileTypes)}
1212
+
1213
+ ### Archivos clave encontrados:
1214
+ ${projectScan.keyFiles?.map(f => `#### ${f.path}\n\`\`\`\n${f.preview}\n\`\`\``).join('\n\n') || 'No disponible'}
1215
+
1216
+ ### Lista de archivos del proyecto:
1217
+ ${projectScan.files?.map(f => f.path).join('\n') || 'No disponible'}
1218
+ ` : 'No hay análisis del proyecto disponible.'}
1219
+
1220
+ ${existingRoadmap ? `### Roadmap existente:
1221
+ ${JSON.stringify(existingRoadmap.project_info, null, 2)}
1222
+
1223
+ ### Features actuales:
1224
+ ${existingRoadmap.features?.map(f => `- ${f.name} (${f.progress}% completado)`).join('\n') || 'Sin features'}
1225
+ ` : ''}
1226
+
1227
+ ## REQUERIMIENTOS DEL USUARIO
1228
+
1229
+ ${requirements}
1230
+
1231
+ ## INSTRUCCIONES
1232
+
1233
+ Analiza el proyecto y los requerimientos para generar:
1234
+
1235
+ 1. **project_info actualizado**:
1236
+ - Mantén la info existente si es válida
1237
+ - Actualiza stack, conventions basándote en el código real
1238
+ - Identifica shared_resources reales del proyecto
1239
+
1240
+ 2. **features**:
1241
+ - Crea features basadas en los requerimientos
1242
+ - Para cada feature, crea 3-7 tasks específicas y accionables
1243
+ - Indica qué archivos serían afectados basándote en la estructura real
1244
+ - Si hay features existentes completadas, mantén su estado
1245
+
1246
+ 3. **Sé específico**:
1247
+ - Usa paths reales del proyecto en affected_files
1248
+ - Referencia componentes/utilidades existentes en reused_resources
1249
+ - Las descripciones deben tener instrucciones detalladas
1250
+
1251
+ RESPONDE ÚNICAMENTE con JSON válido con esta estructura:
1252
+ {
1253
+ "roadmap": {
1254
+ "project_info": { ... },
1255
+ "features": [
1256
+ {
1257
+ "id": "feature-id",
1258
+ "name": "Feature Name",
1259
+ "description": "Descripción detallada",
1260
+ "status": "pending|in_progress|completed",
1261
+ "progress": 0,
1262
+ "priority": "high|medium|low",
1263
+ "tasks": [
1264
+ {
1265
+ "id": "task-id",
1266
+ "name": "Task name",
1267
+ "description": "Instrucciones detalladas de implementación",
1268
+ "status": "pending",
1269
+ "priority": "high|medium|low",
1270
+ "affected_files": ["path/to/file.js"],
1271
+ "reused_resources": ["@/lib/utils"],
1272
+ "ai_notes": "",
1273
+ "technical_debt": []
1274
+ }
1275
+ ]
1276
+ }
1277
+ ]
1278
+ },
1279
+ "clinerules": "contenido del archivo .clinerules",
1280
+ "analysis": {
1281
+ "summary": "Resumen del análisis",
1282
+ "suggestions": ["sugerencia 1", "sugerencia 2"],
1283
+ "warnings": ["advertencia si hay alguna"]
1284
+ }
1285
+ }`;
1286
+
1287
+ let responseText;
1288
+
1289
+ if (useClaudeCode) {
1290
+ responseText = await runClaudeCode(analysisPrompt);
1291
+ } else {
1292
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1293
+ if (!apiKey) {
1294
+ return res.status(400).json({ error: 'API Key required or use Claude Code mode' });
1295
+ }
1296
+
1297
+ const anthropic = new Anthropic({ apiKey });
1298
+ const message = await anthropic.messages.create({
1299
+ model: 'claude-sonnet-4-20250514',
1300
+ max_tokens: 16000,
1301
+ messages: [{ role: 'user', content: analysisPrompt }]
1302
+ });
1303
+ responseText = message.content[0].text;
1304
+ }
1305
+
1306
+ // Parse response
1307
+ let parsed;
1308
+ try {
1309
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
1310
+ if (jsonMatch) {
1311
+ parsed = JSON.parse(jsonMatch[0]);
1312
+ } else {
1313
+ throw new Error('No JSON found');
1314
+ }
1315
+ } catch (parseErr) {
1316
+ return res.status(500).json({
1317
+ error: 'Error parsing AI response',
1318
+ raw: responseText.substring(0, 2000)
1319
+ });
1320
+ }
1321
+
1322
+ // Add timestamp
1323
+ if (parsed.roadmap?.project_info) {
1324
+ parsed.roadmap.project_info.last_sync = new Date().toISOString();
1325
+ }
1326
+
1327
+ res.json({
1328
+ success: true,
1329
+ ...parsed
1330
+ });
1331
+ } catch (err) {
1332
+ console.error('Analysis error:', err);
1333
+ res.status(500).json({ error: err.message });
1334
+ }
1335
+ });
1336
+
1337
+ // Create Vite server in middleware mode
1338
+ const vite = await createViteServer({
1339
+ server: { middlewareMode: true },
1340
+ appType: 'spa'
1341
+ });
1342
+
1343
+ app.use(vite.middlewares);
1344
+
1345
+ const PORT = process.env.PORT || 6969;
1346
+
1347
+ // Load config for startup log
1348
+ const startupConfig = await loadAuthConfig();
1349
+ const authEnabled = startupConfig?.settings?.requireAuth ?? true;
1350
+ const userCount = startupConfig?.users?.length || 0;
1351
+
1352
+ app.listen(PORT, () => {
1353
+ console.log(`\n 🗺️ ROADMAP-KIT Dashboard`);
1354
+ console.log(` ➜ Local: http://localhost:${PORT}/`);
1355
+ console.log(` ➜ Roadmap: ${ROADMAP_PATH}`);
1356
+ console.log(` ➜ Auth: ${authEnabled ? `Habilitada (${userCount} usuarios)` : 'Deshabilitada'}`);
1357
+ if (authEnabled && userCount > 0) {
1358
+ const admins = startupConfig.users.filter(u => u.role === 'admin');
1359
+ console.log(` ➜ Admins: ${admins.map(a => a.email).join(', ')}`);
1360
+ }
1361
+ console.log(` ➜ Hot-reload: auth.json changes apply without restart`);
1362
+ console.log('');
1363
+ });
1364
+ }
1365
+
1366
+ createServer().catch(console.error);