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.
- package/INSTALL.md +358 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/cli.js +548 -0
- package/dashboard/dist/assets/index-BzYzLB7u.css +1 -0
- package/dashboard/dist/assets/index-DIonhzlK.js +506 -0
- package/dashboard/dist/index.html +18 -0
- package/dashboard/dist/roadmap.json +268 -0
- package/dashboard/index.html +17 -0
- package/dashboard/package-lock.json +4172 -0
- package/dashboard/package.json +37 -0
- package/dashboard/postcss.config.js +6 -0
- package/dashboard/public/roadmap.json +268 -0
- package/dashboard/server.js +1366 -0
- package/dashboard/src/App.jsx +6979 -0
- package/dashboard/src/components/CircularProgress.jsx +55 -0
- package/dashboard/src/components/ProgressBar.jsx +33 -0
- package/dashboard/src/components/ProjectSettings.jsx +420 -0
- package/dashboard/src/components/SharedResources.jsx +239 -0
- package/dashboard/src/components/TaskList.jsx +273 -0
- package/dashboard/src/components/TechnicalDebt.jsx +170 -0
- package/dashboard/src/components/ui/accordion.jsx +46 -0
- package/dashboard/src/components/ui/badge.jsx +38 -0
- package/dashboard/src/components/ui/card.jsx +60 -0
- package/dashboard/src/components/ui/progress.jsx +22 -0
- package/dashboard/src/components/ui/tabs.jsx +47 -0
- package/dashboard/src/index.css +440 -0
- package/dashboard/src/lib/utils.js +6 -0
- package/dashboard/src/main.jsx +10 -0
- package/dashboard/tailwind.config.js +142 -0
- package/dashboard/vite.config.js +18 -0
- package/docker/Dockerfile +35 -0
- package/docker/docker-compose.yml +30 -0
- package/docker/entrypoint.sh +31 -0
- package/package.json +68 -0
- package/scanner.js +351 -0
- package/setup.sh +354 -0
- package/templates/clinerules.template +130 -0
- 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);
|