From 1d7c5410c6d9f50c105f27f2f6b47892402218bd Mon Sep 17 00:00:00 2001 From: Janvi Date: Thu, 8 Jan 2026 21:48:57 +0530 Subject: [PATCH 1/3] Add AI recommendation feature using GitHub username input --- client/src/App.tsx | 2 + .../navbar/components/recommendation.tsx | 34 ++++++ .../navbar/components/render-mobile-menu.tsx | 16 ++- server/gemini.js | 38 ++++++ server/index2.js | 17 +++ server/package-lock.json | 2 +- server/package.json | 2 +- server/src/config/env.js | 3 + server/src/services/corelogic.js | 47 ++++++++ server/src/services/featureDetector.js | 27 +++++ server/src/services/featureRules.js | 112 ++++++++++++++++++ server/src/services/getauthenticated.js | 19 +++ server/src/services/getfiles.js | 4 + server/src/services/getlanguages.js | 5 + server/src/services/getprs.js | 9 ++ server/src/services/getrepos.js | 11 ++ server/src/services/getresolutiontime.js | 12 ++ 17 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 client/src/shared/components/organisms/navbar/components/recommendation.tsx create mode 100644 server/gemini.js create mode 100644 server/index2.js create mode 100644 server/src/services/corelogic.js create mode 100644 server/src/services/featureDetector.js create mode 100644 server/src/services/featureRules.js create mode 100644 server/src/services/getauthenticated.js create mode 100644 server/src/services/getfiles.js create mode 100644 server/src/services/getlanguages.js create mode 100644 server/src/services/getprs.js create mode 100644 server/src/services/getrepos.js create mode 100644 server/src/services/getresolutiontime.js diff --git a/client/src/App.tsx b/client/src/App.tsx index b82e81a1..83c3b33a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,7 @@ import Search from './modules/search'; import Profile from './modules/profile'; import Project from './modules/project'; import Sidebar from './shared/components/organisms/sidebar'; +import Recommendation from './shared/components/organisms/navbar/components/recommendation'; // import ChangePassword from "./modules/change-password"; // import ManageProjects from "./modules/manage-projects"; // import EditProfile from "./modules/edit-profile"; @@ -30,6 +31,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/shared/components/organisms/navbar/components/recommendation.tsx b/client/src/shared/components/organisms/navbar/components/recommendation.tsx new file mode 100644 index 00000000..4b9a1e60 --- /dev/null +++ b/client/src/shared/components/organisms/navbar/components/recommendation.tsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; + +export default function Recommendation() { + const [result, setResult] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchRecommendation = async () => { + try { + const res = await fetch("http://localhost:5000/api/recommend", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ platform: "facebook" }) + }); + + const data = await res.json(); + setResult(data.recommendation); + } catch (err) { + setResult("Error getting recommendation"); + } finally { + setLoading(false); + } + }; + + fetchRecommendation(); + }, []); + + return ( +
+ {loading &&

Generating AI recommendation...

} + {!loading &&
{result}
} +
+ ); +} diff --git a/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx b/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx index c39c905e..54398f79 100644 --- a/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx +++ b/client/src/shared/components/organisms/navbar/components/render-mobile-menu.tsx @@ -4,6 +4,7 @@ import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import CreateIcon from '@mui/icons-material/Create'; import LoginIcon from '@mui/icons-material/Login'; +import AssistantIcon from '@mui/icons-material/Assistant'; import { menuId, mobileMenuId } from '../constants'; import A2ZIconButton from '../../../atoms/icon-button'; import A2ZTypography from '../../../atoms/typography'; @@ -81,7 +82,20 @@ const RenderMobileMenu: FC = ({ - + { + navigate('/recommendation'); + handleMobileMenuClose(); + }} + > + + + + + + + {isAuthenticated() ? [ a + b.mergetimehours, 0) / + prMetrics.length + : null, + avgIssueResolutionTime: + issueMetrics.length + ? issueMetrics.reduce((a, b) => a + b.resolutionHours, 0) / + issueMetrics.length + : null + }); + } + + return analysisResults; +} diff --git a/server/src/services/featureDetector.js b/server/src/services/featureDetector.js new file mode 100644 index 00000000..09d200f7 --- /dev/null +++ b/server/src/services/featureDetector.js @@ -0,0 +1,27 @@ +import { FEATURE_RULES } from "./featureRules.js"; + +export function detectCapabilities(files, deps = []) { + const detected = new Set(); + + for (const rule of FEATURE_RULES) { + let matched = false; + + if (rule.signals.files) { + matched ||= files.some(f => rule.signals.files.includes(f.name)); + } + + if (rule.signals.paths) { + matched ||= files.some(f => + rule.signals.paths.some(p => f.path?.startsWith(p)) + ); + } + + if (rule.signals.keywords) { + matched ||= deps.some(d => rule.signals.keywords.includes(d)); + } + + if (matched) detected.add(rule.capability); + } + + return [...detected]; +} diff --git a/server/src/services/featureRules.js b/server/src/services/featureRules.js new file mode 100644 index 00000000..041dd56e --- /dev/null +++ b/server/src/services/featureRules.js @@ -0,0 +1,112 @@ +export const FEATURE_RULES = [ + + // ───────────── DevOps / Infra ───────────── + { + capability: "Containerization", + signals: { files: ["Dockerfile", "docker-compose.yml"] } + }, + { + capability: "CI/CD Automation", + signals: { paths: [".github/workflows/", ".gitlab-ci.yml", ".circleci/"] } + }, + { + capability: "Infrastructure as Code", + signals: { extensions: [".tf"], files: ["kustomization.yaml"] } + }, + { + capability: "Kubernetes Orchestration", + signals: { keywords: ["kubernetes", "helm", "k8s"] } + }, + + // ───────────── Frontend ───────────── + { + capability: "Frontend Application", + signals: { files: ["package.json", "vite.config.js", "next.config.js"] } + }, + { + capability: "Frontend Performance Optimization", + signals: { keywords: ["react-window", "react-virtualized", "lazyload"] } + }, + { + capability: "Static Site Generation", + signals: { keywords: ["gatsby", "next export", "astro"] } + }, + + // ───────────── Backend / Systems ───────────── + { + capability: "Backend API Development", + signals: { files: ["requirements.txt", "pom.xml", "go.mod"] } + }, + { + capability: "Authentication & Authorization", + signals: { keywords: ["jwt", "oauth", "passport", "auth"] } + }, + { + capability: "Caching & Performance", + signals: { keywords: ["redis", "memcached", "cache"] } + }, + { + capability: "Asynchronous Processing", + signals: { keywords: ["queue", "rabbitmq", "kafka", "bull"] } + }, + + // ───────────── Databases ───────────── + { + capability: "Relational Database Usage", + signals: { keywords: ["postgres", "mysql", "sqlite"] } + }, + { + capability: "NoSQL Database Usage", + signals: { keywords: ["mongodb", "cassandra", "dynamodb"] } + }, + + // ───────────── ML / AI ───────────── + { + capability: "ML Model Training", + signals: { files: ["training.py"], keywords: ["scikit-learn", "xgboost"] } + }, + { + capability: "Deep Learning", + signals: { keywords: ["torch", "tensorflow", "keras"] } + }, + { + capability: "NLP / LLM Systems", + signals: { keywords: ["transformers", "langchain", "openai", "llama"] } + }, + { + capability: "Data Analysis & Visualization", + signals: { keywords: ["pandas", "matplotlib", "seaborn"] } + }, + + // ───────────── Media / Streaming ───────────── + { + capability: "Video Streaming", + signals: { keywords: ["ffmpeg", "webrtc", "hls", "m3u8"] } + }, + { + capability: "Real-time Communication", + signals: { keywords: ["socket.io", "websocket", "rtc"] } + }, + + // ───────────── Testing / Quality ───────────── + { + capability: "Automated Testing", + signals: { keywords: ["jest", "pytest", "mocha", "cypress"] } + }, + { + capability: "Code Quality & Linting", + signals: { keywords: ["eslint", "prettier", "flake8"] } + }, + + // ───────────── Docs / Platform ───────────── + { + capability: "Documentation & Developer Experience", + signals: { files: ["README.md"], keywords: ["docs", "storybook"] } + }, + + // ───────────── Catch-all ───────────── + { + capability: "Automation / Tooling", + signals: { keywords: ["script", "automation", "cli"] } + } +]; diff --git a/server/src/services/getauthenticated.js b/server/src/services/getauthenticated.js new file mode 100644 index 00000000..626ad1f2 --- /dev/null +++ b/server/src/services/getauthenticated.js @@ -0,0 +1,19 @@ +export async function gh(url) { + // ensure leading slash + if (!url.startsWith("/")) { + url = "/" + url; + } + + const res = await fetch(`https://api.github.com${url}`, { + headers: { + Accept: "application/vnd.github+json" + } + }); + + if (!res.ok) { + const err = await res.json(); + throw new Error(err.message); + } + + return res.json(); +} diff --git a/server/src/services/getfiles.js b/server/src/services/getfiles.js new file mode 100644 index 00000000..37d3bfdb --- /dev/null +++ b/server/src/services/getfiles.js @@ -0,0 +1,4 @@ +import { gh } from "./getauthenticated.js"; +export async function getfiles(owner,repo){ + return gh(`/repos/${owner}/${repo}/contents`); +} diff --git a/server/src/services/getlanguages.js b/server/src/services/getlanguages.js new file mode 100644 index 00000000..0a34b955 --- /dev/null +++ b/server/src/services/getlanguages.js @@ -0,0 +1,5 @@ +import { gh } from "./getauthenticated.js"; + +export async function getlanguages(repo) { + return gh(`/repos/${repo.owner.login}/${repo.name}/languages`); +} diff --git a/server/src/services/getprs.js b/server/src/services/getprs.js new file mode 100644 index 00000000..4a08bcc3 --- /dev/null +++ b/server/src/services/getprs.js @@ -0,0 +1,9 @@ +import { gh } from "./getauthenticated.js"; +export async function getPRmetrics(owner,repo){ + const prs=await gh(`/repos/${owner}/${repo}/pulls?state=all`); + return prs + .filter(pr=>pr.merged_at).map(pr=>({ + prnumber:pr.number, + mergetimehours:(new Date(pr.merged_at)-new Date(pr.created_at))/36e5 + })); +} \ No newline at end of file diff --git a/server/src/services/getrepos.js b/server/src/services/getrepos.js new file mode 100644 index 00000000..3fde44c2 --- /dev/null +++ b/server/src/services/getrepos.js @@ -0,0 +1,11 @@ +import { gh } from "./getauthenticated.js"; + +export async function getUserRepos(username) { + const data = await gh(`/users/${username}/repos?per_page=1`); + + if (!Array.isArray(data)) { + throw new Error("Repos API failed"); + } + + return data; +} diff --git a/server/src/services/getresolutiontime.js b/server/src/services/getresolutiontime.js new file mode 100644 index 00000000..4b6f304f --- /dev/null +++ b/server/src/services/getresolutiontime.js @@ -0,0 +1,12 @@ +import { gh } from "./getauthenticated.js"; +export async function getIssueMetrics(owner, repo) { + const issues = await gh(`/repos/${owner}/${repo}/issues?state=all`); + + return issues + .filter(i => i.closed_at) + .map(i => ({ + issue: i.number, + resolutionHours: + (new Date(i.closed_at) - new Date(i.created_at)) / 36e5 + })); +} From 8da9ee9a3a01e22b1bad95db209a331c04f57cb7 Mon Sep 17 00:00:00 2001 From: Janvi Date: Sun, 11 Jan 2026 22:59:11 +0530 Subject: [PATCH 2/3] Work in progress on App.tsx --- client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 83c3b33a..4f4e65a7 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -31,7 +31,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> From 81a3de2d34363b6c39b24290bc01d3995b554b60 Mon Sep 17 00:00:00 2001 From: Janvi Date: Fri, 16 Jan 2026 23:03:38 +0530 Subject: [PATCH 3/3] Refactor user server/src/services and Implement the api functions in /infra/apis directory --- client/src/infra/rest/apis/github/index.ts | 68 +++++++++++ client/src/infra/rest/apis/github/typing.ts | 60 ++++++++++ server/index2.js | 2 +- server/src/services/analyze-user.js | 94 +++++++++++++++ .../src/services/constants/feature-rules.js | 105 ++++++++++++++++ server/src/services/corelogic.js | 47 -------- server/src/services/detect-capabilities.js | 38 ++++++ server/src/services/featureDetector.js | 27 ----- server/src/services/featureRules.js | 112 ------------------ server/src/services/fetch-github-repos.js | 26 ++++ server/src/services/fetch-issue-metrics.js | 35 ++++++ .../services/fetch-pull-request-metrics.js | 35 ++++++ server/src/services/fetch-repository-files.js | 26 ++++ .../services/fetch-repository-languages.js | 30 +++++ server/src/services/getfiles.js | 4 - server/src/services/getlanguages.js | 5 - server/src/services/getprs.js | 9 -- server/src/services/getrepos.js | 11 -- server/src/services/getresolutiontime.js | 12 -- server/src/services/index.js | 12 ++ .../github-api.js} | 12 +- server/src/services/utils/index.js | 1 + 22 files changed, 539 insertions(+), 232 deletions(-) create mode 100644 client/src/infra/rest/apis/github/index.ts create mode 100644 client/src/infra/rest/apis/github/typing.ts create mode 100644 server/src/services/analyze-user.js create mode 100644 server/src/services/constants/feature-rules.js delete mode 100644 server/src/services/corelogic.js create mode 100644 server/src/services/detect-capabilities.js delete mode 100644 server/src/services/featureDetector.js delete mode 100644 server/src/services/featureRules.js create mode 100644 server/src/services/fetch-github-repos.js create mode 100644 server/src/services/fetch-issue-metrics.js create mode 100644 server/src/services/fetch-pull-request-metrics.js create mode 100644 server/src/services/fetch-repository-files.js create mode 100644 server/src/services/fetch-repository-languages.js delete mode 100644 server/src/services/getfiles.js delete mode 100644 server/src/services/getlanguages.js delete mode 100644 server/src/services/getprs.js delete mode 100644 server/src/services/getrepos.js delete mode 100644 server/src/services/getresolutiontime.js create mode 100644 server/src/services/index.js rename server/src/services/{getauthenticated.js => utils/github-api.js} (55%) create mode 100644 server/src/services/utils/index.js diff --git a/client/src/infra/rest/apis/github/index.ts b/client/src/infra/rest/apis/github/index.ts new file mode 100644 index 00000000..cae971b2 --- /dev/null +++ b/client/src/infra/rest/apis/github/index.ts @@ -0,0 +1,68 @@ +// api/github/index.ts +import { patch, post } from '../..'; +import { + AnalyzeUserPayload, + AnalyzeUserResponse, + GetIssueMetricsPayload, + IssueMetricsResponse, + GetPRMetricsPayload, + PRMetricsResponse, + GetLanguagesPayload, + LanguagesResponse, + GetFilesPayload, + GetUserReposPayload, + UserReposResponse, +} from './typing'; +import { ApiResponse, BaseApiResponse } from '../../typings'; + +export const analyzeUser = async (payload: AnalyzeUserPayload) => { + return post< + AnalyzeUserPayload, + ApiResponse + >('/api/github/analyze-user', true, payload); +}; + +export const getIssueMetrics = async (payload: GetIssueMetricsPayload) => { + return post< + GetIssueMetricsPayload, + ApiResponse + >('/api/github/issue-metrics', true, payload); +}; + +export const getPRMetrics = async (payload: GetPRMetricsPayload) => { + return post< + GetPRMetricsPayload, + ApiResponse + >('/api/github/pr-metrics', true, payload); +}; + +export const getLanguages = async (payload: GetLanguagesPayload) => { + return post< + GetLanguagesPayload, + ApiResponse + >('/api/github/languages', true, payload); +}; + +export const getFiles = async (payload: GetFilesPayload) => { + return post< + GetFilesPayload, + ApiResponse + >('/api/github/files', true, payload); +}; + +export const getUserRepos = async (payload: GetUserReposPayload) => { + return post< + GetUserReposPayload, + ApiResponse + >('/api/github/user-repos', true, payload); +}; + +export const detectCapabilities = async (payload: { + files: any[]; + deps?: string[]; +}) => { + return post< + { files: any[]; deps?: string[] }, + ApiResponse + >('/api/github/detect-capabilities', true, payload); +}; diff --git a/client/src/infra/rest/apis/github/typing.ts b/client/src/infra/rest/apis/github/typing.ts new file mode 100644 index 00000000..4dff282a --- /dev/null +++ b/client/src/infra/rest/apis/github/typing.ts @@ -0,0 +1,60 @@ +export interface AnalyzeUserPayload{ + username: string; +} +export interface AnalyzeUserResponse{ + repos: Array<{ + repo:string; + languages: string[]; + capabilities: string[]; + avgPRMergeTime: number | null; + avgIssueResolutionTime: number | null; + } >; +} +export interface GetIssueMetricsPayload{ + owner: string; + repo: string; +} + +export interface IssueMetricsResponse{ + issue: Array<{ + issue: number; + resolutionHours: number; + }>; +} + +export interface GetPRMetricsPayload{ + owner : string; + repo: string ; +} + +export interface PRMetricsResponse{ + prs:Array<{ + prnumber:number; + mergetimehours:number; + }>; +} + +export interface GetLanguagesPayload{ + owner:string; + repo:string; +} + +export interface LanguagesResponse { + [language: string]: number; +} + +export interface GetFilesPayload { + owner: string; + repo: string; +} + +export interface GetUserReposPayload { + username: string; +} + +export interface UserReposResponse { + repos: Array<{ + owner: { login: string }; + name: string; + }>; +} \ No newline at end of file diff --git a/server/index2.js b/server/index2.js index 611af148..19e7d417 100644 --- a/server/index2.js +++ b/server/index2.js @@ -1,6 +1,6 @@ import "dotenv/config"; -import { analyzeUser } from "./src/services/corelogic.js"; +import { analyzeUser } from "./src/services/index.js"; import { getAIRecommendation } from "./gemini.js"; async function run() { diff --git a/server/src/services/analyze-user.js b/server/src/services/analyze-user.js new file mode 100644 index 00000000..038beae9 --- /dev/null +++ b/server/src/services/analyze-user.js @@ -0,0 +1,94 @@ +/** + * Analyze User - Comprehensive GitHub user analysis + * Gathers repositories, analyzes code patterns, and computes metrics + */ + +import { fetchGitHubRepos } from './fetch-github-repos.js'; +import { fetchRepositoryFiles } from './fetch-repository-files.js'; +import { fetchRepositoryLanguages } from './fetch-repository-languages.js'; +import { fetchPullRequestMetrics } from './fetch-pull-request-metrics.js'; +import { fetchIssueMetrics } from './fetch-issue-metrics.js'; +import { detectCapabilities } from './detect-capabilities.js'; + +/** + * Analyzes a GitHub user's repositories and generates capability metrics + * Fetches repo information, analyzes files, and computes PR/issue metrics + * @param {string} username - GitHub username to analyze + * @returns {Promise>} Analysis results per repository + * @throws {Error} If user not found or API fails + */ +export async function analyzeUser(username) { + if (!username || typeof username !== 'string') { + throw new Error('Invalid username: must be a non-empty string'); + } + + try { + const repos = await fetchGitHubRepos(username); + const analysisResults = []; + + for (const repo of repos) { + console.log('Analyzing repo:', repo.owner.login, repo.name); + + try { + // Fetch repository files + console.log('Fetching files...'); + const files = await fetchRepositoryFiles( + repo.owner.login, + repo.name + ); + + // Detect capabilities based on files + const capabilities = detectCapabilities(files, []); + + // Fetch PR metrics + console.log('Fetching PR metrics...'); + const prMetrics = await fetchPullRequestMetrics( + repo.owner.login, + repo.name + ); + + // Fetch issue metrics + console.log('Fetching issue metrics...'); + const issueMetrics = await fetchIssueMetrics( + repo.owner.login, + repo.name + ); + + // Fetch languages + console.log('Fetching languages...'); + const languages = await fetchRepositoryLanguages(repo); + + // Calculate averages + const avgPRMergeTime = + prMetrics.length + ? prMetrics.reduce((a, b) => a + b.mergeTimeHours, 0) / + prMetrics.length + : null; + + const avgIssueResolutionTime = + issueMetrics.length + ? issueMetrics.reduce((a, b) => a + b.resolutionTimeHours, 0) / + issueMetrics.length + : null; + + analysisResults.push({ + repo: repo.name, + languages: Object.keys(languages), + capabilities, + avgPRMergeTime, + avgIssueResolutionTime + }); + } catch (repoError) { + console.error(`Error analyzing repo ${repo.name}:`, repoError.message); + // Continue with next repo even if one fails + continue; + } + } + + return analysisResults; + } catch (error) { + throw new Error( + `Failed to analyze user ${username}: ${error.message}` + ); + } +} diff --git a/server/src/services/constants/feature-rules.js b/server/src/services/constants/feature-rules.js new file mode 100644 index 00000000..e201fca8 --- /dev/null +++ b/server/src/services/constants/feature-rules.js @@ -0,0 +1,105 @@ +/** + * Feature Detection Rules - Capability detection rules based on file patterns and keywords + */ + +export const FEATURE_RULES = [ + // ───────────── DevOps / Infra ───────────── + { + capability: 'Containerization', + signals: { files: ['Dockerfile', 'docker-compose.yml'] } + }, + { + capability: 'CI/CD Automation', + signals: { + paths: ['.github/workflows/', '.gitlab-ci.yml', '.circleci/'] + } + }, + { + capability: 'Infrastructure as Code', + signals: { extensions: ['.tf'], files: ['kustomization.yaml'] } + }, + { + capability: 'Kubernetes Orchestration', + signals: { keywords: ['kubernetes', 'helm', 'k8s'] } + }, + + // ───────────── Frontend ───────────── + { + capability: 'Frontend Application', + signals: { files: ['package.json', 'vite.config.js', 'next.config.js'] } + }, + { + capability: 'Frontend Performance Optimization', + signals: { keywords: ['react-window', 'react-virtualized', 'lazyload'] } + }, + { + capability: 'Static Site Generation', + signals: { keywords: ['gatsby', 'next export', 'astro'] } + }, + + // ───────────── Backend / Systems ───────────── + { + capability: 'Backend API Development', + signals: { files: ['requirements.txt', 'pom.xml', 'go.mod'] } + }, + { + capability: 'Authentication & Authorization', + signals: { keywords: ['jwt', 'oauth', 'passport', 'auth'] } + }, + { + capability: 'Caching & Performance', + signals: { keywords: ['redis', 'memcached', 'cache'] } + }, + { + capability: 'Asynchronous Processing', + signals: { keywords: ['queue', 'rabbitmq', 'kafka', 'bull'] } + }, + + // ───────────── Databases ───────────── + { + capability: 'Relational Database Usage', + signals: { keywords: ['postgres', 'mysql', 'sqlite'] } + }, + { + capability: 'NoSQL Database Usage', + signals: { keywords: ['mongodb', 'cassandra', 'dynamodb'] } + }, + + // ───────────── ML / AI ───────────── + { + capability: 'ML Model Training', + signals: { files: ['training.py'], keywords: ['scikit-learn', 'xgboost'] } + }, + { + capability: 'Deep Learning', + signals: { keywords: ['torch', 'tensorflow', 'keras'] } + }, + { + capability: 'NLP / LLM Systems', + signals: { keywords: ['transformers', 'langchain', 'openai', 'llama'] } + }, + { + capability: 'Data Analysis & Visualization', + signals: { keywords: ['pandas', 'matplotlib', 'seaborn'] } + }, + + // ───────────── Media / Streaming ───────────── + { + capability: 'Video Streaming', + signals: { keywords: ['ffmpeg', 'webrtc', 'hls', 'm3u8'] } + }, + { + capability: 'Real-time Communication', + signals: { keywords: ['socket.io', 'websocket', 'rtc'] } + }, + + // ───────────── Testing / Quality ───────────── + { + capability: 'Automated Testing', + signals: { keywords: ['jest', 'pytest', 'mocha', 'cypress'] } + }, + { + capability: 'Code Quality & Linting', + signals: { keywords: ['eslint', 'prettier', 'flake8'] } + } +]; diff --git a/server/src/services/corelogic.js b/server/src/services/corelogic.js deleted file mode 100644 index 05e4f616..00000000 --- a/server/src/services/corelogic.js +++ /dev/null @@ -1,47 +0,0 @@ -import { getUserRepos } from "./getrepos.js"; -import { getfiles } from "./getfiles.js"; -import { getPRmetrics } from "./getprs.js"; -import { getIssueMetrics } from "./getresolutiontime.js"; -import { getlanguages } from "./getlanguages.js"; -import { detectCapabilities } from "./featureDetector.js"; - -export async function analyzeUser(username) { - const repos = await getUserRepos(username); - const analysisResults = []; - - for (const repo of repos) { - console.log("Repo:", repo.owner.login, repo.name); - - console.log("Fetching files..."); - const files = await getfiles(repo.owner.login, repo.name); - - const capabilities = detectCapabilities(files, []); - - console.log("Fetching PRs..."); - const prMetrics = await getPRmetrics(repo.owner.login, repo.name); - - console.log("Fetching issues..."); - const issueMetrics = await getIssueMetrics(repo.owner.login, repo.name); - - console.log("Fetching languages..."); - const languages = await getlanguages(repo); - - analysisResults.push({ - repo: repo.name, - languages: Object.keys(languages), - capabilities, - avgPRMergeTime: - prMetrics.length - ? prMetrics.reduce((a, b) => a + b.mergetimehours, 0) / - prMetrics.length - : null, - avgIssueResolutionTime: - issueMetrics.length - ? issueMetrics.reduce((a, b) => a + b.resolutionHours, 0) / - issueMetrics.length - : null - }); - } - - return analysisResults; -} diff --git a/server/src/services/detect-capabilities.js b/server/src/services/detect-capabilities.js new file mode 100644 index 00000000..0348cd6d --- /dev/null +++ b/server/src/services/detect-capabilities.js @@ -0,0 +1,38 @@ +/** + * Detect developer capabilities based on repository files and dependencies + * Analyzes files, paths, and keywords to identify technical skills + */ + +import { FEATURE_RULES } from './constants/feature-rules.js'; + +/** + * Analyzes files and dependencies to detect developer capabilities + * @param {Array} files - Repository files with name and path properties + * @param {Array} deps - Dependency list (optional) + * @returns {Array} List of detected capabilities + */ +export function detectCapabilities(files, deps = []) { + const detected = new Set(); + + for (const rule of FEATURE_RULES) { + let matched = false; + + if (rule.signals.files) { + matched ||= files.some((f) => rule.signals.files.includes(f.name)); + } + + if (rule.signals.paths) { + matched ||= files.some((f) => + rule.signals.paths.some((p) => f.path?.startsWith(p)) + ); + } + + if (rule.signals.keywords) { + matched ||= deps.some((d) => rule.signals.keywords.includes(d)); + } + + if (matched) detected.add(rule.capability); + } + + return [...detected]; +} diff --git a/server/src/services/featureDetector.js b/server/src/services/featureDetector.js deleted file mode 100644 index 09d200f7..00000000 --- a/server/src/services/featureDetector.js +++ /dev/null @@ -1,27 +0,0 @@ -import { FEATURE_RULES } from "./featureRules.js"; - -export function detectCapabilities(files, deps = []) { - const detected = new Set(); - - for (const rule of FEATURE_RULES) { - let matched = false; - - if (rule.signals.files) { - matched ||= files.some(f => rule.signals.files.includes(f.name)); - } - - if (rule.signals.paths) { - matched ||= files.some(f => - rule.signals.paths.some(p => f.path?.startsWith(p)) - ); - } - - if (rule.signals.keywords) { - matched ||= deps.some(d => rule.signals.keywords.includes(d)); - } - - if (matched) detected.add(rule.capability); - } - - return [...detected]; -} diff --git a/server/src/services/featureRules.js b/server/src/services/featureRules.js deleted file mode 100644 index 041dd56e..00000000 --- a/server/src/services/featureRules.js +++ /dev/null @@ -1,112 +0,0 @@ -export const FEATURE_RULES = [ - - // ───────────── DevOps / Infra ───────────── - { - capability: "Containerization", - signals: { files: ["Dockerfile", "docker-compose.yml"] } - }, - { - capability: "CI/CD Automation", - signals: { paths: [".github/workflows/", ".gitlab-ci.yml", ".circleci/"] } - }, - { - capability: "Infrastructure as Code", - signals: { extensions: [".tf"], files: ["kustomization.yaml"] } - }, - { - capability: "Kubernetes Orchestration", - signals: { keywords: ["kubernetes", "helm", "k8s"] } - }, - - // ───────────── Frontend ───────────── - { - capability: "Frontend Application", - signals: { files: ["package.json", "vite.config.js", "next.config.js"] } - }, - { - capability: "Frontend Performance Optimization", - signals: { keywords: ["react-window", "react-virtualized", "lazyload"] } - }, - { - capability: "Static Site Generation", - signals: { keywords: ["gatsby", "next export", "astro"] } - }, - - // ───────────── Backend / Systems ───────────── - { - capability: "Backend API Development", - signals: { files: ["requirements.txt", "pom.xml", "go.mod"] } - }, - { - capability: "Authentication & Authorization", - signals: { keywords: ["jwt", "oauth", "passport", "auth"] } - }, - { - capability: "Caching & Performance", - signals: { keywords: ["redis", "memcached", "cache"] } - }, - { - capability: "Asynchronous Processing", - signals: { keywords: ["queue", "rabbitmq", "kafka", "bull"] } - }, - - // ───────────── Databases ───────────── - { - capability: "Relational Database Usage", - signals: { keywords: ["postgres", "mysql", "sqlite"] } - }, - { - capability: "NoSQL Database Usage", - signals: { keywords: ["mongodb", "cassandra", "dynamodb"] } - }, - - // ───────────── ML / AI ───────────── - { - capability: "ML Model Training", - signals: { files: ["training.py"], keywords: ["scikit-learn", "xgboost"] } - }, - { - capability: "Deep Learning", - signals: { keywords: ["torch", "tensorflow", "keras"] } - }, - { - capability: "NLP / LLM Systems", - signals: { keywords: ["transformers", "langchain", "openai", "llama"] } - }, - { - capability: "Data Analysis & Visualization", - signals: { keywords: ["pandas", "matplotlib", "seaborn"] } - }, - - // ───────────── Media / Streaming ───────────── - { - capability: "Video Streaming", - signals: { keywords: ["ffmpeg", "webrtc", "hls", "m3u8"] } - }, - { - capability: "Real-time Communication", - signals: { keywords: ["socket.io", "websocket", "rtc"] } - }, - - // ───────────── Testing / Quality ───────────── - { - capability: "Automated Testing", - signals: { keywords: ["jest", "pytest", "mocha", "cypress"] } - }, - { - capability: "Code Quality & Linting", - signals: { keywords: ["eslint", "prettier", "flake8"] } - }, - - // ───────────── Docs / Platform ───────────── - { - capability: "Documentation & Developer Experience", - signals: { files: ["README.md"], keywords: ["docs", "storybook"] } - }, - - // ───────────── Catch-all ───────────── - { - capability: "Automation / Tooling", - signals: { keywords: ["script", "automation", "cli"] } - } -]; diff --git a/server/src/services/fetch-github-repos.js b/server/src/services/fetch-github-repos.js new file mode 100644 index 00000000..655415ec --- /dev/null +++ b/server/src/services/fetch-github-repos.js @@ -0,0 +1,26 @@ +/** + * Fetch GitHub Repositories - Get user's repositories from GitHub API + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches all repositories for a given GitHub user + * @param {string} username - GitHub username + * @param {number} perPage - Number of repos to fetch per page (default: 1) + * @returns {Promise>} Array of repository objects + * @throws {Error} If API call fails or returns invalid data + */ +export async function fetchGitHubRepos(username, perPage = 1) { + try { + const data = await gh(`/users/${username}/repos?per_page=${perPage}`); + + if (!Array.isArray(data)) { + throw new Error('Failed to fetch repositories: Invalid response format'); + } + + return data; + } catch (error) { + throw new Error(`Failed to fetch repositories for user ${username}: ${error.message}`); + } +} diff --git a/server/src/services/fetch-issue-metrics.js b/server/src/services/fetch-issue-metrics.js new file mode 100644 index 00000000..bd32849e --- /dev/null +++ b/server/src/services/fetch-issue-metrics.js @@ -0,0 +1,35 @@ +/** + * Fetch Issue Metrics - Analyze issue resolution times and statistics + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches and analyzes issue metrics for a repository + * Calculates resolution time in hours for each closed issue + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @returns {Promise>} Array of issue metrics with resolution times + * @throws {Error} If API call fails + */ +export async function fetchIssueMetrics(owner, repo) { + try { + const issues = await gh(`/repos/${owner}/${repo}/issues?state=all`); + + if (!Array.isArray(issues)) { + throw new Error('Invalid response format: expected array of issues'); + } + + return issues + .filter((issue) => issue.closed_at) + .map((issue) => ({ + issueNumber: issue.number, + resolutionTimeHours: + (new Date(issue.closed_at) - new Date(issue.created_at)) / 36e5 + })); + } catch (error) { + throw new Error( + `Failed to fetch issue metrics for ${owner}/${repo}: ${error.message}` + ); + } +} diff --git a/server/src/services/fetch-pull-request-metrics.js b/server/src/services/fetch-pull-request-metrics.js new file mode 100644 index 00000000..356edd65 --- /dev/null +++ b/server/src/services/fetch-pull-request-metrics.js @@ -0,0 +1,35 @@ +/** + * Fetch Pull Request Metrics - Analyze PR merge times and statistics + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches and analyzes pull request metrics for a repository + * Calculates merge time in hours for each merged PR + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @returns {Promise>} Array of PR metrics with merge times + * @throws {Error} If API call fails + */ +export async function fetchPullRequestMetrics(owner, repo) { + try { + const prs = await gh(`/repos/${owner}/${repo}/pulls?state=all`); + + if (!Array.isArray(prs)) { + throw new Error('Invalid response format: expected array of PRs'); + } + + return prs + .filter((pr) => pr.merged_at) + .map((pr) => ({ + prNumber: pr.number, + mergeTimeHours: + (new Date(pr.merged_at) - new Date(pr.created_at)) / 36e5 + })); + } catch (error) { + throw new Error( + `Failed to fetch PR metrics for ${owner}/${repo}: ${error.message}` + ); + } +} diff --git a/server/src/services/fetch-repository-files.js b/server/src/services/fetch-repository-files.js new file mode 100644 index 00000000..9ffcdf18 --- /dev/null +++ b/server/src/services/fetch-repository-files.js @@ -0,0 +1,26 @@ +/** + * Fetch Repository Files - Get file structure from a GitHub repository + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches the file contents structure of a GitHub repository + * @param {string} owner - Repository owner (GitHub username) + * @param {string} repo - Repository name + * @param {string} path - File path to fetch (optional, default: root) + * @returns {Promise>} Array of file objects with name and path + * @throws {Error} If API call fails + */ +export async function fetchRepositoryFiles(owner, repo, path = '') { + try { + const url = path + ? `/repos/${owner}/${repo}/contents/${path}` + : `/repos/${owner}/${repo}/contents`; + + const data = await gh(url); + return Array.isArray(data) ? data : [data]; + } catch (error) { + throw new Error(`Failed to fetch files from ${owner}/${repo}: ${error.message}`); + } +} diff --git a/server/src/services/fetch-repository-languages.js b/server/src/services/fetch-repository-languages.js new file mode 100644 index 00000000..3b0f56d7 --- /dev/null +++ b/server/src/services/fetch-repository-languages.js @@ -0,0 +1,30 @@ +/** + * Fetch Repository Languages - Get programming languages used in a repository + */ + +import { gh } from './utils/index.js'; + +/** + * Fetches the programming languages used in a GitHub repository + * @param {Object} repo - Repository object with owner and name properties + * @param {string} repo.owner.login - Repository owner username + * @param {string} repo.name - Repository name + * @returns {Promise} Object with language names as keys and bytes as values + * @throws {Error} If API call fails + */ +export async function fetchRepositoryLanguages(repo) { + try { + if (!repo || !repo.owner || !repo.owner.login || !repo.name) { + throw new Error('Invalid repository object: missing owner or name'); + } + + const data = await gh( + `/repos/${repo.owner.login}/${repo.name}/languages` + ); + return data || {}; + } catch (error) { + throw new Error( + `Failed to fetch languages from ${repo.owner?.login}/${repo.name}: ${error.message}` + ); + } +} diff --git a/server/src/services/getfiles.js b/server/src/services/getfiles.js deleted file mode 100644 index 37d3bfdb..00000000 --- a/server/src/services/getfiles.js +++ /dev/null @@ -1,4 +0,0 @@ -import { gh } from "./getauthenticated.js"; -export async function getfiles(owner,repo){ - return gh(`/repos/${owner}/${repo}/contents`); -} diff --git a/server/src/services/getlanguages.js b/server/src/services/getlanguages.js deleted file mode 100644 index 0a34b955..00000000 --- a/server/src/services/getlanguages.js +++ /dev/null @@ -1,5 +0,0 @@ -import { gh } from "./getauthenticated.js"; - -export async function getlanguages(repo) { - return gh(`/repos/${repo.owner.login}/${repo.name}/languages`); -} diff --git a/server/src/services/getprs.js b/server/src/services/getprs.js deleted file mode 100644 index 4a08bcc3..00000000 --- a/server/src/services/getprs.js +++ /dev/null @@ -1,9 +0,0 @@ -import { gh } from "./getauthenticated.js"; -export async function getPRmetrics(owner,repo){ - const prs=await gh(`/repos/${owner}/${repo}/pulls?state=all`); - return prs - .filter(pr=>pr.merged_at).map(pr=>({ - prnumber:pr.number, - mergetimehours:(new Date(pr.merged_at)-new Date(pr.created_at))/36e5 - })); -} \ No newline at end of file diff --git a/server/src/services/getrepos.js b/server/src/services/getrepos.js deleted file mode 100644 index 3fde44c2..00000000 --- a/server/src/services/getrepos.js +++ /dev/null @@ -1,11 +0,0 @@ -import { gh } from "./getauthenticated.js"; - -export async function getUserRepos(username) { - const data = await gh(`/users/${username}/repos?per_page=1`); - - if (!Array.isArray(data)) { - throw new Error("Repos API failed"); - } - - return data; -} diff --git a/server/src/services/getresolutiontime.js b/server/src/services/getresolutiontime.js deleted file mode 100644 index 4b6f304f..00000000 --- a/server/src/services/getresolutiontime.js +++ /dev/null @@ -1,12 +0,0 @@ -import { gh } from "./getauthenticated.js"; -export async function getIssueMetrics(owner, repo) { - const issues = await gh(`/repos/${owner}/${repo}/issues?state=all`); - - return issues - .filter(i => i.closed_at) - .map(i => ({ - issue: i.number, - resolutionHours: - (new Date(i.closed_at) - new Date(i.created_at)) / 36e5 - })); -} diff --git a/server/src/services/index.js b/server/src/services/index.js new file mode 100644 index 00000000..00c1d80f --- /dev/null +++ b/server/src/services/index.js @@ -0,0 +1,12 @@ +/** + * GitHub Analysis Services + * Exports all service functions for external use + */ + +export { analyzeUser } from './analyze-user.js'; +export { fetchGitHubRepos } from './fetch-github-repos.js'; +export { fetchRepositoryFiles } from './fetch-repository-files.js'; +export { fetchRepositoryLanguages } from './fetch-repository-languages.js'; +export { fetchPullRequestMetrics } from './fetch-pull-request-metrics.js'; +export { fetchIssueMetrics } from './fetch-issue-metrics.js'; +export { detectCapabilities } from './detect-capabilities.js'; diff --git a/server/src/services/getauthenticated.js b/server/src/services/utils/github-api.js similarity index 55% rename from server/src/services/getauthenticated.js rename to server/src/services/utils/github-api.js index 626ad1f2..90e668dd 100644 --- a/server/src/services/getauthenticated.js +++ b/server/src/services/utils/github-api.js @@ -1,12 +1,16 @@ +/** + * GitHub API Helper - Base function for all GitHub API calls + */ + export async function gh(url) { - // ensure leading slash - if (!url.startsWith("/")) { - url = "/" + url; + // Ensure leading slash + if (!url.startsWith('/')) { + url = '/' + url; } const res = await fetch(`https://api.github.com${url}`, { headers: { - Accept: "application/vnd.github+json" + Accept: 'application/vnd.github+json' } }); diff --git a/server/src/services/utils/index.js b/server/src/services/utils/index.js new file mode 100644 index 00000000..30cf98b7 --- /dev/null +++ b/server/src/services/utils/index.js @@ -0,0 +1 @@ +export { gh } from './github-api.js';