roadmap-kit 1.0.1 ā 1.0.2
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/cli.js +611 -26
- package/dashboard/index.html +7 -5
- package/dashboard/server.js +35 -10
- package/dashboard/src/App.jsx +571 -175
- package/dashboard/src/index.css +555 -304
- package/dashboard/src/main.jsx +2 -2
- package/dashboard/src/themes.js +50 -0
- package/dashboard/tailwind.config.js +7 -1
- package/dashboard/vite.config.js +3 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/index-BzYzLB7u.css +0 -1
- package/dashboard/dist/assets/index-DIonhzlK.js +0 -506
- package/dashboard/dist/index.html +0 -18
- package/dashboard/dist/roadmap.json +0 -268
package/cli.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Command } from 'commander';
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
|
|
11
|
-
import { join, dirname } from 'path';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, readdirSync, statSync } from 'fs';
|
|
11
|
+
import { join, dirname, basename } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { execSync, spawn } from 'child_process';
|
|
14
|
+
import net from 'net';
|
|
14
15
|
import chalk from 'chalk';
|
|
15
16
|
import ora from 'ora';
|
|
16
17
|
import { scanGitHistory } from './scanner.js';
|
|
@@ -19,6 +20,530 @@ import { scanGitHistory } from './scanner.js';
|
|
|
19
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
21
|
const __dirname = dirname(__filename);
|
|
21
22
|
|
|
23
|
+
const DEFAULT_PORT = 6969;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Technology detection patterns by category
|
|
27
|
+
* Maps package names/files to readable technology names
|
|
28
|
+
*/
|
|
29
|
+
const TECH_DETECTION = {
|
|
30
|
+
// Frontend Frameworks
|
|
31
|
+
frameworks: {
|
|
32
|
+
'react': 'React',
|
|
33
|
+
'react-dom': 'React',
|
|
34
|
+
'next': 'Next.js',
|
|
35
|
+
'vue': 'Vue',
|
|
36
|
+
'nuxt': 'Nuxt',
|
|
37
|
+
'svelte': 'Svelte',
|
|
38
|
+
'@sveltejs/kit': 'SvelteKit',
|
|
39
|
+
'express': 'Express',
|
|
40
|
+
'@nestjs/core': 'NestJS',
|
|
41
|
+
'fastify': 'Fastify',
|
|
42
|
+
'@angular/core': 'Angular',
|
|
43
|
+
'astro': 'Astro',
|
|
44
|
+
'@remix-run/react': 'Remix'
|
|
45
|
+
},
|
|
46
|
+
// Databases & ORMs
|
|
47
|
+
databases: {
|
|
48
|
+
'prisma': 'Prisma',
|
|
49
|
+
'@prisma/client': 'Prisma',
|
|
50
|
+
'typeorm': 'TypeORM',
|
|
51
|
+
'sequelize': 'Sequelize',
|
|
52
|
+
'mongoose': 'Mongoose',
|
|
53
|
+
'drizzle-orm': 'Drizzle',
|
|
54
|
+
'knex': 'Knex',
|
|
55
|
+
'pg': 'PostgreSQL',
|
|
56
|
+
'mysql2': 'MySQL',
|
|
57
|
+
'better-sqlite3': 'SQLite',
|
|
58
|
+
'mongodb': 'MongoDB',
|
|
59
|
+
'redis': 'Redis',
|
|
60
|
+
'ioredis': 'Redis',
|
|
61
|
+
'@supabase/supabase-js': 'Supabase',
|
|
62
|
+
'firebase': 'Firebase',
|
|
63
|
+
'firebase-admin': 'Firebase'
|
|
64
|
+
},
|
|
65
|
+
// Styling
|
|
66
|
+
styling: {
|
|
67
|
+
'tailwindcss': 'TailwindCSS',
|
|
68
|
+
'styled-components': 'Styled Components',
|
|
69
|
+
'@emotion/react': 'Emotion',
|
|
70
|
+
'sass': 'SCSS',
|
|
71
|
+
'@mui/material': 'Material UI',
|
|
72
|
+
'antd': 'Ant Design',
|
|
73
|
+
'@chakra-ui/react': 'Chakra UI',
|
|
74
|
+
'bootstrap': 'Bootstrap'
|
|
75
|
+
},
|
|
76
|
+
// Testing
|
|
77
|
+
testing: {
|
|
78
|
+
'jest': 'Jest',
|
|
79
|
+
'vitest': 'Vitest',
|
|
80
|
+
'mocha': 'Mocha',
|
|
81
|
+
'@playwright/test': 'Playwright',
|
|
82
|
+
'cypress': 'Cypress',
|
|
83
|
+
'@testing-library/react': 'Testing Library'
|
|
84
|
+
},
|
|
85
|
+
// Build Tools
|
|
86
|
+
build: {
|
|
87
|
+
'vite': 'Vite',
|
|
88
|
+
'webpack': 'Webpack',
|
|
89
|
+
'esbuild': 'esbuild',
|
|
90
|
+
'parcel': 'Parcel',
|
|
91
|
+
'rollup': 'Rollup',
|
|
92
|
+
'turbo': 'Turborepo'
|
|
93
|
+
},
|
|
94
|
+
// Developer Tools
|
|
95
|
+
tools: {
|
|
96
|
+
'typescript': 'TypeScript',
|
|
97
|
+
'eslint': 'ESLint',
|
|
98
|
+
'prettier': 'Prettier',
|
|
99
|
+
'husky': 'Husky',
|
|
100
|
+
'zod': 'Zod',
|
|
101
|
+
'axios': 'Axios',
|
|
102
|
+
'graphql': 'GraphQL',
|
|
103
|
+
'@trpc/server': 'tRPC',
|
|
104
|
+
'socket.io': 'Socket.io'
|
|
105
|
+
},
|
|
106
|
+
// Python (detected from requirements.txt)
|
|
107
|
+
python: {
|
|
108
|
+
'django': 'Django',
|
|
109
|
+
'flask': 'Flask',
|
|
110
|
+
'fastapi': 'FastAPI',
|
|
111
|
+
'sqlalchemy': 'SQLAlchemy',
|
|
112
|
+
'pytest': 'Pytest',
|
|
113
|
+
'celery': 'Celery'
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Configuration files to detect
|
|
119
|
+
*/
|
|
120
|
+
const CONFIG_PATTERNS = [
|
|
121
|
+
'tsconfig.json',
|
|
122
|
+
'.eslintrc',
|
|
123
|
+
'.eslintrc.js',
|
|
124
|
+
'.eslintrc.json',
|
|
125
|
+
'eslint.config.js',
|
|
126
|
+
'prettier.config.js',
|
|
127
|
+
'.prettierrc',
|
|
128
|
+
'.prettierrc.js',
|
|
129
|
+
'tailwind.config.js',
|
|
130
|
+
'tailwind.config.ts',
|
|
131
|
+
'vite.config.js',
|
|
132
|
+
'vite.config.ts',
|
|
133
|
+
'webpack.config.js',
|
|
134
|
+
'next.config.js',
|
|
135
|
+
'next.config.mjs',
|
|
136
|
+
'prisma/schema.prisma',
|
|
137
|
+
'Dockerfile',
|
|
138
|
+
'docker-compose.yml',
|
|
139
|
+
'docker-compose.yaml',
|
|
140
|
+
'.env',
|
|
141
|
+
'.env.local',
|
|
142
|
+
'.env.example'
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a port is available
|
|
147
|
+
* @param {number} port - Port to check
|
|
148
|
+
* @returns {Promise<boolean>} - True if port is available
|
|
149
|
+
*/
|
|
150
|
+
function isPortAvailable(port) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const server = net.createServer();
|
|
153
|
+
|
|
154
|
+
server.once('error', (err) => {
|
|
155
|
+
if (err.code === 'EADDRINUSE') {
|
|
156
|
+
resolve(false);
|
|
157
|
+
} else {
|
|
158
|
+
resolve(false);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.once('listening', () => {
|
|
163
|
+
server.close();
|
|
164
|
+
resolve(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
server.listen(port, '127.0.0.1');
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find an available port starting from startPort
|
|
173
|
+
* @param {number} startPort - Port to start searching from
|
|
174
|
+
* @param {number} maxAttempts - Maximum number of ports to try
|
|
175
|
+
* @returns {Promise<number>} - Available port number
|
|
176
|
+
*/
|
|
177
|
+
async function findAvailablePort(startPort = DEFAULT_PORT, maxAttempts = 10) {
|
|
178
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
179
|
+
const port = startPort + i;
|
|
180
|
+
if (await isPortAvailable(port)) {
|
|
181
|
+
return port;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`No available port found between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Detect folder structure pattern
|
|
189
|
+
* @param {string} projectRoot - Project root directory
|
|
190
|
+
* @returns {string} - Detected pattern (feature-based, layer-based, app-router, mixed, flat)
|
|
191
|
+
*/
|
|
192
|
+
function detectFolderPattern(projectRoot) {
|
|
193
|
+
const srcPath = join(projectRoot, 'src');
|
|
194
|
+
const appPath = join(projectRoot, 'app');
|
|
195
|
+
|
|
196
|
+
// Check for Next.js app router
|
|
197
|
+
if (existsSync(appPath)) {
|
|
198
|
+
try {
|
|
199
|
+
const appContents = readdirSync(appPath);
|
|
200
|
+
if (appContents.some(f => f.startsWith('(') || f === 'layout.tsx' || f === 'layout.js')) {
|
|
201
|
+
return 'app-router';
|
|
202
|
+
}
|
|
203
|
+
} catch (e) { /* ignore */ }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!existsSync(srcPath)) {
|
|
207
|
+
return 'flat';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const srcContents = readdirSync(srcPath);
|
|
212
|
+
const dirs = srcContents.filter(f => {
|
|
213
|
+
try {
|
|
214
|
+
return statSync(join(srcPath, f)).isDirectory();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Layer-based patterns
|
|
221
|
+
const layerPatterns = ['controllers', 'services', 'models', 'repositories', 'routes', 'middleware'];
|
|
222
|
+
const hasLayerPattern = dirs.some(d => layerPatterns.includes(d.toLowerCase()));
|
|
223
|
+
|
|
224
|
+
// Feature-based patterns
|
|
225
|
+
const featurePatterns = ['features', 'modules', 'domains'];
|
|
226
|
+
const hasFeaturePattern = dirs.some(d => featurePatterns.includes(d.toLowerCase()));
|
|
227
|
+
|
|
228
|
+
// Common mixed patterns
|
|
229
|
+
const commonDirs = ['components', 'lib', 'utils', 'hooks', 'styles', 'pages', 'api'];
|
|
230
|
+
const hasCommonDirs = dirs.some(d => commonDirs.includes(d.toLowerCase()));
|
|
231
|
+
|
|
232
|
+
if (hasFeaturePattern) return 'feature-based';
|
|
233
|
+
if (hasLayerPattern) return 'layer-based';
|
|
234
|
+
if (hasCommonDirs && dirs.length > 3) return 'mixed';
|
|
235
|
+
return 'flat';
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return 'flat';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Scan for shared resources (UI components, utilities, DB tables)
|
|
243
|
+
* @param {string} projectRoot - Project root directory
|
|
244
|
+
* @returns {Object} - { ui_components, utilities, database_tables }
|
|
245
|
+
*/
|
|
246
|
+
function scanSharedResources(projectRoot) {
|
|
247
|
+
const resources = {
|
|
248
|
+
ui_components: [],
|
|
249
|
+
utilities: [],
|
|
250
|
+
database_tables: []
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Common paths for components
|
|
254
|
+
const componentPaths = [
|
|
255
|
+
join(projectRoot, 'src', 'components'),
|
|
256
|
+
join(projectRoot, 'src', 'components', 'ui'),
|
|
257
|
+
join(projectRoot, 'components'),
|
|
258
|
+
join(projectRoot, 'app', 'components')
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
// Common paths for utilities
|
|
262
|
+
const utilityPaths = [
|
|
263
|
+
join(projectRoot, 'src', 'lib'),
|
|
264
|
+
join(projectRoot, 'src', 'utils'),
|
|
265
|
+
join(projectRoot, 'lib'),
|
|
266
|
+
join(projectRoot, 'utils')
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Scan for UI components
|
|
270
|
+
for (const compPath of componentPaths) {
|
|
271
|
+
if (existsSync(compPath)) {
|
|
272
|
+
try {
|
|
273
|
+
const files = readdirSync(compPath);
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (file.match(/\.(jsx|tsx)$/) && !file.includes('.test.') && !file.includes('.spec.')) {
|
|
276
|
+
const fullPath = join(compPath, file);
|
|
277
|
+
try {
|
|
278
|
+
if (statSync(fullPath).isFile()) {
|
|
279
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
280
|
+
const name = basename(file, file.includes('.tsx') ? '.tsx' : '.jsx');
|
|
281
|
+
resources.ui_components.push({
|
|
282
|
+
path: relativePath,
|
|
283
|
+
description: `${name} component`,
|
|
284
|
+
usage: `import { ${name} } from '@/${relativePath.replace(/\.(jsx|tsx)$/, '')}'`
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
} catch (e) { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch (e) { /* ignore */ }
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Scan for utilities
|
|
295
|
+
for (const utilPath of utilityPaths) {
|
|
296
|
+
if (existsSync(utilPath)) {
|
|
297
|
+
try {
|
|
298
|
+
const files = readdirSync(utilPath);
|
|
299
|
+
for (const file of files) {
|
|
300
|
+
if (file.match(/\.(js|ts)$/) && !file.match(/\.(d\.ts|test\.|spec\.)/) && !file.includes('index')) {
|
|
301
|
+
const fullPath = join(utilPath, file);
|
|
302
|
+
try {
|
|
303
|
+
if (statSync(fullPath).isFile()) {
|
|
304
|
+
const relativePath = fullPath.replace(projectRoot + '/', '');
|
|
305
|
+
const name = basename(file, file.includes('.ts') ? '.ts' : '.js');
|
|
306
|
+
resources.utilities.push({
|
|
307
|
+
path: relativePath,
|
|
308
|
+
description: `${name} utility`,
|
|
309
|
+
exports: [],
|
|
310
|
+
usage: `import { ... } from '@/${relativePath.replace(/\.(js|ts)$/, '')}'`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
} catch (e) { /* ignore */ }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch (e) { /* ignore */ }
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Scan for Prisma schema (database tables)
|
|
321
|
+
const prismaSchemaPath = join(projectRoot, 'prisma', 'schema.prisma');
|
|
322
|
+
if (existsSync(prismaSchemaPath)) {
|
|
323
|
+
try {
|
|
324
|
+
const schemaContent = readFileSync(prismaSchemaPath, 'utf-8');
|
|
325
|
+
const modelRegex = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
326
|
+
let match;
|
|
327
|
+
|
|
328
|
+
while ((match = modelRegex.exec(schemaContent)) !== null) {
|
|
329
|
+
const modelName = match[1];
|
|
330
|
+
const modelBody = match[2];
|
|
331
|
+
|
|
332
|
+
// Extract field names (first word of each line)
|
|
333
|
+
const fields = modelBody
|
|
334
|
+
.split('\n')
|
|
335
|
+
.map(line => line.trim())
|
|
336
|
+
.filter(line => line && !line.startsWith('//') && !line.startsWith('@@'))
|
|
337
|
+
.map(line => line.split(/\s+/)[0])
|
|
338
|
+
.filter(f => f);
|
|
339
|
+
|
|
340
|
+
resources.database_tables.push({
|
|
341
|
+
name: modelName.toLowerCase(),
|
|
342
|
+
description: `${modelName} model`,
|
|
343
|
+
fields: fields.slice(0, 10) // Limit to first 10 fields
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
} catch (e) { /* ignore */ }
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return resources;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Analyze git history for project maturity
|
|
354
|
+
* @param {string} projectRoot - Project root directory
|
|
355
|
+
* @returns {Object|null} - Git info or null if not a git repo
|
|
356
|
+
*/
|
|
357
|
+
function analyzeGitHistory(projectRoot) {
|
|
358
|
+
try {
|
|
359
|
+
// Check if it's a git repo
|
|
360
|
+
if (!existsSync(join(projectRoot, '.git'))) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Count commits
|
|
365
|
+
const commitCount = parseInt(
|
|
366
|
+
execSync('git rev-list --count HEAD 2>/dev/null', { cwd: projectRoot, encoding: 'utf-8' }).trim(),
|
|
367
|
+
10
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Get first commit date
|
|
371
|
+
let firstCommit = null;
|
|
372
|
+
let lastCommit = null;
|
|
373
|
+
try {
|
|
374
|
+
firstCommit = execSync('git log --reverse --format=%aI | head -1', { cwd: projectRoot, encoding: 'utf-8', shell: true }).trim();
|
|
375
|
+
lastCommit = execSync('git log -1 --format=%aI', { cwd: projectRoot, encoding: 'utf-8' }).trim();
|
|
376
|
+
} catch (e) { /* ignore */ }
|
|
377
|
+
|
|
378
|
+
// Determine maturity
|
|
379
|
+
let maturity;
|
|
380
|
+
if (commitCount < 20) {
|
|
381
|
+
maturity = 'new';
|
|
382
|
+
} else if (commitCount <= 100) {
|
|
383
|
+
maturity = 'early';
|
|
384
|
+
} else {
|
|
385
|
+
maturity = 'established';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
commit_count: commitCount,
|
|
390
|
+
maturity,
|
|
391
|
+
first_commit: firstCommit,
|
|
392
|
+
last_commit: lastCommit
|
|
393
|
+
};
|
|
394
|
+
} catch (e) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Extract conventions from config files
|
|
401
|
+
* @param {string} projectRoot - Project root directory
|
|
402
|
+
* @param {string[]} configFiles - List of detected config files
|
|
403
|
+
* @returns {Object} - Conventions object
|
|
404
|
+
*/
|
|
405
|
+
function extractConventions(projectRoot, configFiles) {
|
|
406
|
+
const conventions = {
|
|
407
|
+
naming: {
|
|
408
|
+
variables: 'camelCase',
|
|
409
|
+
components: 'PascalCase',
|
|
410
|
+
files: 'kebab-case for utilities, PascalCase for components',
|
|
411
|
+
constants: 'UPPER_SNAKE_CASE'
|
|
412
|
+
},
|
|
413
|
+
file_structure: '',
|
|
414
|
+
database: '',
|
|
415
|
+
styling: '',
|
|
416
|
+
error_handling: ''
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Detect TypeScript config
|
|
420
|
+
const hasTypeScript = configFiles.includes('tsconfig.json');
|
|
421
|
+
if (hasTypeScript) {
|
|
422
|
+
conventions.naming.types = 'PascalCase for types and interfaces';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Detect ESLint & Prettier
|
|
426
|
+
const hasEslint = configFiles.some(f => f.includes('eslint'));
|
|
427
|
+
const hasPrettier = configFiles.some(f => f.includes('prettier'));
|
|
428
|
+
const tools = [];
|
|
429
|
+
if (hasEslint) tools.push('ESLint');
|
|
430
|
+
if (hasPrettier) tools.push('Prettier');
|
|
431
|
+
if (tools.length > 0) {
|
|
432
|
+
conventions.naming.enforced_by = tools.join(', ');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Detect architecture from folder structure
|
|
436
|
+
const folderPattern = detectFolderPattern(projectRoot);
|
|
437
|
+
switch (folderPattern) {
|
|
438
|
+
case 'app-router':
|
|
439
|
+
conventions.file_structure = 'Next.js App Router - app-based routing with layouts and server components';
|
|
440
|
+
break;
|
|
441
|
+
case 'feature-based':
|
|
442
|
+
conventions.file_structure = 'Feature-based architecture - features/modules as self-contained units';
|
|
443
|
+
break;
|
|
444
|
+
case 'layer-based':
|
|
445
|
+
conventions.file_structure = 'Layered architecture (MVC/Clean) - separated controllers, services, models';
|
|
446
|
+
break;
|
|
447
|
+
case 'mixed':
|
|
448
|
+
conventions.file_structure = 'Component-based - separated components, lib, utils, pages';
|
|
449
|
+
break;
|
|
450
|
+
default:
|
|
451
|
+
conventions.file_structure = 'Flat structure';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Detect styling conventions
|
|
455
|
+
const hasTailwind = configFiles.some(f => f.includes('tailwind'));
|
|
456
|
+
if (hasTailwind) {
|
|
457
|
+
conventions.styling = 'TailwindCSS utility classes';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Detect database conventions
|
|
461
|
+
if (existsSync(join(projectRoot, 'prisma', 'schema.prisma'))) {
|
|
462
|
+
conventions.database = 'Prisma ORM - snake_case for tables/columns, PascalCase for models';
|
|
463
|
+
} else if (configFiles.some(f => f.includes('drizzle'))) {
|
|
464
|
+
conventions.database = 'Drizzle ORM';
|
|
465
|
+
} else if (existsSync(join(projectRoot, 'migrations'))) {
|
|
466
|
+
conventions.database = 'SQL migrations';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return conventions;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Analyze project structure comprehensively
|
|
474
|
+
* @param {string} projectRoot - Project root directory
|
|
475
|
+
* @returns {Object} - Analysis result
|
|
476
|
+
*/
|
|
477
|
+
function analyzeProjectStructure(projectRoot) {
|
|
478
|
+
const analysis = {
|
|
479
|
+
stack: [],
|
|
480
|
+
conventions: {},
|
|
481
|
+
shared_resources: { ui_components: [], utilities: [], database_tables: [] },
|
|
482
|
+
projectMaturity: null,
|
|
483
|
+
folderPattern: 'flat',
|
|
484
|
+
gitInfo: null,
|
|
485
|
+
configFiles: []
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Detect config files
|
|
489
|
+
for (const pattern of CONFIG_PATTERNS) {
|
|
490
|
+
const filePath = join(projectRoot, pattern);
|
|
491
|
+
if (existsSync(filePath)) {
|
|
492
|
+
analysis.configFiles.push(pattern);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Detect technologies from package.json
|
|
497
|
+
const packageJsonPath = join(projectRoot, 'package.json');
|
|
498
|
+
if (existsSync(packageJsonPath)) {
|
|
499
|
+
try {
|
|
500
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
501
|
+
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
502
|
+
|
|
503
|
+
// Check all tech categories
|
|
504
|
+
for (const [category, techs] of Object.entries(TECH_DETECTION)) {
|
|
505
|
+
if (category === 'python') continue; // Skip Python for JS projects
|
|
506
|
+
|
|
507
|
+
for (const [pkg, name] of Object.entries(techs)) {
|
|
508
|
+
if (allDeps[pkg] && !analysis.stack.includes(name)) {
|
|
509
|
+
analysis.stack.push(name);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} catch (e) { /* ignore */ }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Detect technologies from requirements.txt (Python)
|
|
517
|
+
const requirementsPath = join(projectRoot, 'requirements.txt');
|
|
518
|
+
if (existsSync(requirementsPath)) {
|
|
519
|
+
try {
|
|
520
|
+
const requirements = readFileSync(requirementsPath, 'utf-8').toLowerCase();
|
|
521
|
+
for (const [pkg, name] of Object.entries(TECH_DETECTION.python)) {
|
|
522
|
+
if (requirements.includes(pkg) && !analysis.stack.includes(name)) {
|
|
523
|
+
analysis.stack.push(name);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (e) { /* ignore */ }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Detect folder pattern
|
|
530
|
+
analysis.folderPattern = detectFolderPattern(projectRoot);
|
|
531
|
+
|
|
532
|
+
// Scan shared resources
|
|
533
|
+
analysis.shared_resources = scanSharedResources(projectRoot);
|
|
534
|
+
|
|
535
|
+
// Analyze git history
|
|
536
|
+
analysis.gitInfo = analyzeGitHistory(projectRoot);
|
|
537
|
+
if (analysis.gitInfo) {
|
|
538
|
+
analysis.projectMaturity = analysis.gitInfo.maturity;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Extract conventions
|
|
542
|
+
analysis.conventions = extractConventions(projectRoot, analysis.configFiles);
|
|
543
|
+
|
|
544
|
+
return analysis;
|
|
545
|
+
}
|
|
546
|
+
|
|
22
547
|
const program = new Command();
|
|
23
548
|
|
|
24
549
|
/**
|
|
@@ -75,7 +600,7 @@ async function initRoadmap(options) {
|
|
|
75
600
|
try {
|
|
76
601
|
// Detect environment
|
|
77
602
|
const env = detectEnvironment(projectRoot);
|
|
78
|
-
spinner.text = `Detected ${env} project...`;
|
|
603
|
+
spinner.text = `Detected ${env} project, analyzing structure...`;
|
|
79
604
|
|
|
80
605
|
// Paths
|
|
81
606
|
const roadmapPath = join(projectRoot, 'roadmap.json');
|
|
@@ -90,37 +615,45 @@ async function initRoadmap(options) {
|
|
|
90
615
|
process.exit(1);
|
|
91
616
|
}
|
|
92
617
|
|
|
618
|
+
// Perform comprehensive project analysis
|
|
619
|
+
spinner.text = 'Analyzing project structure...';
|
|
620
|
+
const analysis = analyzeProjectStructure(projectRoot);
|
|
621
|
+
|
|
93
622
|
// Load template
|
|
94
623
|
const template = JSON.parse(readFileSync(templatePath, 'utf-8'));
|
|
95
624
|
|
|
96
|
-
//
|
|
97
|
-
if (env === 'javascript') {
|
|
625
|
+
// Populate from package.json basics
|
|
626
|
+
if (env === 'javascript' && existsSync(join(projectRoot, 'package.json'))) {
|
|
98
627
|
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8'));
|
|
99
628
|
template.project_info.name = packageJson.name || 'My Project';
|
|
100
629
|
template.project_info.description = packageJson.description || '';
|
|
101
630
|
template.project_info.version = packageJson.version || '1.0.0';
|
|
102
|
-
|
|
103
|
-
// Detect common JS frameworks
|
|
104
|
-
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
105
|
-
const stack = [];
|
|
106
|
-
if (deps.react) stack.push('React');
|
|
107
|
-
if (deps.next) stack.push('Next.js');
|
|
108
|
-
if (deps.vue) stack.push('Vue');
|
|
109
|
-
if (deps.express) stack.push('Express');
|
|
110
|
-
if (deps['@nestjs/core']) stack.push('NestJS');
|
|
111
|
-
if (deps.prisma) stack.push('Prisma');
|
|
112
|
-
if (deps.typescript) stack.push('TypeScript');
|
|
113
|
-
|
|
114
|
-
template.project_info.stack = stack.length > 0 ? stack : ['JavaScript'];
|
|
115
631
|
} else if (env === 'python') {
|
|
116
|
-
template.project_info.
|
|
632
|
+
template.project_info.name = basename(projectRoot);
|
|
633
|
+
if (!analysis.stack.includes('Python')) {
|
|
634
|
+
analysis.stack.unshift('Python');
|
|
635
|
+
}
|
|
117
636
|
} else if (env === 'go') {
|
|
118
|
-
template.project_info.
|
|
637
|
+
template.project_info.name = basename(projectRoot);
|
|
638
|
+
analysis.stack.unshift('Go');
|
|
119
639
|
}
|
|
120
640
|
|
|
121
|
-
//
|
|
641
|
+
// Apply comprehensive analysis results
|
|
642
|
+
template.project_info.stack = analysis.stack.length > 0 ? analysis.stack : [env === 'javascript' ? 'JavaScript' : env];
|
|
643
|
+
template.project_info.conventions = analysis.conventions;
|
|
644
|
+
template.project_info.shared_resources = analysis.shared_resources;
|
|
122
645
|
template.project_info.last_sync = new Date().toISOString();
|
|
123
646
|
|
|
647
|
+
// Add git info if available
|
|
648
|
+
if (analysis.gitInfo) {
|
|
649
|
+
template.project_info.git_info = {
|
|
650
|
+
commit_count: analysis.gitInfo.commit_count,
|
|
651
|
+
maturity: analysis.gitInfo.maturity,
|
|
652
|
+
first_commit: analysis.gitInfo.first_commit,
|
|
653
|
+
last_commit: analysis.gitInfo.last_commit
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
124
657
|
// Save roadmap.json
|
|
125
658
|
writeFileSync(roadmapPath, JSON.stringify(template, null, 2), 'utf-8');
|
|
126
659
|
|
|
@@ -131,11 +664,27 @@ async function initRoadmap(options) {
|
|
|
131
664
|
|
|
132
665
|
spinner.succeed('Roadmap initialized successfully');
|
|
133
666
|
|
|
667
|
+
// Display analysis summary
|
|
668
|
+
console.log(chalk.cyan('\nš Project Analysis:'));
|
|
669
|
+
console.log(chalk.white(` ⢠Stack: ${analysis.stack.length > 0 ? analysis.stack.join(', ') : 'Not detected'}`));
|
|
670
|
+
console.log(chalk.white(` ⢠Structure: ${analysis.folderPattern}`));
|
|
671
|
+
|
|
672
|
+
const uiCount = analysis.shared_resources.ui_components.length;
|
|
673
|
+
const utilCount = analysis.shared_resources.utilities.length;
|
|
674
|
+
const dbCount = analysis.shared_resources.database_tables.length;
|
|
675
|
+
|
|
676
|
+
if (uiCount > 0) console.log(chalk.white(` ⢠UI Components: ${uiCount} detected`));
|
|
677
|
+
if (utilCount > 0) console.log(chalk.white(` ⢠Utilities: ${utilCount} detected`));
|
|
678
|
+
if (dbCount > 0) console.log(chalk.white(` ⢠Database tables: ${dbCount} detected`));
|
|
679
|
+
|
|
680
|
+
if (analysis.gitInfo) {
|
|
681
|
+
console.log(chalk.white(` ⢠Git history: ${analysis.gitInfo.commit_count} commits (${analysis.gitInfo.maturity})`));
|
|
682
|
+
}
|
|
683
|
+
|
|
134
684
|
console.log(chalk.cyan('\nš Next steps:'));
|
|
135
|
-
console.log(chalk.white(' 1.
|
|
685
|
+
console.log(chalk.white(' 1. Review roadmap.json and add your features'));
|
|
136
686
|
console.log(chalk.white(` 2. Edit ${getAIRulesFilename(projectRoot)} to customize AI rules`));
|
|
137
|
-
console.log(chalk.white(' 3. Run "roadmap-kit
|
|
138
|
-
console.log(chalk.white(' 4. Run "roadmap-kit dashboard" to view progress'));
|
|
687
|
+
console.log(chalk.white(' 3. Run "roadmap-kit dashboard" to view and manage'));
|
|
139
688
|
|
|
140
689
|
} catch (error) {
|
|
141
690
|
spinner.fail('Error initializing roadmap');
|
|
@@ -149,6 +698,7 @@ async function initRoadmap(options) {
|
|
|
149
698
|
*/
|
|
150
699
|
async function openDashboard(options) {
|
|
151
700
|
const projectRoot = options.path || process.cwd();
|
|
701
|
+
const requestedPort = options.port ? parseInt(options.port, 10) : null;
|
|
152
702
|
|
|
153
703
|
// Check for roadmap.json in different locations
|
|
154
704
|
let roadmapPath = join(projectRoot, 'roadmap.json');
|
|
@@ -173,6 +723,36 @@ async function openDashboard(options) {
|
|
|
173
723
|
const spinner = ora('Starting dashboard...').start();
|
|
174
724
|
|
|
175
725
|
try {
|
|
726
|
+
// Determine port to use
|
|
727
|
+
let port;
|
|
728
|
+
let portNote = '';
|
|
729
|
+
|
|
730
|
+
if (requestedPort) {
|
|
731
|
+
// User specified a port
|
|
732
|
+
if (await isPortAvailable(requestedPort)) {
|
|
733
|
+
port = requestedPort;
|
|
734
|
+
} else {
|
|
735
|
+
spinner.fail(`Port ${requestedPort} is already in use`);
|
|
736
|
+
console.log(chalk.yellow(' Try a different port with --port <port>'));
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
// Auto-detect available port
|
|
741
|
+
if (await isPortAvailable(DEFAULT_PORT)) {
|
|
742
|
+
port = DEFAULT_PORT;
|
|
743
|
+
} else {
|
|
744
|
+
spinner.text = 'Default port in use, finding available port...';
|
|
745
|
+
try {
|
|
746
|
+
port = await findAvailablePort(DEFAULT_PORT);
|
|
747
|
+
portNote = ` ${chalk.yellow('Note:')} Port ${DEFAULT_PORT} in use, using port ${port} instead\n`;
|
|
748
|
+
} catch (err) {
|
|
749
|
+
spinner.fail('No available port found');
|
|
750
|
+
console.log(chalk.yellow(` Ports ${DEFAULT_PORT}-${DEFAULT_PORT + 9} are all in use`));
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
176
756
|
// Check if dashboard deps are installed
|
|
177
757
|
if (!existsSync(join(dashboardPath, 'node_modules'))) {
|
|
178
758
|
spinner.text = 'Installing dashboard dependencies (first time)...';
|
|
@@ -180,7 +760,10 @@ async function openDashboard(options) {
|
|
|
180
760
|
}
|
|
181
761
|
|
|
182
762
|
spinner.succeed('Dashboard starting...');
|
|
183
|
-
|
|
763
|
+
if (portNote) {
|
|
764
|
+
console.log(portNote);
|
|
765
|
+
}
|
|
766
|
+
console.log(chalk.green(`\nā Dashboard running at http://localhost:${port}`));
|
|
184
767
|
console.log(chalk.cyan(` š Roadmap: ${roadmapPath}`));
|
|
185
768
|
console.log(chalk.gray(' Press Ctrl+C to stop\n'));
|
|
186
769
|
|
|
@@ -188,7 +771,8 @@ async function openDashboard(options) {
|
|
|
188
771
|
const serverProcess = spawn('npm', ['run', 'dev'], {
|
|
189
772
|
cwd: dashboardPath,
|
|
190
773
|
stdio: 'inherit',
|
|
191
|
-
shell: true
|
|
774
|
+
shell: true,
|
|
775
|
+
env: { ...process.env, PROJECT_ROOT: projectRoot, PORT: port.toString() }
|
|
192
776
|
});
|
|
193
777
|
|
|
194
778
|
serverProcess.on('error', (error) => {
|
|
@@ -506,6 +1090,7 @@ program
|
|
|
506
1090
|
.command('dashboard')
|
|
507
1091
|
.description('Open roadmap dashboard')
|
|
508
1092
|
.option('-p, --path <path>', 'Project path', process.cwd())
|
|
1093
|
+
.option('--port <port>', 'Dashboard port (default: 6969, auto-detects if in use)')
|
|
509
1094
|
.action(openDashboard);
|
|
510
1095
|
|
|
511
1096
|
// Docker command
|