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,142 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
export default {
|
|
3
|
+
content: [
|
|
4
|
+
"./index.html",
|
|
5
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
6
|
+
],
|
|
7
|
+
darkMode: 'class',
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
fontFamily: {
|
|
11
|
+
display: ['Orbitron', 'system-ui', 'sans-serif'],
|
|
12
|
+
sans: ['Space Grotesk', 'system-ui', 'sans-serif'],
|
|
13
|
+
mono: ['IBM Plex Mono', 'monospace'],
|
|
14
|
+
},
|
|
15
|
+
colors: {
|
|
16
|
+
// Pure black base
|
|
17
|
+
void: {
|
|
18
|
+
DEFAULT: '#000000',
|
|
19
|
+
50: '#0a0a0a',
|
|
20
|
+
100: '#0d0d0d',
|
|
21
|
+
200: '#121212',
|
|
22
|
+
300: '#1a1a1a',
|
|
23
|
+
400: '#222222',
|
|
24
|
+
},
|
|
25
|
+
// Electric terminal green
|
|
26
|
+
matrix: {
|
|
27
|
+
DEFAULT: '#00ff88',
|
|
28
|
+
dim: '#00cc6a',
|
|
29
|
+
bright: '#33ffaa',
|
|
30
|
+
glow: 'rgba(0, 255, 136, 0.5)',
|
|
31
|
+
},
|
|
32
|
+
// Amber warning/active
|
|
33
|
+
signal: {
|
|
34
|
+
DEFAULT: '#ff9500',
|
|
35
|
+
dim: '#cc7700',
|
|
36
|
+
bright: '#ffaa33',
|
|
37
|
+
glow: 'rgba(255, 149, 0, 0.5)',
|
|
38
|
+
},
|
|
39
|
+
// Cyan interactive
|
|
40
|
+
cyber: {
|
|
41
|
+
DEFAULT: '#00d4ff',
|
|
42
|
+
dim: '#00a8cc',
|
|
43
|
+
bright: '#33ddff',
|
|
44
|
+
glow: 'rgba(0, 212, 255, 0.5)',
|
|
45
|
+
},
|
|
46
|
+
// Rose danger
|
|
47
|
+
alert: {
|
|
48
|
+
DEFAULT: '#ff3366',
|
|
49
|
+
dim: '#cc2952',
|
|
50
|
+
bright: '#ff5580',
|
|
51
|
+
glow: 'rgba(255, 51, 102, 0.5)',
|
|
52
|
+
},
|
|
53
|
+
// Status semantic colors
|
|
54
|
+
status: {
|
|
55
|
+
completed: '#00ff88',
|
|
56
|
+
progress: '#ff9500',
|
|
57
|
+
pending: '#555555'
|
|
58
|
+
},
|
|
59
|
+
severity: {
|
|
60
|
+
high: '#ff3366',
|
|
61
|
+
medium: '#ff9500',
|
|
62
|
+
low: '#00d4ff'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
boxShadow: {
|
|
66
|
+
'matrix': '0 0 20px rgba(0, 255, 136, 0.3), 0 0 40px rgba(0, 255, 136, 0.1)',
|
|
67
|
+
'signal': '0 0 20px rgba(255, 149, 0, 0.3), 0 0 40px rgba(255, 149, 0, 0.1)',
|
|
68
|
+
'cyber': '0 0 20px rgba(0, 212, 255, 0.3), 0 0 40px rgba(0, 212, 255, 0.1)',
|
|
69
|
+
'alert': '0 0 20px rgba(255, 51, 102, 0.3), 0 0 40px rgba(255, 51, 102, 0.1)',
|
|
70
|
+
'inner-glow': 'inset 0 0 30px rgba(0, 255, 136, 0.05)',
|
|
71
|
+
},
|
|
72
|
+
keyframes: {
|
|
73
|
+
"scan": {
|
|
74
|
+
"0%": { transform: "translateY(-100%)" },
|
|
75
|
+
"100%": { transform: "translateY(100vh)" },
|
|
76
|
+
},
|
|
77
|
+
"pulse-glow": {
|
|
78
|
+
"0%, 100%": { opacity: "0.5" },
|
|
79
|
+
"50%": { opacity: "1" },
|
|
80
|
+
},
|
|
81
|
+
"flicker": {
|
|
82
|
+
"0%, 100%": { opacity: "1" },
|
|
83
|
+
"50%": { opacity: "0.95" },
|
|
84
|
+
"52%": { opacity: "1" },
|
|
85
|
+
"54%": { opacity: "0.9" },
|
|
86
|
+
"56%": { opacity: "1" },
|
|
87
|
+
},
|
|
88
|
+
"typing": {
|
|
89
|
+
"from": { width: "0" },
|
|
90
|
+
"to": { width: "100%" },
|
|
91
|
+
},
|
|
92
|
+
"blink": {
|
|
93
|
+
"0%, 100%": { opacity: "1" },
|
|
94
|
+
"50%": { opacity: "0" },
|
|
95
|
+
},
|
|
96
|
+
"slide-up": {
|
|
97
|
+
"from": { opacity: "0", transform: "translateY(20px)" },
|
|
98
|
+
"to": { opacity: "1", transform: "translateY(0)" },
|
|
99
|
+
},
|
|
100
|
+
"slide-in-left": {
|
|
101
|
+
"from": { opacity: "0", transform: "translateX(-20px)" },
|
|
102
|
+
"to": { opacity: "1", transform: "translateX(0)" },
|
|
103
|
+
},
|
|
104
|
+
"slide-left": {
|
|
105
|
+
"from": { opacity: "0", transform: "translateX(100%)" },
|
|
106
|
+
"to": { opacity: "1", transform: "translateX(0)" },
|
|
107
|
+
},
|
|
108
|
+
"fade-in": {
|
|
109
|
+
"from": { opacity: "0" },
|
|
110
|
+
"to": { opacity: "1" },
|
|
111
|
+
},
|
|
112
|
+
"progress-fill": {
|
|
113
|
+
"from": { width: "0" },
|
|
114
|
+
"to": { width: "var(--progress-width)" },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
animation: {
|
|
118
|
+
"scan": "scan 8s linear infinite",
|
|
119
|
+
"pulse-glow": "pulse-glow 2s ease-in-out infinite",
|
|
120
|
+
"flicker": "flicker 0.15s ease-in-out",
|
|
121
|
+
"typing": "typing 2s steps(30) forwards",
|
|
122
|
+
"blink": "blink 1s step-end infinite",
|
|
123
|
+
"slide-up": "slide-up 0.4s ease-out forwards",
|
|
124
|
+
"slide-in-left": "slide-in-left 0.3s ease-out forwards",
|
|
125
|
+
"slide-left": "slide-left 0.3s ease-out forwards",
|
|
126
|
+
"fade-in": "fade-in 0.3s ease-out forwards",
|
|
127
|
+
"progress-fill": "progress-fill 1s ease-out forwards",
|
|
128
|
+
},
|
|
129
|
+
backgroundImage: {
|
|
130
|
+
'grid-pattern': `
|
|
131
|
+
linear-gradient(rgba(0, 255, 136, 0.03) 1px, transparent 1px),
|
|
132
|
+
linear-gradient(90deg, rgba(0, 255, 136, 0.03) 1px, transparent 1px)
|
|
133
|
+
`,
|
|
134
|
+
'scanline': 'repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0, 0, 0, 0.1) 2px, rgba(0, 0, 0, 0.1) 4px)',
|
|
135
|
+
},
|
|
136
|
+
backgroundSize: {
|
|
137
|
+
'grid': '40px 40px',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
plugins: [],
|
|
142
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
|
|
4
|
+
// https://vitejs.dev/config/
|
|
5
|
+
//
|
|
6
|
+
// Environment variables:
|
|
7
|
+
// PORT - Server port (default: 6969)
|
|
8
|
+
// BASE_PATH - Base path for subpath deployment (default: '/')
|
|
9
|
+
// Example: BASE_PATH=/roadmap npm run build
|
|
10
|
+
//
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
plugins: [react()],
|
|
13
|
+
base: process.env.BASE_PATH || '/',
|
|
14
|
+
server: {
|
|
15
|
+
port: parseInt(process.env.PORT) || 6969,
|
|
16
|
+
open: true
|
|
17
|
+
}
|
|
18
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ROADMAP-KIT Dockerfile
|
|
2
|
+
# Lightweight Node.js image for running the dashboard
|
|
3
|
+
|
|
4
|
+
FROM node:20-alpine
|
|
5
|
+
|
|
6
|
+
# Install git (required for scanner)
|
|
7
|
+
RUN apk add --no-cache git
|
|
8
|
+
|
|
9
|
+
# Set working directory
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
|
|
12
|
+
# Copy roadmap-kit files
|
|
13
|
+
COPY package.json ./
|
|
14
|
+
COPY scanner.js ./
|
|
15
|
+
COPY cli.js ./
|
|
16
|
+
COPY dashboard ./dashboard
|
|
17
|
+
|
|
18
|
+
# Install dependencies
|
|
19
|
+
RUN npm install
|
|
20
|
+
|
|
21
|
+
# Install dashboard dependencies
|
|
22
|
+
WORKDIR /app/dashboard
|
|
23
|
+
RUN npm install
|
|
24
|
+
|
|
25
|
+
# Back to root
|
|
26
|
+
WORKDIR /app
|
|
27
|
+
|
|
28
|
+
# Expose port
|
|
29
|
+
EXPOSE 3001
|
|
30
|
+
|
|
31
|
+
# Set entrypoint
|
|
32
|
+
COPY docker/entrypoint.sh /entrypoint.sh
|
|
33
|
+
RUN chmod +x /entrypoint.sh
|
|
34
|
+
|
|
35
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# ROADMAP-KIT Docker Compose Configuration
|
|
2
|
+
# Add this to your existing docker-compose.yml or use standalone
|
|
3
|
+
|
|
4
|
+
version: '3.8'
|
|
5
|
+
|
|
6
|
+
services:
|
|
7
|
+
roadmap-dashboard:
|
|
8
|
+
build:
|
|
9
|
+
context: ../
|
|
10
|
+
dockerfile: docker/Dockerfile
|
|
11
|
+
container_name: roadmap-dashboard
|
|
12
|
+
ports:
|
|
13
|
+
- "${ROADMAP_PORT:-6969}:6969"
|
|
14
|
+
environment:
|
|
15
|
+
- PORT=6969
|
|
16
|
+
volumes:
|
|
17
|
+
# Mount .git directory (read-only) for scanner
|
|
18
|
+
- ../.git:/app/.git:ro
|
|
19
|
+
# Mount roadmap.json for sync
|
|
20
|
+
- ../roadmap.json:/app/roadmap.json
|
|
21
|
+
# Mount project root for file access
|
|
22
|
+
- ..:/project:ro
|
|
23
|
+
restart: unless-stopped
|
|
24
|
+
- NODE_ENV=development
|
|
25
|
+
networks:
|
|
26
|
+
- roadmap-network
|
|
27
|
+
|
|
28
|
+
networks:
|
|
29
|
+
roadmap-network:
|
|
30
|
+
driver: bridge
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
# ROADMAP-KIT Entrypoint
|
|
4
|
+
# Runs scanner and starts Vite dashboard
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "šŗļø ROADMAP-KIT Docker Container"
|
|
9
|
+
echo "================================="
|
|
10
|
+
|
|
11
|
+
# Check if roadmap.json exists
|
|
12
|
+
if [ ! -f /app/roadmap.json ]; then
|
|
13
|
+
echo "ā ļø roadmap.json not found"
|
|
14
|
+
echo "Creating from template..."
|
|
15
|
+
cp /app/templates/roadmap.template.json /app/roadmap.json
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Run scanner to update roadmap
|
|
19
|
+
echo "š Scanning Git history..."
|
|
20
|
+
cd /app
|
|
21
|
+
node scanner.js || echo "ā ļø Scanner failed (might not be a git repo)"
|
|
22
|
+
|
|
23
|
+
# Copy roadmap.json to dashboard public folder
|
|
24
|
+
echo "š Copying roadmap to dashboard..."
|
|
25
|
+
mkdir -p /app/dashboard/public
|
|
26
|
+
cp /app/roadmap.json /app/dashboard/public/roadmap.json
|
|
27
|
+
|
|
28
|
+
# Start Vite dev server
|
|
29
|
+
echo "š Starting dashboard on http://localhost:3001"
|
|
30
|
+
cd /app/dashboard
|
|
31
|
+
exec npm run dev -- --host 0.0.0.0
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "roadmap-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered project management for Vibe Coding. Track features, tasks, and technical debt with a visual dashboard.",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"roadmap-kit": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"scanner.js",
|
|
13
|
+
"setup.sh",
|
|
14
|
+
"dashboard/",
|
|
15
|
+
"templates/",
|
|
16
|
+
"docker/",
|
|
17
|
+
"!dashboard/node_modules",
|
|
18
|
+
"README.md",
|
|
19
|
+
"INSTALL.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"scan": "node scanner.js",
|
|
24
|
+
"dev": "cd dashboard && npm run dev",
|
|
25
|
+
"dev:scan": "node scanner.js && cd dashboard && npm run dev",
|
|
26
|
+
"build": "cd dashboard && npm run build",
|
|
27
|
+
"preview": "cd dashboard && npm run preview",
|
|
28
|
+
"postinstall": "echo '\\nšŗļø ROADMAP-KIT installed! Run: npx roadmap-kit init\\n'"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"ai",
|
|
32
|
+
"vibe-coding",
|
|
33
|
+
"roadmap",
|
|
34
|
+
"project-management",
|
|
35
|
+
"claude",
|
|
36
|
+
"cursor",
|
|
37
|
+
"cline",
|
|
38
|
+
"windsurf",
|
|
39
|
+
"copilot",
|
|
40
|
+
"ai-coding",
|
|
41
|
+
"developer-tools",
|
|
42
|
+
"task-tracking",
|
|
43
|
+
"technical-debt"
|
|
44
|
+
],
|
|
45
|
+
"author": {
|
|
46
|
+
"name": "HACKLET",
|
|
47
|
+
"url": "https://hacklet.es"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"homepage": "https://roadmap.ink",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/hacklet1101/roadmap-kit.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/hacklet1101/roadmap-kit/issues"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"simple-git": "^3.22.0",
|
|
60
|
+
"chalk": "^5.3.0",
|
|
61
|
+
"commander": "^12.0.0",
|
|
62
|
+
"ora": "^8.0.1"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/scanner.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ROADMAP-KIT SCANNER
|
|
5
|
+
* Reads Git commits and updates roadmap.json automatically
|
|
6
|
+
* Parses tags: [task:id], [status:value], [debt:description]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { simpleGit } from 'simple-git';
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import ora from 'ora';
|
|
15
|
+
|
|
16
|
+
// Get current directory
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// Configuration
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
maxCommits: 50,
|
|
23
|
+
roadmapPath: null, // Will be set based on execution context
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse commit message for tags
|
|
28
|
+
* Tags format: [task:id] [status:value] [debt:description]
|
|
29
|
+
*/
|
|
30
|
+
function parseCommitTags(message) {
|
|
31
|
+
const tags = {
|
|
32
|
+
taskId: null,
|
|
33
|
+
status: null,
|
|
34
|
+
debts: []
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Extract [task:id]
|
|
38
|
+
const taskMatch = message.match(/\[task:([^\]]+)\]/);
|
|
39
|
+
if (taskMatch) {
|
|
40
|
+
tags.taskId = taskMatch[1];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Extract [status:value]
|
|
44
|
+
const statusMatch = message.match(/\[status:([^\]]+)\]/);
|
|
45
|
+
if (statusMatch) {
|
|
46
|
+
const status = statusMatch[1];
|
|
47
|
+
if (['pending', 'in_progress', 'completed'].includes(status)) {
|
|
48
|
+
tags.status = status;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract all [debt:description]
|
|
53
|
+
const debtMatches = message.matchAll(/\[debt:([^\]]+)\]/g);
|
|
54
|
+
for (const match of debtMatches) {
|
|
55
|
+
tags.debts.push(match[1]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return tags;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get file statistics from commit
|
|
63
|
+
*/
|
|
64
|
+
async function getCommitStats(git, commitHash) {
|
|
65
|
+
try {
|
|
66
|
+
const diffSummary = await git.diffSummary([`${commitHash}^`, commitHash]);
|
|
67
|
+
return {
|
|
68
|
+
lines_added: diffSummary.insertions || 0,
|
|
69
|
+
lines_removed: diffSummary.deletions || 0,
|
|
70
|
+
files_created: diffSummary.files.filter(f => f.binary === false && f.deletions === 0).length,
|
|
71
|
+
files_modified: diffSummary.files.filter(f => f.binary === false && f.deletions > 0).length,
|
|
72
|
+
files: diffSummary.files.map(f => f.file)
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// If it's the first commit, there's no parent to diff against
|
|
76
|
+
return {
|
|
77
|
+
lines_added: 0,
|
|
78
|
+
lines_removed: 0,
|
|
79
|
+
files_created: 0,
|
|
80
|
+
files_modified: 0,
|
|
81
|
+
files: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Calculate complexity score based on lines changed
|
|
88
|
+
*/
|
|
89
|
+
function calculateComplexity(linesAdded, linesRemoved) {
|
|
90
|
+
const totalLines = linesAdded + linesRemoved;
|
|
91
|
+
if (totalLines < 50) return 1;
|
|
92
|
+
if (totalLines < 100) return 2;
|
|
93
|
+
if (totalLines < 200) return 3;
|
|
94
|
+
if (totalLines < 500) return 5;
|
|
95
|
+
if (totalLines < 1000) return 7;
|
|
96
|
+
return 10;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Find task in roadmap by ID
|
|
101
|
+
*/
|
|
102
|
+
function findTask(roadmap, taskId) {
|
|
103
|
+
for (const feature of roadmap.features) {
|
|
104
|
+
for (const task of feature.tasks) {
|
|
105
|
+
if (task.id === taskId) {
|
|
106
|
+
return { feature, task };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Update task with commit information
|
|
115
|
+
*/
|
|
116
|
+
function updateTask(task, commit, tags, stats) {
|
|
117
|
+
// Update status
|
|
118
|
+
if (tags.status) {
|
|
119
|
+
task.status = tags.status;
|
|
120
|
+
|
|
121
|
+
// Set timestamps
|
|
122
|
+
if (tags.status === 'in_progress' && !task.started_at) {
|
|
123
|
+
task.started_at = commit.date;
|
|
124
|
+
}
|
|
125
|
+
if (tags.status === 'completed' && !task.completed_at) {
|
|
126
|
+
task.completed_at = commit.date;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Update git information
|
|
131
|
+
if (!task.git) {
|
|
132
|
+
task.git = {
|
|
133
|
+
branch: null,
|
|
134
|
+
pr_number: null,
|
|
135
|
+
pr_url: null,
|
|
136
|
+
last_commit: null,
|
|
137
|
+
commits: []
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
task.git.last_commit = commit.hash;
|
|
141
|
+
if (!task.git.commits.includes(commit.hash)) {
|
|
142
|
+
task.git.commits.push(commit.hash);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update affected files
|
|
146
|
+
if (stats.files && stats.files.length > 0) {
|
|
147
|
+
task.affected_files = task.affected_files || [];
|
|
148
|
+
stats.files.forEach(file => {
|
|
149
|
+
if (!task.affected_files.includes(file)) {
|
|
150
|
+
task.affected_files.push(file);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Update metrics
|
|
156
|
+
if (!task.metrics) {
|
|
157
|
+
task.metrics = {
|
|
158
|
+
lines_added: 0,
|
|
159
|
+
lines_removed: 0,
|
|
160
|
+
files_created: 0,
|
|
161
|
+
files_modified: 0,
|
|
162
|
+
complexity_score: 0
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
task.metrics.lines_added += stats.lines_added;
|
|
166
|
+
task.metrics.lines_removed += stats.lines_removed;
|
|
167
|
+
task.metrics.files_created += stats.files_created;
|
|
168
|
+
task.metrics.files_modified += stats.files_modified;
|
|
169
|
+
task.metrics.complexity_score = calculateComplexity(
|
|
170
|
+
task.metrics.lines_added,
|
|
171
|
+
task.metrics.lines_removed
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Add technical debt
|
|
175
|
+
if (tags.debts.length > 0) {
|
|
176
|
+
task.technical_debt = task.technical_debt || [];
|
|
177
|
+
tags.debts.forEach(debtDesc => {
|
|
178
|
+
task.technical_debt.push({
|
|
179
|
+
description: debtDesc,
|
|
180
|
+
severity: 'medium', // Default severity
|
|
181
|
+
estimated_effort: 'TBD'
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return task;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Calculate feature progress
|
|
191
|
+
*/
|
|
192
|
+
function calculateFeatureProgress(feature) {
|
|
193
|
+
if (!feature.tasks || feature.tasks.length === 0) return 0;
|
|
194
|
+
|
|
195
|
+
const completedTasks = feature.tasks.filter(t => t.status === 'completed').length;
|
|
196
|
+
return Math.round((completedTasks / feature.tasks.length) * 100);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Calculate total project progress
|
|
201
|
+
*/
|
|
202
|
+
function calculateTotalProgress(roadmap) {
|
|
203
|
+
if (!roadmap.features || roadmap.features.length === 0) return 0;
|
|
204
|
+
|
|
205
|
+
const totalProgress = roadmap.features.reduce((sum, feature) => {
|
|
206
|
+
return sum + (feature.progress || 0);
|
|
207
|
+
}, 0);
|
|
208
|
+
|
|
209
|
+
return Math.round(totalProgress / roadmap.features.length);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Load roadmap.json
|
|
214
|
+
*/
|
|
215
|
+
function loadRoadmap(roadmapPath) {
|
|
216
|
+
if (!existsSync(roadmapPath)) {
|
|
217
|
+
console.error(chalk.red('ā roadmap.json not found'));
|
|
218
|
+
console.log(chalk.yellow(' Run "roadmap-kit init" to create one'));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const content = readFileSync(roadmapPath, 'utf-8');
|
|
224
|
+
return JSON.parse(content);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(chalk.red('ā Error reading roadmap.json:'), error.message);
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Save roadmap.json
|
|
233
|
+
*/
|
|
234
|
+
function saveRoadmap(roadmapPath, roadmap) {
|
|
235
|
+
try {
|
|
236
|
+
writeFileSync(roadmapPath, JSON.stringify(roadmap, null, 2), 'utf-8');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error(chalk.red('ā Error saving roadmap.json:'), error.message);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Main scanner function
|
|
245
|
+
*/
|
|
246
|
+
export async function scanGitHistory(projectRoot = process.cwd()) {
|
|
247
|
+
const roadmapPath = join(projectRoot, 'roadmap.json');
|
|
248
|
+
const spinner = ora('Scanning Git history...').start();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Initialize git
|
|
252
|
+
const git = simpleGit(projectRoot);
|
|
253
|
+
|
|
254
|
+
// Check if it's a git repository
|
|
255
|
+
const isRepo = await git.checkIsRepo();
|
|
256
|
+
if (!isRepo) {
|
|
257
|
+
spinner.fail('Not a Git repository');
|
|
258
|
+
console.log(chalk.yellow(' Initialize Git first: git init'));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Load roadmap
|
|
263
|
+
const roadmap = loadRoadmap(roadmapPath);
|
|
264
|
+
|
|
265
|
+
// Get last sync date
|
|
266
|
+
const lastSync = roadmap.project_info.last_sync
|
|
267
|
+
? new Date(roadmap.project_info.last_sync)
|
|
268
|
+
: null;
|
|
269
|
+
|
|
270
|
+
// Get commits
|
|
271
|
+
const log = await git.log({ maxCount: CONFIG.maxCommits });
|
|
272
|
+
|
|
273
|
+
let updatedTasks = 0;
|
|
274
|
+
let newDebts = 0;
|
|
275
|
+
let processedCommits = 0;
|
|
276
|
+
|
|
277
|
+
// Process commits from newest to oldest
|
|
278
|
+
for (const commit of log.all) {
|
|
279
|
+
const commitDate = new Date(commit.date);
|
|
280
|
+
|
|
281
|
+
// Skip commits before last sync (if any)
|
|
282
|
+
if (lastSync && commitDate <= lastSync) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
processedCommits++;
|
|
287
|
+
const tags = parseCommitTags(commit.message);
|
|
288
|
+
|
|
289
|
+
// If no task tag, skip
|
|
290
|
+
if (!tags.taskId) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Find task in roadmap
|
|
295
|
+
const result = findTask(roadmap, tags.taskId);
|
|
296
|
+
if (!result) {
|
|
297
|
+
console.log(chalk.yellow(`\n ā Task "${tags.taskId}" not found in roadmap`));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Get commit stats
|
|
302
|
+
const stats = await getCommitStats(git, commit.hash);
|
|
303
|
+
|
|
304
|
+
// Update task
|
|
305
|
+
updateTask(result.task, commit, tags, stats);
|
|
306
|
+
updatedTasks++;
|
|
307
|
+
|
|
308
|
+
if (tags.debts.length > 0) {
|
|
309
|
+
newDebts += tags.debts.length;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
spinner.text = `Processing commits... (${processedCommits} commits, ${updatedTasks} tasks updated)`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Recalculate progress
|
|
316
|
+
roadmap.features.forEach(feature => {
|
|
317
|
+
feature.progress = calculateFeatureProgress(feature);
|
|
318
|
+
});
|
|
319
|
+
roadmap.project_info.total_progress = calculateTotalProgress(roadmap);
|
|
320
|
+
roadmap.project_info.last_sync = new Date().toISOString();
|
|
321
|
+
|
|
322
|
+
// Save updated roadmap
|
|
323
|
+
saveRoadmap(roadmapPath, roadmap);
|
|
324
|
+
|
|
325
|
+
spinner.succeed('Roadmap updated successfully');
|
|
326
|
+
|
|
327
|
+
// Print summary
|
|
328
|
+
console.log(chalk.cyan('\nš Summary:'));
|
|
329
|
+
console.log(chalk.white(` ⢠Processed commits: ${processedCommits}`));
|
|
330
|
+
console.log(chalk.white(` ⢠Updated tasks: ${updatedTasks}`));
|
|
331
|
+
console.log(chalk.white(` ⢠New technical debts: ${newDebts}`));
|
|
332
|
+
console.log(chalk.green(` ⢠Total progress: ${roadmap.project_info.total_progress}%`));
|
|
333
|
+
|
|
334
|
+
if (updatedTasks === 0 && processedCommits > 0) {
|
|
335
|
+
console.log(chalk.yellow('\nš” Tip: Use commit tags like [task:id] [status:completed] to track tasks'));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
} catch (error) {
|
|
339
|
+
spinner.fail('Error scanning Git history');
|
|
340
|
+
console.error(chalk.red(error.message));
|
|
341
|
+
if (error.stack) {
|
|
342
|
+
console.error(chalk.gray(error.stack));
|
|
343
|
+
}
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Run scanner if executed directly
|
|
349
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
350
|
+
scanGitHistory();
|
|
351
|
+
}
|