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 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
- // Customize based on environment
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.stack = ['Python'];
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.stack = ['Go'];
637
+ template.project_info.name = basename(projectRoot);
638
+ analysis.stack.unshift('Go');
119
639
  }
120
640
 
121
- // Set initial timestamp
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. Edit roadmap.json to add your features and tasks'));
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 scan" to sync with Git'));
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
- console.log(chalk.green('\nāœ“ Dashboard running at http://localhost:6969'));
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