├── .DS_Store ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc ├── .vscode-test.mjs ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist └── extension.js ├── esbuild.js ├── eslint.config.mjs ├── package-lock.json ├── package.json ├── src ├── components │ ├── CodingStatsDashboard.tsx │ └── ui │ │ └── card.tsx ├── extension.ts ├── services │ ├── changeAnalyzer.ts │ ├── gitService.ts │ ├── githubService.ts │ ├── projectContext.ts │ ├── scheduler.ts │ ├── statisticsProvider.ts │ ├── statisticsView.ts │ ├── statusBarManager.ts │ ├── summaryGenerator.ts │ ├── tracker.ts │ └── websiteGenerator.ts └── test │ ├── extension.test.ts │ ├── runTest.ts │ └── suite │ └── index.ts ├── studio-128x128.png ├── temp-test └── package.json ├── tsconfig.json └── vsc-extension-quickstart.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teamial/DevTrack/08314eb19138782b1f53077260c5f86bb37fa976/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "prettier" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint", "prettier"], 19 | "rules": { 20 | "no-console": ["warn", { "allow": ["warn", "error"] }], 21 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] 22 | }, 23 | "overrides": [ 24 | { 25 | "files": ["src/test/**/*.ts"], 26 | "rules": { 27 | "no-undef": "off", 28 | "no-unused-vars": "off" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '18' 20 | 21 | - name: Install Dependencies 22 | run: npm install 23 | 24 | - name: Lint 25 | run: npm run lint 26 | 27 | # Set up virtual display for VS Code tests 28 | # - name: Set up virtual display 29 | # run: | 30 | # sudo apt-get install -y xvfb 31 | # export DISPLAY=':99.0' 32 | # Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 33 | 34 | # # Run tests with xvfb-run 35 | # - name: Test 36 | # run: xvfb-run -a npm test 37 | 38 | - name: Build 39 | run: npm run compile -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | *.log 7 | .env 8 | node_modules 9 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register", 3 | "spec": "src/test/**/*.test.ts", 4 | "extensions": ["ts"], 5 | "watch-files": ["src/**/*.ts"], 6 | "recursive": true, 7 | "timeout": 5000, 8 | "exit": true 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode-test.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@vscode/test-cli'; 2 | 3 | export default defineConfig({ 4 | files: 'out/test/**/*.test.js', 5 | }); 6 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | out/test/** 5 | src/**/*.spec.ts 6 | .vscodeignore 7 | *.log 8 | .eslintrc.js 9 | node_modules/** 10 | src/** 11 | .gitignore 12 | .yarnrc 13 | esbuild.js 14 | vsc-extension-quickstart.md 15 | **/tsconfig.json 16 | **/eslint.config.mjs 17 | **/*.map 18 | **/*.ts 19 | **/.vscode-test.* 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "devtrack" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Teanna Cole 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevTrack (・ω<)☆ 2 | 3 | ## **Track Your Development Journey with Ease!** 4 | 5 | ### What's DevTrack? 6 | 7 | DevTrack is a productivity tool designed to seamlessly track and log your coding activities. It doesn't only provide a clear log of your coding activities but helps accurately reflect your contributions on GitHub. With a focus on simplicity and productivity, DevTrack integrates directly with your GitHub account to automatically create a history of your progress, making it easy to review, reflect, and share your work. DevTrack tracks changes to your code, commits updates to a dedicated repository, and enables you to visualize your progress over time. Whether you're working on personal projects, contributing to open source, or collaborating with a team, DevTrack is the ultimate tool for staying on top of your development journey. 8 | 9 | --- 10 | ## **Key Features** 11 | 12 | 1. **Secure GitHub Integration**: Seamlessly authenticate with your GitHub account using VS Code's secure authentication system, ensuring your credentials are safely managed. 13 | 14 | 2. **Automated Activity Monitoring**: Track your coding progress automatically in the background, capturing changes to your workspace files while respecting your privacy preferences. 15 | 16 | 3. **Intelligent Version Control**: Maintain a dedicated `code-tracking` repository on GitHub that automatically documents your coding journey through organized commit histories. Each commit captures the essence of your development session. 17 | 18 | 4. **Flexible Configuration Options**: 19 | - Customize commit frequency to match your workflow 20 | - Define specific file patterns to exclude from tracking 21 | - Toggle commit confirmation dialogs 22 | - Specify custom repository names for activity logging 23 | 24 | 5. **Real-Time Status Indicators**: Stay informed about your tracking status through VS Code's status bar, providing immediate visibility into: 25 | - Active tracking status 26 | - Last commit timestamp 27 | - GitHub connection status 28 | - Sync status with remote repository 29 | 30 | 6. **Seamless Workspace Integration**: Works quietly in the background of any VS Code workspace, requiring minimal setup while maintaining a comprehensive record of your development activities. 31 | 32 | 7. **Smart Synchronization**: 33 | - Handles remote repository management automatically 34 | - Resolves conflicts intelligently 35 | - Maintains data integrity across multiple development sessions 36 | - Preserves your existing workspace configurations 37 | 38 | 8. **Development Timeline**: 39 | - Creates a chronological record of your coding activities 40 | - Documents file modifications and project progress 41 | - Maintains a searchable history of your development journey 42 | - Helps track time invested in different projects 43 | 44 | --- 45 | 46 | ## **Getting Started** 47 | 1. **Install the Extension**: 48 | - **You must install [Git](https://git-scm.com/downloads) if you haven't already in order for DevTrack to work** 49 | - Open Visual Studio Code. 50 | - Go to the Extensions Marketplace (`Ctrl+Shift+X` or `Cmd+Shift+X`). 51 | - Search for "DevTrack" and click "Install." 52 | (**Alternatively**: Install [DevTrack](https://marketplace.visualstudio.com/items?itemName=TeannaCole.devtrack) from VS Code Marketplace) 53 | 54 | 3. **Log in to GitHub**: 55 | - Click the GitHub icon in the VS Code status bar. 56 | - Authenticate with your GitHub account. 57 | 58 | 4. Choose your tracking preferences in VS Code Settings (`Ctrl+,` or `Cmd+,`): 59 | ```json 60 | { 61 | "devtrack.repoName": "code-tracking", 62 | "devtrack.commitFrequency": 30, 63 | "devtrack.exclude": [ 64 | "node_modules", 65 | "dist", 66 | ".git" 67 | ] 68 | } 69 | ``` 70 | 4. Start tracking with `DevTrack: Start Tracking` command in the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`). 71 | 5. **View Your Progress**: 72 | - Visit your `code-tracking` repository on GitHub to see your logs. 73 | 74 | Want to showcase your DevTrack usage? [Open a PR](https://github.com/Teamial/DevTrack/pulls) to add your example! 75 | 76 | --- 77 | ## Windows Installation Guide 78 | 79 | If you're using Windows, please follow these steps to ensure DevTrack works correctly: 80 | 81 | 1. Install Git for Windows 82 | - Download Git from https://git-scm.com/download/windows 83 | - During installation: 84 | - Select "Use Git from the Command Line and also from 3rd-party software" 85 | - Choose "Use Windows' default console window" 86 | - Select "Enable Git Credential Manager" 87 | - All other options can remain default 88 | 89 | 2. Configure Git (Run in Command Prompt or PowerShell) 90 | ```powershell 91 | git config --global core.autocrlf true 92 | git config --global core.safecrlf false 93 | git config --global core.longpaths true 94 | 95 | 3. **Refer back to Getting Started section** 96 | --- 97 | ## Preview Example 98 | 99 | ![Project Tracking Example](https://github.com/user-attachments/assets/eeab8b20-203d-441c-af2e-969c6cdeb980) 100 | - Repository: [code-tracking](https://github.com/Teamial/code-tracking) 101 | - Shows daily coding patterns 102 | - Tracks multiple project files 103 | - Automatic commits every 30 minutes 104 | 105 | --- 106 | ## For Contributors 107 | 108 | 1. **Clone the Repository**: 109 | ```bash 110 | git clone https://github.com//code-tracking.git 111 | cd code-tracking 112 | ``` 113 | 114 | 2. **Create a New Branch**: 115 | ```bash 116 | git checkout -b feat/your-feature-name 117 | # or 118 | git checkout -b fix/your-bug-fix 119 | ``` 120 | 121 | 3. **Install Dependencies**: 122 | ```bash 123 | npm install 124 | ``` 125 | 126 | 4. **Run the Extension**: 127 | - Open the project in VS Code. 128 | - Press `F5` to start debugging the extension. 129 | 130 | 5. **Contribute**: 131 | - Follow the Contributor Expectations outlined below. 132 | 133 | --- 134 | 135 | ## **Contributor Expectations** 136 | 137 | Contributions to improve DevTrack are always welcome! Here's how you can help: 138 | 139 | - **Open an Issue First**: Before submitting a pull request, file an issue explaining the bug or feature. 140 | - **Test Your Changes**: Verify that your contributions don't break existing functionality. 141 | - **Squash Commits**: Consolidate multiple commits into a single, meaningful one before opening a pull request. 142 | 143 | --- 144 | 145 | ### **Start Tracking Your Coding Journey with DevTrack Today!** 146 | [**Buy Me a Coffee!**](https://marketplace.visualstudio.com/items?itemName=TeannaCole.devtrack) 147 | 148 | -------------------------------------------------------------------------------- /esbuild.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const production = process.argv.includes('--production'); 3 | const watch = process.argv.includes('--watch'); 4 | 5 | async function build() { 6 | const context = await esbuild.context({ 7 | entryPoints: ['src/extension.ts'], 8 | bundle: true, 9 | outfile: 'dist/extension.js', 10 | external: ['vscode'], 11 | format: 'cjs', 12 | platform: 'node', 13 | target: 'node14', 14 | sourcemap: !production, 15 | minify: production, 16 | treeShaking: true, 17 | }); 18 | 19 | if (watch) { 20 | await context.watch(); 21 | console.log('[watch] build finished, watching for changes...'); 22 | } else { 23 | await context.rebuild(); 24 | await context.dispose(); 25 | console.log('[build] build finished'); 26 | } 27 | } 28 | 29 | build().catch((err) => { 30 | console.error(err); 31 | process.exit(1); 32 | }); 33 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // eslint.config.mjs 2 | import tsPlugin from "@typescript-eslint/eslint-plugin"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import prettierPlugin from "eslint-plugin-prettier"; 5 | import prettierConfig from "eslint-config-prettier"; 6 | 7 | export default [ 8 | { 9 | files: ["**/*.js", "**/*.tsx"], 10 | languageOptions: { 11 | parser: tsParser, 12 | parserOptions: { 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | }, 16 | }, 17 | }, 18 | // Apply TypeScript-specific rules 19 | { 20 | files: ["**/*.ts", "**/*.tsx"], 21 | languageOptions: { 22 | parser: tsParser, 23 | parserOptions: { 24 | ecmaVersion: 2022, 25 | sourceType: "module", 26 | }, 27 | }, 28 | plugins: { 29 | "@typescript-eslint": tsPlugin, 30 | prettier: prettierPlugin, 31 | }, 32 | rules: { 33 | "no-unused-vars": "warn", 34 | "no-undef": "warn", 35 | "no-console": "warn", 36 | "no-empty": "warn", 37 | "no-debugger": "warn", 38 | "no-alert": "warn", 39 | "no-eval": "warn", 40 | "no-implied-eval": "warn", 41 | "no-multi-str": "warn", 42 | "no-template-curly-in-string": "warn", 43 | "no-unreachable": "warn", 44 | "no-useless-escape": "warn", 45 | 46 | "@typescript-eslint/naming-convention": [ 47 | "warn", 48 | { 49 | selector: "import", 50 | format: ["camelCase", "PascalCase"], 51 | }, 52 | ], 53 | "@typescript-eslint/explicit-function-return-type": "off", 54 | curly: "warn", 55 | eqeqeq: "warn", 56 | "no-throw-literal": "warn", 57 | semi: ["warn", "always"], 58 | "prettier/prettier": "warn", // Integrate Prettier 59 | }, 60 | }, 61 | ]; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtrack", 3 | "displayName": "DevTrack", 4 | "description": "Track your coding journey effortlessly with DevTrack", 5 | "version": "7.1.5", 6 | "publisher": "TeannaCole", 7 | "repository": { 8 | "type": "GitHub", 9 | "url": "https://github.com/Teamial/DevTrack/tree/master" 10 | }, 11 | "license": "MIT", 12 | "engines": { 13 | "vscode": "^1.96.0" 14 | }, 15 | "categories": [ 16 | "SCM Providers", 17 | "Other", 18 | "Programming Languages", 19 | "Formatters", 20 | "Education" 21 | ], 22 | "keywords": [ 23 | "git", 24 | "tracking", 25 | "productivity", 26 | "automation", 27 | "code-analytics", 28 | "time-tracking", 29 | "version-control", 30 | "collaboration", 31 | "github", 32 | "documentation" 33 | ], 34 | "activationEvents": [ 35 | "onView:devtrack", 36 | "onStartupFinished" 37 | ], 38 | "main": "./dist/extension.js", 39 | "icon": "studio-128x128.png", 40 | "contributes": { 41 | "commands": [ 42 | { 43 | "command": "devtrack.startTracking", 44 | "title": "DevTrack: Start Tracking", 45 | "enablement": "workspaceFolderCount != 0", 46 | "icon": "$(play-circle)" 47 | }, 48 | { 49 | "command": "devtrack.stopTracking", 50 | "title": "DevTrack: Stop Tracking", 51 | "enablement": "workspaceFolderCount != 0", 52 | "icon": "$(stop-circle)" 53 | }, 54 | { 55 | "command": "devtrack.pauseTracking", 56 | "title": "DevTrack: Pause Tracking", 57 | "enablement": "workspaceFolderCount != 0", 58 | "icon": "$(debug-pause)" 59 | }, 60 | { 61 | "command": "devtrack.resumeTracking", 62 | "title": "DevTrack: Resume Tracking", 63 | "enablement": "workspaceFolderCount != 0", 64 | "icon": "$(debug-continue)" 65 | }, 66 | { 67 | "command": "devtrack.commitNow", 68 | "title": "DevTrack: Commit Now", 69 | "enablement": "workspaceFolderCount != 0", 70 | "icon": "$(save-all)" 71 | }, 72 | { 73 | "command": "devtrack.login", 74 | "title": "DevTrack: Login to GitHub", 75 | "enablement": "workspaceFolderCount != 0", 76 | "icon": "$(github)" 77 | }, 78 | { 79 | "command": "devtrack.logout", 80 | "title": "DevTrack: Logout from GitHub", 81 | "icon": "$(sign-out)" 82 | }, 83 | { 84 | "command": "devtrack.showGitGuide", 85 | "title": "DevTrack: Show Git Installation Guide", 86 | "icon": "$(book)" 87 | }, 88 | { 89 | "command": "devtrack.openFolder", 90 | "title": "DevTrack: Open Folder", 91 | "enablement": "workspaceFolderCount == 0", 92 | "icon": "$(folder-opened)" 93 | }, 94 | { 95 | "command": "devtrack.showDashboard", 96 | "title": "DevTrack: Show Activity Dashboard", 97 | "enablement": "workspaceFolderCount != 0", 98 | "icon": "$(graph)" 99 | }, 100 | { 101 | "command": "devtrack.generateReport", 102 | "title": "DevTrack: Generate Activity Report", 103 | "enablement": "workspaceFolderCount != 0", 104 | "icon": "$(file-text)" 105 | }, 106 | { 107 | "command": "devtrack.generateWebsite", 108 | "title": "DevTrack: Generate Statistics Website", 109 | "enablement": "workspaceFolderCount != 0", 110 | "icon": "$(globe)" 111 | }, 112 | { 113 | "command": "devtrack.showSettings", 114 | "title": "DevTrack: Open Settings", 115 | "icon": "$(gear)" 116 | }, 117 | { 118 | "command": "devtrack.test", 119 | "title": "DevTrack: Test Extension Loading" 120 | } 121 | ], 122 | "menus": { 123 | "commandPalette": [ 124 | { 125 | "command": "devtrack.commitNow", 126 | "when": "devtrack:isTracking" 127 | }, 128 | { 129 | "command": "devtrack.pauseTracking", 130 | "when": "devtrack:isTracking" 131 | }, 132 | { 133 | "command": "devtrack.resumeTracking", 134 | "when": "!devtrack:isTracking && devtrack:isInitialized" 135 | } 136 | ] 137 | }, 138 | "configuration": { 139 | "type": "object", 140 | "title": "DevTrack Configuration", 141 | "properties": { 142 | "devtrack.repoName": { 143 | "type": "string", 144 | "default": "code-tracking", 145 | "description": "Name of the GitHub repository to store tracking logs." 146 | }, 147 | "devtrack.commitFrequency": { 148 | "type": "number", 149 | "default": 30, 150 | "minimum": 5, 151 | "maximum": 120, 152 | "description": "How often (in minutes) to commit your coding history." 153 | }, 154 | "devtrack.confirmBeforeCommit": { 155 | "type": "boolean", 156 | "default": true, 157 | "description": "Show a confirmation dialog before each automatic commit." 158 | }, 159 | "devtrack.exclude": { 160 | "type": "array", 161 | "items": { 162 | "type": "string" 163 | }, 164 | "default": [ 165 | "node_modules/**", 166 | ".git/**", 167 | "dist/**", 168 | "out/**", 169 | "build/**" 170 | ], 171 | "description": "List of glob patterns to exclude from tracking." 172 | }, 173 | "devtrack.enableAdaptiveScheduling": { 174 | "type": "boolean", 175 | "default": true, 176 | "description": "Automatically adjust commit schedule based on coding activity." 177 | }, 178 | "devtrack.minChangesForCommit": { 179 | "type": "number", 180 | "default": 1, 181 | "minimum": 1, 182 | "description": "Minimum number of file changes required to trigger a commit." 183 | }, 184 | "devtrack.minActiveTimeForCommit": { 185 | "type": "number", 186 | "default": 60, 187 | "minimum": 30, 188 | "description": "Minimum active coding time (in seconds) required to trigger a commit." 189 | }, 190 | "devtrack.trackLineChanges": { 191 | "type": "boolean", 192 | "default": true, 193 | "description": "Track number of lines changed per file." 194 | }, 195 | "devtrack.trackKeystrokes": { 196 | "type": "boolean", 197 | "default": true, 198 | "description": "Track number of keystrokes during coding sessions." 199 | }, 200 | "devtrack.maxIdleTimeBeforePause": { 201 | "type": "number", 202 | "default": 900, 203 | "minimum": 300, 204 | "description": "Maximum idle time (in seconds) before tracking is paused." 205 | }, 206 | "devtrack.showReportOnCommit": { 207 | "type": "boolean", 208 | "default": false, 209 | "description": "Show a brief report after each successful commit." 210 | } 211 | } 212 | } 213 | }, 214 | "scripts": { 215 | "vscode:prepublish": "npm run package", 216 | "compile": "npm run check-types && npm run lint && node esbuild.js", 217 | "build": "npm run compile && node esbuild.js", 218 | "watch": "npm-run-all -p watch:*", 219 | "watch:esbuild": "node esbuild.js --watch", 220 | "watch:tsc": "tsc --noEmit --watch --project tsconfig.json", 221 | "package": "npm run check-types && npm run lint && node esbuild.js --production", 222 | "compile-tests": "tsc -p . --outDir out", 223 | "watch-tests": "tsc -p . -w --outDir out", 224 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 225 | "check-types": "tsc --noEmit", 226 | "lint": "eslint src", 227 | "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", 228 | "test": "node ./out/test/runTest.js" 229 | }, 230 | "dependencies": { 231 | "@octokit/rest": "^21.0.2", 232 | "lucide-react": "^0.330.0", 233 | "minimatch": "^10.0.1", 234 | "react": "^18.2.0", 235 | "react-dom": "^18.2.0", 236 | "recharts": "^2.12.0", 237 | "simple-git": "^3.27.0" 238 | }, 239 | "devDependencies": { 240 | "@types/glob": "^8.1.0", 241 | "@types/minimatch": "^5.1.2", 242 | "@types/mocha": "^10.0.10", 243 | "@types/node": "^22.13.5", 244 | "@types/react": "^18.2.55", 245 | "@types/react-dom": "^18.2.19", 246 | "@types/vscode": "^1.96.0", 247 | "@typescript-eslint/eslint-plugin": "^8.18.0", 248 | "@typescript-eslint/parser": "^8.18.0", 249 | "@vscode/test-cli": "^0.0.10", 250 | "@vscode/test-electron": "^2.4.1", 251 | "@vscode/vsce": "^2.15.0", 252 | "esbuild": "^0.24.2", 253 | "eslint": "^9.19.0", 254 | "eslint-config-prettier": "^9.1.0", 255 | "eslint-plugin-prettier": "^5.2.1", 256 | "glob": "^10.4.5", 257 | "mocha": "^10.8.2", 258 | "npm-run-all": "^4.1.5", 259 | "prettier": "^3.4.2", 260 | "typescript": "^5.7.3", 261 | "vscode": "^1.1.37" 262 | }, 263 | "extensionDependencies": [ 264 | "vscode.git" 265 | ] 266 | } 267 | -------------------------------------------------------------------------------- /src/components/CodingStatsDashboard.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-unused-vars */ 3 | import React, { useState, useEffect } from 'react'; 4 | import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; 5 | import { 6 | LineChart, 7 | Line, 8 | XAxis, 9 | YAxis, 10 | CartesianGrid, 11 | Tooltip, 12 | Legend, 13 | ResponsiveContainer, 14 | BarChart, 15 | Bar, 16 | } from 'recharts'; 17 | import { 18 | Clock, 19 | FileCode, 20 | GitBranch, 21 | ArrowUpDown, 22 | Moon, 23 | Sun, 24 | } from 'lucide-react'; 25 | 26 | interface ActivityData { 27 | date: string; 28 | commits: number; 29 | filesChanged: number; 30 | linesChanged: number; 31 | } 32 | 33 | interface FileStats { 34 | type: string; 35 | count: number; 36 | } 37 | 38 | interface TimeDistribution { 39 | hour: string; 40 | changes: number; 41 | } 42 | 43 | // Declare global types for VSCode webview 44 | declare global { 45 | interface Window { 46 | vscode?: { 47 | postMessage: (message: { command: string; theme?: string }) => void; 48 | }; 49 | initialStats?: { 50 | activityTimeline: ActivityData[]; 51 | fileTypes: FileStats[]; 52 | timeDistribution: TimeDistribution[]; 53 | }; 54 | } 55 | } 56 | 57 | const CodingStatsDashboard = () => { 58 | const [activityData, setActivityData] = useState([]); 59 | const [fileStats, setFileStats] = useState([]); 60 | const [timeDistribution, setTimeDistribution] = useState( 61 | [] 62 | ); 63 | const [loading, setLoading] = useState(true); 64 | const [isDarkMode, setIsDarkMode] = useState(() => { 65 | // First check stored preference 66 | const stored = localStorage.getItem('devtrack-dashboard-theme'); 67 | if (stored) { 68 | return stored === 'dark'; 69 | } 70 | 71 | // Then check if VSCode is in dark mode 72 | if (window.vscode) { 73 | return document.body.classList.contains('vscode-dark'); 74 | } 75 | 76 | // Finally fallback to system preference 77 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 78 | }); 79 | 80 | useEffect(() => { 81 | // Save theme preference 82 | localStorage.setItem( 83 | 'devtrack-dashboard-theme', 84 | isDarkMode ? 'dark' : 'light' 85 | ); 86 | // Apply theme classes 87 | document.body.classList.toggle('dark', isDarkMode); 88 | if (window.vscode) { 89 | window.vscode?.postMessage({ 90 | command: 'themeChanged', 91 | theme: isDarkMode ? 'dark' : 'light', 92 | }); 93 | } 94 | }, [isDarkMode]); 95 | 96 | useEffect(() => { 97 | // Watch for VSCode theme changes 98 | const observer = new MutationObserver((mutations: MutationRecord[]) => { 99 | mutations.forEach((mutation: MutationRecord) => { 100 | if ((mutation.target as Element).classList.contains('vscode-dark')) { 101 | setIsDarkMode(true); 102 | } else if ( 103 | (mutation.target as Element).classList.contains('vscode-light') 104 | ) { 105 | setIsDarkMode(false); 106 | } 107 | }); 108 | }); 109 | 110 | observer.observe(document.body, { 111 | attributes: true, 112 | attributeFilter: ['class'], 113 | }); 114 | 115 | return () => observer.disconnect(); 116 | }, []); 117 | 118 | useEffect(() => { 119 | // Load initial stats from VSCode if available 120 | if (window.initialStats) { 121 | setActivityData(window.initialStats.activityTimeline || []); 122 | setFileStats(window.initialStats.fileTypes || []); 123 | setTimeDistribution(window.initialStats.timeDistribution || []); 124 | setLoading(false); 125 | } 126 | 127 | // Listen for stats updates from VSCode 128 | const messageHandler = (event: MessageEvent) => { 129 | const message = event.data as { 130 | command: string; 131 | stats?: { 132 | activityTimeline: Array; 133 | fileTypes: Array; 134 | timeDistribution: Array; 135 | }; 136 | }; 137 | if (message.command === 'updateStats' && message.stats) { 138 | setActivityData(message.stats.activityTimeline || []); 139 | setFileStats(message.stats.fileTypes || []); 140 | setTimeDistribution(message.stats.timeDistribution || []); 141 | } 142 | }; 143 | 144 | window.addEventListener('message', messageHandler); 145 | return () => window.removeEventListener('message', messageHandler); 146 | }, []); 147 | 148 | const themeColors = { 149 | text: isDarkMode ? 'text-gray-100' : 'text-gray-900', 150 | subtext: isDarkMode ? 'text-gray-300' : 'text-gray-500', 151 | background: isDarkMode ? 'bg-gray-900' : 'bg-white', 152 | cardBg: isDarkMode ? 'bg-gray-800' : 'bg-white', 153 | border: isDarkMode ? 'border-gray-700' : 'border-gray-200', 154 | chartColors: { 155 | grid: isDarkMode ? '#374151' : '#e5e7eb', 156 | text: isDarkMode ? '#e5e7eb' : '#4b5563', 157 | line1: isDarkMode ? '#93c5fd' : '#3b82f6', 158 | line2: isDarkMode ? '#86efac' : '#22c55e', 159 | line3: isDarkMode ? '#fde047' : '#eab308', 160 | bar: isDarkMode ? '#93c5fd' : '#3b82f6', 161 | }, 162 | }; 163 | 164 | if (loading) { 165 | return ( 166 |
169 |
Loading statistics...
170 |
171 | ); 172 | } 173 | 174 | return ( 175 |
178 | {/* Theme Toggle */} 179 |
180 | 193 |
194 | 195 | {/* Overview Cards */} 196 |
197 | 198 | 199 |
200 | 201 |
202 |

203 | Total Coding Hours 204 |

205 |

206 | 24.5 207 |

208 |
209 |
210 |
211 |
212 | 213 | 214 | 215 |
216 | 217 |
218 |

219 | Files Modified 220 |

221 |

54

222 |
223 |
224 |
225 |
226 | 227 | 228 | 229 |
230 | 231 |
232 |

233 | Total Commits 234 |

235 |

82

236 |
237 |
238 |
239 |
240 | 241 | 242 | 243 |
244 | 245 |
246 |

247 | Lines Changed 248 |

249 |

250 | 1,146 251 |

252 |
253 |
254 |
255 |
256 |
257 | 258 | {/* Activity Timeline */} 259 | 260 | 261 | 262 | Coding Activity Timeline 263 | 264 | 265 | 266 |
267 | 268 | 269 | 273 | 278 | 283 | 289 | 297 | 300 | 307 | 314 | 321 | 322 | 323 |
324 |
325 |
326 | 327 | {/* File Type Distribution */} 328 | 329 | 330 | 331 | File Type Distribution 332 | 333 | 334 | 335 |
336 | 337 | 338 | 342 | 347 | 351 | 359 | 362 | 367 | 368 | 369 |
370 |
371 |
372 | 373 | {/* Daily Distribution */} 374 | 375 | 376 | 377 | Daily Coding Distribution 378 | 379 | 380 | 381 |
382 | 383 | 384 | 388 | 393 | 397 | 405 | 408 | 413 | 414 | 415 |
416 |
417 |
418 |
419 | ); 420 | }; 421 | 422 | export default CodingStatsDashboard; 423 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CardProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export function Card({ 9 | children, 10 | className = '', 11 | ...props 12 | }: CardProps & React.ComponentProps<'div'>) { 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | } 19 | 20 | interface CardHeaderProps { 21 | children: React.ReactNode; 22 | className?: string; 23 | } 24 | 25 | export function CardHeader({ 26 | children, 27 | className = '', 28 | ...props 29 | }: CardHeaderProps & React.ComponentProps<'div'>) { 30 | return ( 31 |
32 | {children} 33 |
34 | ); 35 | } 36 | 37 | interface CardTitleProps { 38 | children: React.ReactNode; 39 | className?: string; 40 | } 41 | 42 | export function CardTitle({ 43 | children, 44 | className = '', 45 | ...props 46 | }: CardTitleProps & React.ComponentProps<'h3'>) { 47 | return ( 48 |

52 | {children} 53 |

54 | ); 55 | } 56 | 57 | interface CardContentProps { 58 | children: React.ReactNode; 59 | className?: string; 60 | } 61 | 62 | export function CardContent({ 63 | children, 64 | className = '', 65 | ...props 66 | }: CardContentProps & React.ComponentProps<'div'>) { 67 | return ( 68 |
69 | {children} 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import { execSync } from 'child_process'; 6 | import { Buffer } from 'node:buffer'; 7 | import { GitHubService } from './services/githubService'; 8 | import { GitService } from './services/gitService'; 9 | import { Tracker } from './services/tracker'; 10 | import { SummaryGenerator } from './services/summaryGenerator'; 11 | import { Scheduler } from './services/scheduler'; 12 | import { ChangeAnalyzer } from './services/changeAnalyzer'; 13 | import { platform, homedir } from 'os'; 14 | import { StatisticsProvider } from './services/statisticsProvider'; 15 | import { StatisticsView } from './services/statisticsView'; 16 | 17 | // Interfaces 18 | interface PersistedAuthState { 19 | username?: string; 20 | repoName?: string; 21 | lastWorkspace?: string; 22 | } 23 | 24 | interface DevTrackServices { 25 | outputChannel: vscode.OutputChannel; 26 | githubService: GitHubService; 27 | gitService: GitService; 28 | tracker: Tracker; 29 | summaryGenerator: SummaryGenerator; 30 | scheduler: Scheduler | null; 31 | trackingStatusBar: vscode.StatusBarItem; 32 | authStatusBar: vscode.StatusBarItem; 33 | countdownStatusBar: vscode.StatusBarItem; 34 | extensionContext: vscode.ExtensionContext; 35 | changeAnalyzer: ChangeAnalyzer; 36 | statisticsProvider?: StatisticsProvider; 37 | statisticsView?: StatisticsView; 38 | } 39 | 40 | // Extension Activation 41 | export async function activate( 42 | context: vscode.ExtensionContext 43 | ): Promise { 44 | // Create output channel first to be used throughout 45 | const channel = vscode.window.createOutputChannel('DevTrack'); 46 | context.subscriptions.push(channel); 47 | channel.appendLine('DevTrack: Extension activating...'); 48 | 49 | try { 50 | // Register test command 51 | const testCommand = vscode.commands.registerCommand('devtrack.test', () => { 52 | vscode.window.showInformationMessage( 53 | 'DevTrack Debug Version: Test Command Executed' 54 | ); 55 | }); 56 | context.subscriptions.push(testCommand); 57 | 58 | // Initialize services with the created output channel 59 | const services = await initializeServices(context, channel); 60 | if (!services) { 61 | return; 62 | } 63 | 64 | // Register commands and setup handlers 65 | await registerCommands(context, services); 66 | // Also register website commands 67 | await registerWebsiteCommands(context, services); 68 | // Register dashboard and statistics commands 69 | await registerDashboardCommands(context, services); 70 | setupConfigurationHandling(services); 71 | showWelcomeMessage(context, services); 72 | 73 | channel.appendLine('DevTrack: Extension activated successfully'); 74 | } catch (error) { 75 | channel.appendLine(`DevTrack: Activation error - ${error}`); 76 | vscode.window.showErrorMessage('DevTrack: Failed to activate extension'); 77 | } 78 | } 79 | 80 | // Services Initialization 81 | async function initializeServices( 82 | context: vscode.ExtensionContext, 83 | channel: vscode.OutputChannel 84 | ): Promise { 85 | try { 86 | // Use os.homedir() instead of process.env 87 | const homeDir = homedir(); 88 | if (!homeDir) { 89 | throw new Error('Unable to determine home directory'); 90 | } 91 | 92 | // Create tracking directory structure 93 | const trackingBase = path.join(homeDir, '.devtrack', 'tracking'); 94 | await fs.promises.mkdir(trackingBase, { recursive: true }); 95 | 96 | // Create workspace-specific tracking directory 97 | const workspaceId = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath 98 | ? Buffer.from(vscode.workspace.workspaceFolders[0].uri.fsPath) 99 | .toString('base64') 100 | .replace(/[/+=]/g, '_') 101 | : 'default'; 102 | const trackingDir = path.join(trackingBase, workspaceId); 103 | await fs.promises.mkdir(trackingDir, { recursive: true }); 104 | 105 | // Initialize services 106 | const services: DevTrackServices = { 107 | outputChannel: channel, 108 | githubService: new GitHubService(channel), 109 | gitService: new GitService(channel), 110 | tracker: new Tracker(channel, trackingDir), 111 | summaryGenerator: new SummaryGenerator(channel, context), 112 | scheduler: null, 113 | trackingStatusBar: createStatusBarItem('tracking'), 114 | authStatusBar: createStatusBarItem('auth'), 115 | countdownStatusBar: createStatusBarItem('countdown'), 116 | extensionContext: context, 117 | changeAnalyzer: new ChangeAnalyzer(channel), 118 | }; 119 | 120 | // Initialize statistics provider if available 121 | try { 122 | services.statisticsProvider = new StatisticsProvider( 123 | channel, 124 | trackingDir 125 | ); 126 | services.statisticsView = new StatisticsView( 127 | context, 128 | services.statisticsProvider, 129 | channel, 130 | trackingDir 131 | ); 132 | channel.appendLine('DevTrack: Statistics services initialized'); 133 | } catch (error) { 134 | channel.appendLine( 135 | `DevTrack: Statistics initialization error - ${error}` 136 | ); 137 | // Continue without statistics (non-fatal) 138 | } 139 | 140 | // Add status bars to subscriptions and show them 141 | context.subscriptions.push( 142 | services.trackingStatusBar, 143 | services.authStatusBar, 144 | services.countdownStatusBar 145 | ); 146 | services.trackingStatusBar.show(); 147 | services.authStatusBar.show(); 148 | // Countdown bar shows only when tracking is active 149 | 150 | // Try to restore authentication state 151 | await restoreAuthenticationState(context, services); 152 | 153 | return services; 154 | } catch (error) { 155 | channel.appendLine(`DevTrack: Service initialization error - ${error}`); 156 | return null; 157 | } 158 | } 159 | 160 | // Register dashboard commands 161 | async function registerDashboardCommands( 162 | context: vscode.ExtensionContext, 163 | services: DevTrackServices 164 | ): Promise { 165 | // Show dashboard command 166 | const showDashboardCommand = vscode.commands.registerCommand( 167 | 'devtrack.showDashboard', 168 | async () => { 169 | if (services.statisticsView) { 170 | services.statisticsView.show(); 171 | } else { 172 | vscode.window.showErrorMessage( 173 | 'DevTrack: Statistics view not available' 174 | ); 175 | } 176 | } 177 | ); 178 | context.subscriptions.push(showDashboardCommand); 179 | 180 | // Generate report command 181 | const generateReportCommand = vscode.commands.registerCommand( 182 | 'devtrack.generateReport', 183 | async () => { 184 | if (!services.statisticsProvider) { 185 | vscode.window.showErrorMessage( 186 | 'DevTrack: Statistics provider not available' 187 | ); 188 | return; 189 | } 190 | 191 | try { 192 | vscode.window.withProgress( 193 | { 194 | location: vscode.ProgressLocation.Notification, 195 | title: 'DevTrack: Generating coding report', 196 | cancellable: false, 197 | }, 198 | async (progress) => { 199 | progress.report({ message: 'Collecting statistics...' }); 200 | const stats = await services.statisticsProvider?.getStatistics(); 201 | 202 | if (!stats) { 203 | throw new Error('Failed to retrieve statistics'); 204 | } 205 | 206 | // Create report markdown 207 | const reportContent = `# DevTrack Coding Report 208 | 209 | ## Summary 210 | - **Total Coding Time:** ${Math.round(stats.totalTime)} hours 211 | - **Files Modified:** ${stats.filesModified} 212 | - **Total Commits:** ${stats.totalCommits} 213 | - **Lines Changed:** ${stats.linesChanged.toLocaleString()} 214 | 215 | ## File Type Distribution 216 | ${ 217 | stats.fileTypes && stats.fileTypes.length > 0 218 | ? stats.fileTypes 219 | .slice(0, 5) 220 | .map((ft) => `- **${ft.name || 'Unknown'}:** ${ft.count} files`) 221 | .join('\n') 222 | : '- No file type data available' 223 | } 224 | 225 | ## Most Active Times 226 | ${ 227 | stats.timeDistribution && stats.timeDistribution.length > 0 228 | ? [...stats.timeDistribution] 229 | .sort((a, b) => b.changes - a.changes) 230 | .slice(0, 3) 231 | .map((td) => `- **${td.hour}:** ${td.changes} changes`) 232 | .join('\n') 233 | : '- No activity time data available' 234 | } 235 | 236 | *Generated by DevTrack on ${new Date().toLocaleDateString()}* 237 | `; 238 | 239 | // Show the report in a new editor 240 | const doc = await vscode.workspace.openTextDocument({ 241 | content: reportContent, 242 | language: 'markdown', 243 | }); 244 | await vscode.window.showTextDocument(doc); 245 | 246 | progress.report({ message: 'Done' }); 247 | } 248 | ); 249 | } catch (error) { 250 | services.outputChannel.appendLine( 251 | `DevTrack: Error generating report - ${error}` 252 | ); 253 | vscode.window.showErrorMessage('DevTrack: Failed to generate report'); 254 | } 255 | } 256 | ); 257 | context.subscriptions.push(generateReportCommand); 258 | } 259 | 260 | async function registerWebsiteCommands( 261 | context: vscode.ExtensionContext, 262 | services: DevTrackServices 263 | ): Promise { 264 | // Register command to manually generate the website 265 | const generateWebsiteCommand = vscode.commands.registerCommand( 266 | 'devtrack.generateWebsite', 267 | async () => { 268 | try { 269 | vscode.window.withProgress( 270 | { 271 | location: vscode.ProgressLocation.Notification, 272 | title: 'DevTrack: Generating statistics website', 273 | cancellable: false, 274 | }, 275 | async (progress) => { 276 | progress.report({ message: 'Initializing...' }); 277 | 278 | // Import WebsiteGenerator dynamically to avoid circular dependencies 279 | const { WebsiteGenerator } = await import( 280 | './services/websiteGenerator' 281 | ); 282 | 283 | // Get home directory and tracking path 284 | const homeDir = homedir(); 285 | 286 | // Create workspace-specific tracking directory 287 | const workspaceId = vscode.workspace.workspaceFolders?.[0]?.uri 288 | .fsPath 289 | ? Buffer.from(vscode.workspace.workspaceFolders[0].uri.fsPath) 290 | .toString('base64') 291 | .replace(/[/+=]/g, '_') 292 | : 'default'; 293 | const trackingDir = path.join( 294 | homeDir, 295 | '.devtrack', 296 | 'tracking', 297 | workspaceId 298 | ); 299 | 300 | progress.report({ message: 'Generating website files...' }); 301 | 302 | // Create website generator 303 | const websiteGenerator = new WebsiteGenerator( 304 | services.outputChannel, 305 | trackingDir 306 | ); 307 | await websiteGenerator.generateWebsite(); 308 | 309 | progress.report({ message: 'Committing changes...' }); 310 | 311 | // Instead of directly accessing git, use commitAndPush method 312 | await services.gitService.commitAndPush( 313 | 'DevTrack: Update statistics website' 314 | ); 315 | 316 | progress.report({ message: 'Done' }); 317 | 318 | // Show success message with GitHub Pages URL 319 | const username = await services.githubService.getUsername(); 320 | const config = vscode.workspace.getConfiguration('devtrack'); 321 | const repoName = config.get('repoName') || 'code-tracking'; 322 | 323 | if (username) { 324 | const pagesUrl = `https://${username}.github.io/${repoName}/`; 325 | 326 | const viewWebsite = 'View Website'; 327 | vscode.window 328 | .showInformationMessage( 329 | `DevTrack: Statistics website generated and pushed to GitHub. It should be available soon at ${pagesUrl}`, 330 | viewWebsite 331 | ) 332 | .then((selection) => { 333 | if (selection === viewWebsite) { 334 | vscode.env.openExternal(vscode.Uri.parse(pagesUrl)); 335 | } 336 | }); 337 | } else { 338 | vscode.window.showInformationMessage( 339 | 'DevTrack: Statistics website generated and pushed to GitHub. It should be available soon.' 340 | ); 341 | } 342 | } 343 | ); 344 | } catch (error: any) { 345 | services.outputChannel.appendLine( 346 | `DevTrack: Failed to generate website - ${error.message}` 347 | ); 348 | vscode.window.showErrorMessage( 349 | `DevTrack: Failed to generate website - ${error.message}` 350 | ); 351 | } 352 | } 353 | ); 354 | 355 | context.subscriptions.push(generateWebsiteCommand); 356 | } 357 | 358 | // Status Bar Creation 359 | function createStatusBarItem( 360 | type: 'tracking' | 'auth' | 'countdown' 361 | ): vscode.StatusBarItem { 362 | let priority = 100; 363 | if (type === 'tracking') { 364 | priority = 100; 365 | } 366 | if (type === 'auth') { 367 | priority = 101; 368 | } 369 | if (type === 'countdown') { 370 | priority = 99; 371 | } 372 | 373 | const item = vscode.window.createStatusBarItem( 374 | vscode.StatusBarAlignment.Right, 375 | priority 376 | ); 377 | 378 | if (type === 'tracking') { 379 | item.text = '$(circle-slash) DevTrack: Stopped'; 380 | item.tooltip = 'Click to start/stop tracking'; 381 | item.command = 'devtrack.startTracking'; 382 | } else if (type === 'auth') { 383 | item.text = '$(mark-github) DevTrack: Not Connected'; 384 | item.tooltip = 'Click to connect to GitHub'; 385 | item.command = 'devtrack.login'; 386 | } else if (type === 'countdown') { 387 | item.text = '$(clock)'; 388 | item.tooltip = 'Time until next DevTrack commit'; 389 | item.command = 'devtrack.commitNow'; 390 | // Don't show by default - only when active 391 | item.hide(); 392 | } 393 | 394 | return item; 395 | } 396 | 397 | // Authentication State Management 398 | async function restoreAuthenticationState( 399 | context: vscode.ExtensionContext, 400 | services: DevTrackServices 401 | ): Promise { 402 | try { 403 | const persistedState = 404 | context.globalState.get('devtrackAuthState'); 405 | if (!persistedState?.username) { 406 | return false; 407 | } 408 | 409 | const session = await vscode.authentication.getSession( 410 | 'github', 411 | ['repo', 'read:user'], 412 | { 413 | createIfNone: false, 414 | silent: true, 415 | } 416 | ); 417 | 418 | if (session) { 419 | services.githubService.setToken(session.accessToken); 420 | const username = await services.githubService.getUsername(); 421 | 422 | if (username === persistedState.username) { 423 | const repoName = persistedState.repoName || 'code-tracking'; 424 | const remoteUrl = `https://github.com/${username}/${repoName}.git`; 425 | 426 | await services.gitService.ensureRepoSetup(remoteUrl); 427 | await initializeTracker(services); 428 | 429 | updateStatusBar(services, 'auth', true); 430 | updateStatusBar(services, 'tracking', true); 431 | 432 | services.outputChannel.appendLine( 433 | 'DevTrack: Successfully restored authentication state' 434 | ); 435 | return true; 436 | } 437 | } 438 | } catch (error) { 439 | services.outputChannel.appendLine( 440 | `DevTrack: Error restoring auth state - ${error}` 441 | ); 442 | } 443 | return false; 444 | } 445 | 446 | // Command Registration 447 | async function registerCommands( 448 | context: vscode.ExtensionContext, 449 | services: DevTrackServices 450 | ): Promise { 451 | const commands = [ 452 | { 453 | command: 'devtrack.startTracking', 454 | callback: () => handleStartTracking(services), 455 | }, 456 | { 457 | command: 'devtrack.stopTracking', 458 | callback: () => handleStopTracking(services), 459 | }, 460 | { 461 | command: 'devtrack.login', 462 | callback: () => handleLogin(services), 463 | }, 464 | { 465 | command: 'devtrack.logout', 466 | callback: () => handleLogout(services), 467 | }, 468 | { 469 | command: 'devtrack.showGitGuide', 470 | callback: () => GitInstallationHandler.showInstallationGuide(), 471 | }, 472 | { 473 | command: 'devtrack.openFolder', 474 | callback: () => vscode.commands.executeCommand('vscode.openFolder'), 475 | }, 476 | // New commands 477 | { 478 | command: 'devtrack.commitNow', 479 | callback: () => handleCommitNow(services), 480 | }, 481 | { 482 | command: 'devtrack.pauseTracking', 483 | callback: () => handlePauseTracking(services), 484 | }, 485 | { 486 | command: 'devtrack.resumeTracking', 487 | callback: () => handleResumeTracking(services), 488 | }, 489 | { 490 | command: 'devtrack.showSettings', 491 | callback: () => handleShowSettings(), 492 | }, 493 | ]; 494 | 495 | commands.forEach(({ command, callback }) => { 496 | context.subscriptions.push( 497 | vscode.commands.registerCommand(command, callback) 498 | ); 499 | }); 500 | } 501 | 502 | // New command handlers 503 | async function handleCommitNow(services: DevTrackServices): Promise { 504 | if (!services.scheduler) { 505 | vscode.window.showErrorMessage('DevTrack: Tracking not active'); 506 | return; 507 | } 508 | 509 | await services.scheduler.commitNow(); 510 | } 511 | 512 | async function handlePauseTracking(services: DevTrackServices): Promise { 513 | if (!services.scheduler) { 514 | vscode.window.showErrorMessage('DevTrack: Tracking not active'); 515 | return; 516 | } 517 | 518 | services.tracker.stopTracking(); 519 | updateStatusBar(services, 'tracking', false); 520 | services.countdownStatusBar.hide(); 521 | vscode.window.showInformationMessage('DevTrack: Tracking paused'); 522 | } 523 | 524 | async function handleResumeTracking(services: DevTrackServices): Promise { 525 | if (!services.scheduler) { 526 | await handleStartTracking(services); 527 | return; 528 | } 529 | 530 | services.tracker.startTracking(); 531 | updateStatusBar(services, 'tracking', true); 532 | services.countdownStatusBar.show(); 533 | vscode.window.showInformationMessage('DevTrack: Tracking resumed'); 534 | } 535 | 536 | function handleShowSettings(): void { 537 | vscode.commands.executeCommand('workbench.action.openSettings', 'devtrack'); 538 | } 539 | 540 | // Command Handlers 541 | async function handleStartTracking(services: DevTrackServices): Promise { 542 | try { 543 | if (!vscode.workspace.workspaceFolders?.length) { 544 | throw new Error( 545 | 'Please open a folder or workspace before starting tracking.' 546 | ); 547 | } 548 | 549 | const gitInstalled = await GitInstallationHandler.checkGitInstallation( 550 | services.outputChannel 551 | ); 552 | if (!gitInstalled) { 553 | return; 554 | } 555 | 556 | if (services.scheduler) { 557 | services.scheduler.start(); 558 | services.tracker.startTracking(); // Make sure tracker is also tracking 559 | updateStatusBar(services, 'tracking', true); 560 | services.countdownStatusBar.show(); // Show countdown 561 | vscode.window.showInformationMessage('DevTrack: Tracking started.'); 562 | } else { 563 | const response = await vscode.window.showInformationMessage( 564 | 'DevTrack needs to be set up before starting. Would you like to set it up now?', 565 | 'Set Up DevTrack', 566 | 'Cancel' 567 | ); 568 | 569 | if (response === 'Set Up DevTrack') { 570 | await initializeDevTrack(services); 571 | } 572 | } 573 | } catch (error: unknown) { 574 | handleError(services, 'Error starting tracking', error as Error); 575 | } 576 | } 577 | 578 | async function handleStopTracking(services: DevTrackServices): Promise { 579 | if (services.scheduler) { 580 | services.scheduler.stop(); 581 | services.tracker.stopTracking(); // Stop the tracker too 582 | updateStatusBar(services, 'tracking', false); 583 | services.countdownStatusBar.hide(); // Hide countdown 584 | vscode.window.showInformationMessage('DevTrack: Tracking stopped.'); 585 | } else { 586 | vscode.window.showErrorMessage('DevTrack: Please connect to GitHub first.'); 587 | } 588 | } 589 | 590 | async function handleLogin(services: DevTrackServices): Promise { 591 | try { 592 | services.outputChannel.appendLine('DevTrack: Starting login process...'); 593 | 594 | const session = await vscode.authentication.getSession( 595 | 'github', 596 | ['repo', 'read:user'], 597 | { createIfNone: true } 598 | ); 599 | 600 | if (session) { 601 | services.githubService.setToken(session.accessToken); 602 | await initializeDevTrack(services); 603 | } else { 604 | vscode.window.showInformationMessage( 605 | 'DevTrack: GitHub connection was cancelled.' 606 | ); 607 | } 608 | } catch (error: any) { 609 | handleError(services, 'Login failed', error); 610 | } 611 | } 612 | 613 | async function handleLogout(services: DevTrackServices): Promise { 614 | const confirm = await vscode.window.showWarningMessage( 615 | 'Are you sure you want to logout from DevTrack?', 616 | { modal: true }, 617 | 'Yes', 618 | 'No' 619 | ); 620 | 621 | if (confirm !== 'Yes') { 622 | return; 623 | } 624 | 625 | try { 626 | cleanup(services); 627 | await services.extensionContext.globalState.update( 628 | 'devtrackAuthState', 629 | undefined 630 | ); 631 | vscode.window.showInformationMessage('DevTrack: Successfully logged out.'); 632 | 633 | const loginChoice = await vscode.window.showInformationMessage( 634 | 'Would you like to log in with a different account?', 635 | 'Yes', 636 | 'No' 637 | ); 638 | 639 | if (loginChoice === 'Yes') { 640 | await vscode.commands.executeCommand('devtrack.login'); 641 | } 642 | } catch (error: any) { 643 | handleError(services, 'Logout failed', error); 644 | } 645 | } 646 | 647 | // GitInstallationHandler Class 648 | class GitInstallationHandler { 649 | private static readonly DOWNLOAD_URLS = { 650 | win32: 'https://git-scm.com/download/win', 651 | darwin: 'https://git-scm.com/download/mac', 652 | linux: 'https://git-scm.com/download/linux', 653 | }; 654 | 655 | static async checkGitInstallation( 656 | outputChannel: vscode.OutputChannel 657 | ): Promise { 658 | try { 659 | const gitVersion = execSync('git --version', { encoding: 'utf8' }); 660 | outputChannel.appendLine(`DevTrack: Git found - ${gitVersion.trim()}`); 661 | return true; 662 | } catch { 663 | const currentPlatform = platform(); 664 | const response = await vscode.window.showErrorMessage( 665 | 'Git is required but not found on your system.', 666 | { 667 | modal: true, 668 | detail: 'Would you like to view the installation guide?', 669 | }, 670 | 'Show Installation Guide', 671 | ...(currentPlatform === 'win32' ? ['Fix PATH Issue'] : []), 672 | 'Cancel' 673 | ); 674 | 675 | if (response === 'Show Installation Guide') { 676 | this.showInstallationGuide(); 677 | } else if (response === 'Fix PATH Issue') { 678 | this.showPathFixGuide(); 679 | } 680 | return false; 681 | } 682 | } 683 | 684 | public static showInstallationGuide(): void { 685 | const panel = vscode.window.createWebviewPanel( 686 | 'gitInstallGuide', 687 | 'Git Installation Guide', 688 | vscode.ViewColumn.One, 689 | { enableScripts: true } 690 | ); 691 | 692 | const currentPlatform = platform(); 693 | const downloadUrl = 694 | this.DOWNLOAD_URLS[currentPlatform as keyof typeof this.DOWNLOAD_URLS]; 695 | const instructions = this.getInstructions(currentPlatform); 696 | panel.webview.html = this.getWebviewContent(instructions, downloadUrl); 697 | } 698 | 699 | private static showPathFixGuide(): void { 700 | const panel = vscode.window.createWebviewPanel( 701 | 'gitPathGuide', 702 | 'Fix Git PATH Issue', 703 | vscode.ViewColumn.One, 704 | { enableScripts: true } 705 | ); 706 | 707 | panel.webview.html = ` 708 | 709 | 710 | 715 | 716 | 717 |

Adding Git to System PATH

718 |
⚠️ Ensure Git is installed before proceeding.
719 |
720 |

Steps:

721 |
    722 |
  1. Open System Properties (Windows + R, type sysdm.cpl)
  2. 723 |
  3. Go to Advanced tab
  4. 724 |
  5. Click Environment Variables
  6. 725 |
  7. Under System Variables, find and select Path
  8. 726 |
  9. Click Edit
  10. 727 |
  11. Add Git paths: 728 |
      729 |
    • C:\\Program Files\\Git\\cmd
    • 730 |
    • C:\\Program Files\\Git\\bin
    • 731 |
    732 |
  12. 733 |
  13. Click OK on all windows
  14. 734 |
  15. Restart VS Code
  16. 735 |
736 |
737 | 738 | `; 739 | } 740 | 741 | private static getInstructions(platform: string): string { 742 | const instructions: Record = { 743 | win32: `Windows Installation: 744 | 1. Download Git from ${this.DOWNLOAD_URLS.win32} 745 | 2. Run installer 746 | 3. Select "Git from command line and 3rd-party software" 747 | 4. Select "Use Windows' default console" 748 | 5. Enable Git Credential Manager 749 | 6. Complete installation 750 | 7. Open new terminal and verify with 'git --version'`, 751 | darwin: `Mac Installation: 752 | Option 1 (Homebrew): 753 | 1. Install Homebrew: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 754 | 2. Run: brew install git 755 | 756 | Option 2 (Direct): 757 | 1. Download from ${this.DOWNLOAD_URLS.darwin} 758 | 2. Install the package`, 759 | linux: `Linux Installation: 760 | Debian/Ubuntu: 761 | 1. sudo apt-get update 762 | 2. sudo apt-get install git 763 | 764 | Fedora: 765 | 1. sudo dnf install git`, 766 | }; 767 | 768 | return instructions[platform] || instructions.linux; 769 | } 770 | 771 | private static getWebviewContent( 772 | instructions: string, 773 | downloadUrl: string 774 | ): string { 775 | return ` 776 | 777 | 778 | 791 | 792 | 793 |

Git Installation Guide

794 |
${instructions}
795 | Download Git 796 | 797 | `; 798 | } 799 | } 800 | 801 | // DevTrack Initialization 802 | async function initializeDevTrack(services: DevTrackServices): Promise { 803 | try { 804 | services.outputChannel.appendLine('DevTrack: Starting initialization...'); 805 | 806 | // Verify Git installation 807 | if ( 808 | !(await GitInstallationHandler.checkGitInstallation( 809 | services.outputChannel 810 | )) 811 | ) { 812 | throw new Error( 813 | 'Git must be installed before DevTrack can be initialized.' 814 | ); 815 | } 816 | 817 | // Get GitHub session 818 | const session = await vscode.authentication.getSession( 819 | 'github', 820 | ['repo', 'read:user'], 821 | { createIfNone: true } 822 | ); 823 | 824 | if (!session) { 825 | throw new Error('GitHub authentication is required to use DevTrack.'); 826 | } 827 | 828 | // Initialize GitHub service 829 | services.githubService.setToken(session.accessToken); 830 | const username = await services.githubService.getUsername(); 831 | 832 | if (!username) { 833 | throw new Error('Unable to retrieve GitHub username.'); 834 | } 835 | 836 | // Setup repository 837 | const config = vscode.workspace.getConfiguration('devtrack'); 838 | const repoName = config.get('repoName') || 'code-tracking'; 839 | const remoteUrl = `https://github.com/${username}/${repoName}.git`; 840 | 841 | // Create repository if it doesn't exist 842 | const repoExists = await services.githubService.repoExists(repoName); 843 | if (!repoExists) { 844 | const createdRepoUrl = await services.githubService.createRepo(repoName); 845 | if (!createdRepoUrl) { 846 | throw new Error('Failed to create GitHub repository.'); 847 | } 848 | } 849 | 850 | // Initialize Git repository 851 | await services.gitService.ensureRepoSetup(remoteUrl); 852 | 853 | // Initialize tracker 854 | await initializeTracker(services); 855 | 856 | // Update UI and persist state 857 | updateStatusBar(services, 'auth', true); 858 | updateStatusBar(services, 'tracking', true); 859 | services.countdownStatusBar.show(); 860 | 861 | await services.extensionContext.globalState.update('devtrackAuthState', { 862 | username, 863 | repoName, 864 | lastWorkspace: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath, 865 | }); 866 | 867 | services.outputChannel.appendLine( 868 | 'DevTrack: Initialization completed successfully' 869 | ); 870 | vscode.window.showInformationMessage( 871 | 'DevTrack has been set up successfully and tracking has started.' 872 | ); 873 | } catch (error: any) { 874 | handleError(services, 'Initialization failed', error); 875 | throw error; 876 | } 877 | } 878 | 879 | // Tracker Initialization 880 | async function initializeTracker(services: DevTrackServices): Promise { 881 | const config = vscode.workspace.getConfiguration('devtrack'); 882 | const commitFrequency = config.get('commitFrequency') || 30; 883 | 884 | services.scheduler = new Scheduler( 885 | commitFrequency, 886 | services.tracker, 887 | services.summaryGenerator, 888 | services.gitService, 889 | services.outputChannel 890 | ); 891 | 892 | // Start tracker 893 | services.tracker.startTracking(); 894 | services.scheduler.start(); 895 | services.countdownStatusBar.show(); 896 | 897 | services.outputChannel.appendLine( 898 | `DevTrack: Tracker initialized with ${commitFrequency} minute intervals` 899 | ); 900 | } 901 | 902 | // Configuration Handling 903 | function setupConfigurationHandling(services: DevTrackServices): void { 904 | vscode.workspace.onDidChangeConfiguration((event) => { 905 | if (event.affectsConfiguration('devtrack')) { 906 | handleConfigurationChange(services); 907 | } 908 | }); 909 | } 910 | 911 | async function handleConfigurationChange( 912 | services: DevTrackServices 913 | ): Promise { 914 | try { 915 | const config = vscode.workspace.getConfiguration('devtrack'); 916 | 917 | // Update commit frequency if scheduler exists 918 | if (services.scheduler) { 919 | const newFrequency = config.get('commitFrequency') || 30; 920 | services.scheduler.updateFrequency(newFrequency); 921 | services.outputChannel.appendLine( 922 | `DevTrack: Updated commit frequency to ${newFrequency} minutes` 923 | ); 924 | 925 | // Update scheduler options 926 | const minChangesForCommit = 927 | config.get('minChangesForCommit') || 1; 928 | const minActiveTimeForCommit = 929 | config.get('minActiveTimeForCommit') || 60; 930 | const enableAdaptiveScheduling = 931 | config.get('enableAdaptiveScheduling') || true; 932 | 933 | services.scheduler.updateOptions({ 934 | minChangesForCommit, 935 | minActiveTimeForCommit, 936 | enableAdaptiveScheduling, 937 | }); 938 | } 939 | 940 | // Update exclude patterns 941 | const newExcludePatterns = config.get('exclude') || []; 942 | services.tracker.updateExcludePatterns(newExcludePatterns); 943 | services.outputChannel.appendLine('DevTrack: Updated exclude patterns'); 944 | } catch (error: any) { 945 | handleError(services, 'Configuration update failed', error); 946 | } 947 | } 948 | 949 | // UI Updates 950 | function updateStatusBar( 951 | services: DevTrackServices, 952 | type: 'tracking' | 'auth', 953 | active: boolean 954 | ): void { 955 | if (type === 'tracking') { 956 | services.trackingStatusBar.text = active 957 | ? '$(clock) DevTrack: Tracking' 958 | : '$(circle-slash) DevTrack: Stopped'; 959 | services.trackingStatusBar.tooltip = active 960 | ? 'Click to stop tracking' 961 | : 'Click to start tracking'; 962 | services.trackingStatusBar.command = active 963 | ? 'devtrack.stopTracking' 964 | : 'devtrack.startTracking'; 965 | } else { 966 | services.authStatusBar.text = active 967 | ? '$(check) DevTrack: Connected' 968 | : '$(mark-github) DevTrack: Not Connected'; 969 | services.authStatusBar.tooltip = active 970 | ? 'Click to logout' 971 | : 'Click to connect to GitHub'; 972 | services.authStatusBar.command = active 973 | ? 'devtrack.logout' 974 | : 'devtrack.login'; 975 | } 976 | } 977 | 978 | // Error Handling 979 | function handleError( 980 | services: DevTrackServices, 981 | context: string, 982 | error: Error 983 | ): void { 984 | const message = error.message || 'An unknown error occurred'; 985 | services.outputChannel.appendLine(`DevTrack: ${context} - ${message}`); 986 | vscode.window.showErrorMessage(`DevTrack: ${message}`); 987 | } 988 | 989 | // Cleanup 990 | function cleanup(services: DevTrackServices): void { 991 | try { 992 | services.githubService.setToken(''); 993 | if (services.scheduler) { 994 | services.scheduler.stop(); 995 | services.scheduler = null; 996 | } 997 | services.tracker.stopTracking(); 998 | updateStatusBar(services, 'auth', false); 999 | updateStatusBar(services, 'tracking', false); 1000 | services.countdownStatusBar.hide(); 1001 | services.outputChannel.appendLine('DevTrack: Cleaned up services'); 1002 | } catch (error: any) { 1003 | services.outputChannel.appendLine( 1004 | `DevTrack: Cleanup error - ${error.message}` 1005 | ); 1006 | } 1007 | } 1008 | 1009 | // Welcome Message 1010 | function showWelcomeMessage( 1011 | context: vscode.ExtensionContext, 1012 | services: DevTrackServices 1013 | ): void { 1014 | if (!context.globalState.get('devtrackWelcomeShown')) { 1015 | const message = 1016 | 'Welcome to DevTrack! Would you like to set up automatic code tracking?'; 1017 | 1018 | vscode.window 1019 | .showInformationMessage(message, 'Get Started', 'Learn More', 'Later') 1020 | .then((selection) => { 1021 | if (selection === 'Get Started') { 1022 | vscode.commands.executeCommand('devtrack.login'); 1023 | } else if (selection === 'Learn More') { 1024 | showWelcomeInfo(services.outputChannel); 1025 | } 1026 | }); 1027 | 1028 | context.globalState.update('devtrackWelcomeShown', true); 1029 | } 1030 | } 1031 | 1032 | function showWelcomeInfo(outputChannel: vscode.OutputChannel): void { 1033 | const welcomeMessage = ` 1034 | DevTrack helps you monitor your coding activity by: 1035 | - Tracking actual time spent coding (not just when you make commits) 1036 | - Creating a private repository with detailed logs of your work 1037 | - Generating visual reports of your coding patterns 1038 | - Helping you understand your most productive times 1039 | 1040 | To get started, you'll need: 1041 | 1. A GitHub account 1042 | 2. An open workspace/folder 1043 | 3. Git installed on your system 1044 | `; 1045 | 1046 | vscode.window 1047 | .showInformationMessage(welcomeMessage, 'Set Up Now', 'Later') 1048 | .then((choice) => { 1049 | if (choice === 'Set Up Now') { 1050 | vscode.commands.executeCommand('devtrack.login'); 1051 | } 1052 | }); 1053 | } 1054 | 1055 | // Deactivation 1056 | export function deactivate(): void { 1057 | // VSCode will handle disposal of subscriptions 1058 | } 1059 | -------------------------------------------------------------------------------- /src/services/changeAnalyzer.ts: -------------------------------------------------------------------------------- 1 | // src/services/changeAnalyzer.ts 2 | 3 | import * as vscode from 'vscode'; 4 | import { Change } from './tracker'; 5 | import { OutputChannel } from 'vscode'; 6 | 7 | export type ChangeType = 8 | | 'feature' 9 | | 'bugfix' 10 | | 'refactor' 11 | | 'docs' 12 | | 'style' 13 | | 'other'; 14 | 15 | interface ScoreMap { 16 | feature: number; 17 | bugfix: number; 18 | refactor: number; 19 | docs: number; 20 | style: number; 21 | } 22 | 23 | export interface ChangeAnalysis { 24 | type: ChangeType; 25 | confidence: number; 26 | details: string[]; 27 | } 28 | 29 | export class ChangeAnalyzer { 30 | private outputChannel: OutputChannel; 31 | 32 | constructor(outputChannel: OutputChannel) { 33 | this.outputChannel = outputChannel; 34 | } 35 | 36 | private readonly INDICATORS = { 37 | feature: { 38 | keywords: ['feat', 'feature', 'add', 'implement', 'new'], 39 | patterns: [ 40 | /new (class|interface|type|enum|function|component)/i, 41 | /implement.*feature/i, 42 | /\+\s*export/, 43 | ], 44 | }, 45 | bugfix: { 46 | keywords: ['fix', 'bug', 'issue', 'crash', 'error', 'resolve'], 47 | patterns: [ 48 | /fix(es|ed)?/i, 49 | /\b(bug|issue|crash|error)\b/i, 50 | /catch\s*\(/, 51 | /try\s*{/, 52 | ], 53 | }, 54 | refactor: { 55 | keywords: [ 56 | 'refactor', 57 | 'restructure', 58 | 'reorganize', 59 | 'improve', 60 | 'optimize', 61 | ], 62 | patterns: [ 63 | /refactor/i, 64 | /\brenamed?\b/i, 65 | /\bmoved?\b/i, 66 | /\bcleanup\b/i, 67 | /\boptimize(d)?\b/i, 68 | ], 69 | }, 70 | docs: { 71 | keywords: ['doc', 'comment', 'readme', 'changelog'], 72 | patterns: [ 73 | /\/\*\*?[\s\S]*?\*\//, // JSDoc comments 74 | /^\s*\/\//, // Single line comments 75 | /\.md$/i, // Markdown files 76 | ], 77 | }, 78 | style: { 79 | keywords: ['style', 'format', 'lint', 'prettier'], 80 | patterns: [/\bindent/i, /\bformat/i, /\.css$/, /\.scss$/, /style:\s/], 81 | }, 82 | }; 83 | 84 | public async analyzeChanges(changes: Change[]): Promise { 85 | try { 86 | const scores: ScoreMap = { 87 | feature: 0, 88 | bugfix: 0, 89 | refactor: 0, 90 | docs: 0, 91 | style: 0, 92 | }; 93 | 94 | const details: string[] = []; 95 | 96 | for (const change of changes) { 97 | const content = await this.getFileContent(change.uri); 98 | const filename = change.uri.fsPath.toLowerCase(); 99 | 100 | // Analyze file extension and name 101 | if (filename.endsWith('.test.ts') || filename.endsWith('.spec.ts')) { 102 | scores.feature += 0.5; 103 | details.push('Test file changes detected'); 104 | } 105 | 106 | // Analyze content changes 107 | for (const [type, indicators] of Object.entries(this.INDICATORS)) { 108 | if (this.isValidChangeType(type)) { 109 | // Check keywords in content 110 | const keywordMatches = indicators.keywords.filter((keyword) => 111 | content.toLowerCase().includes(keyword.toLowerCase()) 112 | ); 113 | 114 | // Check regex patterns 115 | const patternMatches = indicators.patterns.filter((pattern) => 116 | pattern.test(content) 117 | ); 118 | 119 | if (keywordMatches.length > 0) { 120 | scores[type] += keywordMatches.length * 0.5; 121 | details.push( 122 | `Found ${type} keywords: ${keywordMatches.join(', ')}` 123 | ); 124 | } 125 | 126 | if (patternMatches.length > 0) { 127 | scores[type] += patternMatches.length; 128 | details.push(`Detected ${type} patterns in code`); 129 | } 130 | } 131 | } 132 | } 133 | 134 | // Determine the most likely type 135 | const maxScore = Math.max(...Object.values(scores)); 136 | const type = 137 | (Object.entries(scores).find( 138 | ([, score]) => score === maxScore 139 | )?.[0] as ChangeType) || 'other'; 140 | 141 | // Calculate confidence (0-1) 142 | const totalScore = Object.values(scores).reduce((a, b) => a + b, 0); 143 | const confidence = totalScore > 0 ? maxScore / totalScore : 0; 144 | 145 | return { 146 | type, 147 | confidence, 148 | details: [...new Set(details)], // Remove duplicates 149 | }; 150 | } catch (error) { 151 | this.outputChannel.appendLine(`Error analyzing changes: ${error}`); 152 | return { type: 'other', confidence: 0, details: [] }; 153 | } 154 | } 155 | 156 | private isValidChangeType(type: string): type is keyof ScoreMap { 157 | return type in this.INDICATORS; 158 | } 159 | 160 | private async getFileContent(uri: vscode.Uri): Promise { 161 | try { 162 | const document = await vscode.workspace.openTextDocument(uri); 163 | return document.getText(); 164 | } catch (error) { 165 | this.outputChannel.appendLine(`Error reading file content: ${error}`); 166 | return ''; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/services/githubService.ts: -------------------------------------------------------------------------------- 1 | // services/githubService.ts 2 | import { Octokit } from '@octokit/rest'; 3 | import * as vscode from 'vscode'; 4 | import { OutputChannel } from 'vscode'; 5 | 6 | export class GitHubService { 7 | private octokit!: Octokit; 8 | private token: string = ''; 9 | private outputChannel: OutputChannel; 10 | 11 | constructor(outputChannel: OutputChannel) { 12 | this.outputChannel = outputChannel; 13 | // Token will be set via setToken method 14 | } 15 | 16 | /** 17 | * Sets the GitHub token and initializes Octokit. 18 | * @param token - The GitHub access token obtained via OAuth. 19 | */ 20 | setToken(token: string) { 21 | this.token = token; 22 | this.octokit = new Octokit({ auth: this.token }); 23 | } 24 | 25 | /** 26 | * Creates a new repository for the authenticated user. 27 | * @param repoName - The name of the repository to create. 28 | * @param description - (Optional) Description of the repository. 29 | * @returns The clone URL of the created repository or null if creation failed. 30 | */ 31 | async createRepo( 32 | repoName: string, 33 | description: string = 'DevTrack Repository' 34 | ): Promise { 35 | try { 36 | const response = await this.octokit.repos.createForAuthenticatedUser({ 37 | name: repoName, 38 | description, 39 | private: false, 40 | }); 41 | return response.data.clone_url; 42 | } catch (error: any) { 43 | this.outputChannel.appendLine( 44 | `Error creating repository: ${error.message}` 45 | ); 46 | vscode.window.showErrorMessage( 47 | `DevTrack: Failed to create repository "${repoName}".` 48 | ); 49 | return null; 50 | } 51 | } 52 | 53 | /** 54 | * Checks if a repository exists for the authenticated user. 55 | * @param repoName - The name of the repository to check. 56 | * @returns True if the repository exists, false otherwise. 57 | */ 58 | async repoExists(repoName: string): Promise { 59 | try { 60 | const username = await this.getUsername(); 61 | if (!username) { 62 | vscode.window.showErrorMessage( 63 | 'DevTrack: Unable to retrieve GitHub username.' 64 | ); 65 | return false; 66 | } 67 | await this.octokit.repos.get({ 68 | owner: username, 69 | repo: repoName, 70 | }); 71 | return true; 72 | } catch (error: any) { 73 | if (error.status === 404) { 74 | return false; 75 | } 76 | vscode.window.showErrorMessage( 77 | `DevTrack: Error checking repository "${repoName}".` 78 | ); 79 | return false; 80 | } 81 | } 82 | 83 | /** 84 | * Retrieves the authenticated user's GitHub username. 85 | * @returns The GitHub username or null if retrieval failed. 86 | */ 87 | async getUsername(): Promise { 88 | try { 89 | const { data } = await this.octokit.users.getAuthenticated(); 90 | return data.login; 91 | } catch (error: any) { 92 | this.outputChannel.appendLine( 93 | `Error fetching username: ${error.message}` 94 | ); 95 | vscode.window.showErrorMessage( 96 | 'DevTrack: Unable to fetch GitHub username.' 97 | ); 98 | return null; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/services/projectContext.ts: -------------------------------------------------------------------------------- 1 | // src/services/projectContext.ts 2 | import * as vscode from 'vscode'; 3 | import { Change } from './tracker'; 4 | import * as path from 'path'; 5 | 6 | interface CommitHistory { 7 | timestamp: number; 8 | summary: string; 9 | files: string[]; 10 | hash?: string; 11 | } 12 | 13 | interface ProjectStats { 14 | mostChangedFiles: Map; 15 | recentMilestones: string[]; 16 | activeBranch: string; 17 | lastCommitTime: Date; 18 | } 19 | 20 | interface GitCommit { 21 | hash: string; 22 | message: string; 23 | commitDate: Date; 24 | files: { uri: vscode.Uri }[]; 25 | } 26 | 27 | export class ProjectContext { 28 | private outputChannel: vscode.OutputChannel; 29 | private extensionContext: vscode.ExtensionContext; 30 | private commitHistory: CommitHistory[] = []; 31 | private projectStats: ProjectStats = { 32 | mostChangedFiles: new Map(), 33 | recentMilestones: [], 34 | activeBranch: '', 35 | lastCommitTime: new Date(), 36 | }; 37 | 38 | constructor( 39 | outputChannel: vscode.OutputChannel, 40 | extensionContext: vscode.ExtensionContext 41 | ) { 42 | this.outputChannel = outputChannel; 43 | this.extensionContext = extensionContext; 44 | this.loadContext(); 45 | } 46 | 47 | private async loadContext() { 48 | try { 49 | await this.loadGitHistory(); 50 | await this.updateProjectStats(); 51 | this.outputChannel.appendLine('DevTrack: Loaded project context'); 52 | } catch (error) { 53 | this.outputChannel.appendLine( 54 | `DevTrack: Error loading context: ${error}` 55 | ); 56 | } 57 | } 58 | 59 | private async loadGitHistory() { 60 | try { 61 | const gitExt = vscode.extensions.getExtension('vscode.git'); 62 | if (gitExt) { 63 | const git = gitExt.exports.getAPI(1); 64 | if (git.repositories.length > 0) { 65 | const repo = git.repositories[0]; 66 | const commits = (await repo.log({ maxEntries: 50 })) as GitCommit[]; 67 | 68 | this.commitHistory = commits.map((commit: GitCommit) => ({ 69 | timestamp: commit.commitDate.getTime(), 70 | summary: commit.message, 71 | files: 72 | commit.files?.map((f) => 73 | vscode.workspace.asRelativePath(f.uri) 74 | ) || [], 75 | hash: commit.hash, 76 | })); 77 | } 78 | } 79 | } catch (error) { 80 | this.outputChannel.appendLine( 81 | `DevTrack: Error loading git history: ${error}` 82 | ); 83 | } 84 | } 85 | 86 | private shouldTrackFile(filePath: string): boolean { 87 | // Ignore specific patterns 88 | const ignorePatterns = [ 89 | 'node_modules', 90 | '.git', 91 | '.DS_Store', 92 | 'dist', 93 | 'out', 94 | 'build', 95 | '.vscode', 96 | ]; 97 | 98 | // Get file extension 99 | const fileExt = path.extname(filePath).toLowerCase().slice(1); 100 | 101 | // Track only specific file types 102 | const trackedExtensions = [ 103 | 'ts', 104 | 'js', 105 | 'py', 106 | 'java', 107 | 'c', 108 | 'cpp', 109 | 'h', 110 | 'hpp', 111 | 'css', 112 | 'scss', 113 | 'html', 114 | 'jsx', 115 | 'tsx', 116 | 'vue', 117 | 'php', 118 | 'rb', 119 | 'go', 120 | 'rs', 121 | 'swift', 122 | 'md', 123 | 'json', 124 | 'yml', 125 | 'yaml', 126 | ]; 127 | 128 | return ( 129 | !ignorePatterns.some((pattern) => filePath.includes(pattern)) && 130 | Boolean(fileExt) && 131 | trackedExtensions.includes(fileExt) 132 | ); 133 | } 134 | 135 | public async addCommit(summary: string, changes: Change[]) { 136 | try { 137 | const trackedFiles = changes 138 | .map((change) => vscode.workspace.asRelativePath(change.uri)) 139 | .filter((filePath) => this.shouldTrackFile(filePath)); 140 | 141 | // eslint-disable-next-line no-unused-vars 142 | const commit: CommitHistory = { 143 | timestamp: Date.now(), 144 | summary: summary, 145 | files: trackedFiles, 146 | }; 147 | 148 | await this.loadGitHistory(); // Refresh Git history 149 | await this.updateProjectStats(); 150 | } catch (error) { 151 | this.outputChannel.appendLine(`DevTrack: Error adding commit: ${error}`); 152 | } 153 | } 154 | 155 | private async updateProjectStats() { 156 | try { 157 | const stats = new Map(); 158 | 159 | // Update file change frequencies from Git history 160 | this.commitHistory.forEach((commit) => { 161 | commit.files.forEach((file) => { 162 | if (this.shouldTrackFile(file)) { 163 | const count = stats.get(file) || 0; 164 | stats.set(file, count + 1); 165 | } 166 | }); 167 | }); 168 | 169 | // Sort by frequency 170 | const sortedFiles = new Map( 171 | [...stats.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10) 172 | ); 173 | 174 | // Get git branch 175 | let currentBranch = ''; 176 | try { 177 | const gitExt = vscode.extensions.getExtension('vscode.git'); 178 | if (gitExt) { 179 | const git = gitExt.exports.getAPI(1); 180 | if (git.repositories.length > 0) { 181 | currentBranch = git.repositories[0].state.HEAD?.name || ''; 182 | } 183 | } 184 | } catch (error) { 185 | this.outputChannel.appendLine( 186 | `DevTrack: Error getting git branch: ${error}` 187 | ); 188 | } 189 | 190 | this.projectStats = { 191 | mostChangedFiles: sortedFiles, 192 | recentMilestones: [], 193 | activeBranch: currentBranch, 194 | lastCommitTime: new Date( 195 | this.commitHistory[0]?.timestamp || Date.now() 196 | ), 197 | }; 198 | } catch (error) { 199 | this.outputChannel.appendLine( 200 | `DevTrack: Error updating project stats: ${error}` 201 | ); 202 | } 203 | } 204 | 205 | public getContextForSummary(currentChanges: Change[]): string { 206 | let context = ''; 207 | 208 | try { 209 | // Get current changed files 210 | const currentFiles = currentChanges 211 | .map((change) => path.basename(change.uri.fsPath)) 212 | .filter((file, index, self) => self.indexOf(file) === index); 213 | 214 | if (currentFiles.length > 0) { 215 | context += `Files: ${currentFiles.join(', ')}. `; 216 | } 217 | 218 | // Add branch context 219 | if (this.projectStats.activeBranch) { 220 | context += `[${this.projectStats.activeBranch}] `; 221 | } 222 | } catch (error) { 223 | this.outputChannel.appendLine( 224 | `DevTrack: Error generating context summary: ${error}` 225 | ); 226 | } 227 | 228 | return context; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/services/scheduler.ts: -------------------------------------------------------------------------------- 1 | // services/scheduler.ts 2 | import * as vscode from 'vscode'; 3 | import { 4 | setTimeout, 5 | clearInterval, 6 | setInterval, 7 | clearTimeout, 8 | } from 'node:timers'; 9 | import { Tracker, ActivityMetrics } from './tracker'; 10 | import { SummaryGenerator } from './summaryGenerator'; 11 | import { GitService } from './gitService'; 12 | import { OutputChannel } from 'vscode'; 13 | 14 | interface SchedulerOptions { 15 | commitFrequency: number; 16 | minChangesForCommit: number; 17 | minActiveTimeForCommit: number; // in seconds 18 | maxIdleTimeBeforePause: number; // in seconds 19 | enableAdaptiveScheduling: boolean; 20 | } 21 | 22 | export class Scheduler { 23 | private timer: ReturnType | null = null; 24 | private isCommitting = false; 25 | private pendingChanges = false; 26 | private lastCommitTime: Date = new Date(); 27 | private statusBarItem: vscode.StatusBarItem; 28 | private countdownTimer: ReturnType | null = null; 29 | private isAdaptiveMode: boolean = false; 30 | private inactivityPauseTimer: ReturnType | null = null; 31 | private options: SchedulerOptions; 32 | private isRunning: boolean = false; 33 | 34 | constructor( 35 | private commitFrequency: number, 36 | private tracker: Tracker, 37 | private summaryGenerator: SummaryGenerator, 38 | private gitService: GitService, 39 | private outputChannel: OutputChannel 40 | ) { 41 | // Default options 42 | this.options = { 43 | commitFrequency: commitFrequency, 44 | minChangesForCommit: 1, 45 | minActiveTimeForCommit: 60, // 1 minute of active time 46 | maxIdleTimeBeforePause: 15 * 60, // 15 minutes 47 | enableAdaptiveScheduling: true, 48 | }; 49 | 50 | // Create status bar item for countdown 51 | this.statusBarItem = vscode.window.createStatusBarItem( 52 | vscode.StatusBarAlignment.Right, 53 | 99 54 | ); 55 | this.statusBarItem.tooltip = 'Time until next DevTrack commit'; 56 | this.statusBarItem.command = 'devtrack.commitNow'; 57 | 58 | // Listen for activity metrics from tracker 59 | this.tracker.on('activityMetrics', (metrics: ActivityMetrics) => { 60 | this.handleActivityMetrics(metrics); 61 | }); 62 | } 63 | 64 | start() { 65 | if (this.isRunning) { 66 | return; 67 | } 68 | 69 | this.isRunning = true; 70 | this.lastCommitTime = new Date(); 71 | this.resetTimer(); 72 | this.startCountdown(); 73 | this.statusBarItem.show(); 74 | this.outputChannel.appendLine( 75 | `Scheduler: Started with a frequency of ${this.commitFrequency} minutes.` 76 | ); 77 | } 78 | 79 | stop() { 80 | if (!this.isRunning) { 81 | return; 82 | } 83 | 84 | this.isRunning = false; 85 | if (this.timer) { 86 | clearTimeout(this.timer); 87 | this.timer = null; 88 | } 89 | 90 | if (this.countdownTimer) { 91 | clearInterval(this.countdownTimer); 92 | this.countdownTimer = null; 93 | } 94 | 95 | if (this.inactivityPauseTimer) { 96 | clearTimeout(this.inactivityPauseTimer); 97 | this.inactivityPauseTimer = null; 98 | } 99 | 100 | this.statusBarItem.hide(); 101 | this.outputChannel.appendLine('Scheduler: Stopped.'); 102 | } 103 | 104 | private resetTimer() { 105 | if (this.timer) { 106 | clearTimeout(this.timer); 107 | } 108 | 109 | const timeoutMs = this.commitFrequency * 60 * 1000; 110 | this.timer = setTimeout(() => this.commitChanges(), timeoutMs); 111 | } 112 | 113 | private startCountdown() { 114 | if (this.countdownTimer) { 115 | clearInterval(this.countdownTimer); 116 | } 117 | 118 | this.updateCountdown(); 119 | 120 | this.countdownTimer = setInterval(() => { 121 | this.updateCountdown(); 122 | }, 1000); 123 | } 124 | 125 | private updateCountdown() { 126 | if (!this.isRunning) { 127 | return; 128 | } 129 | 130 | const now = new Date(); 131 | const elapsedMs = now.getTime() - this.lastCommitTime.getTime(); 132 | const remainingMs = Math.max( 133 | 0, 134 | this.commitFrequency * 60 * 1000 - elapsedMs 135 | ); 136 | 137 | const remainingMinutes = Math.floor(remainingMs / 60000); 138 | const remainingSeconds = Math.floor((remainingMs % 60000) / 1000); 139 | 140 | this.statusBarItem.text = `$(clock) ${remainingMinutes}:${remainingSeconds.toString().padStart(2, '0')}`; 141 | } 142 | 143 | private handleActivityMetrics(metrics: ActivityMetrics) { 144 | // If we're using adaptive scheduling, we might want to commit 145 | // earlier if there's a lot of activity 146 | if ( 147 | this.options.enableAdaptiveScheduling && 148 | this.isRunning && 149 | !this.isCommitting 150 | ) { 151 | const now = new Date(); 152 | const timeSinceLastCommit = 153 | (now.getTime() - this.lastCommitTime.getTime()) / 1000; 154 | 155 | // If we've been active for at least half the commit frequency 156 | // and have significant changes, commit early 157 | if ( 158 | timeSinceLastCommit > (this.commitFrequency * 60) / 2 && 159 | metrics.fileChanges >= 5 && 160 | metrics.activeTime > this.options.minActiveTimeForCommit 161 | ) { 162 | this.outputChannel.appendLine( 163 | 'Scheduler: Adaptive commit triggered due to high activity' 164 | ); 165 | this.commitChanges(); 166 | return; 167 | } 168 | 169 | // Setup inactivity pause timer if we detect no activity for a while 170 | const timeSinceLastActivity = 171 | (now.getTime() - metrics.lastActiveTimestamp.getTime()) / 1000; 172 | if ( 173 | timeSinceLastActivity > this.options.maxIdleTimeBeforePause && 174 | !this.inactivityPauseTimer 175 | ) { 176 | this.inactivityPauseTimer = setTimeout(() => { 177 | if (this.isRunning) { 178 | this.outputChannel.appendLine( 179 | 'Scheduler: Pausing due to inactivity' 180 | ); 181 | // Don't actually stop, just pause the timer 182 | if (this.timer) { 183 | clearTimeout(this.timer); 184 | this.timer = null; 185 | } 186 | } 187 | }, 60000); // Wait a minute before actually pausing 188 | } else if (timeSinceLastActivity < 60 && this.inactivityPauseTimer) { 189 | // Activity detected, clear inactivity timer 190 | clearTimeout(this.inactivityPauseTimer); 191 | this.inactivityPauseTimer = null; 192 | 193 | // Resume timer if it was paused 194 | if (this.isRunning && !this.timer) { 195 | this.resetTimer(); 196 | this.outputChannel.appendLine( 197 | 'Scheduler: Resuming from inactivity pause' 198 | ); 199 | } 200 | } 201 | } 202 | } 203 | 204 | async commitChanges() { 205 | if (this.isCommitting) { 206 | this.pendingChanges = true; 207 | this.outputChannel.appendLine( 208 | 'Scheduler: Commit already in progress, queuing changes.' 209 | ); 210 | return; 211 | } 212 | 213 | const changedFiles = this.tracker.getChangedFiles(); 214 | if (changedFiles.length === 0) { 215 | this.outputChannel.appendLine('Scheduler: No changes detected.'); 216 | this.resetTimer(); // Reset timer anyway 217 | this.lastCommitTime = new Date(); 218 | return; 219 | } 220 | 221 | // Get activity metrics 222 | const activityMetrics = this.tracker.getActivityMetrics(); 223 | 224 | // Skip commit if not enough active time (unless it's been a long time) 225 | const now = new Date(); 226 | const hoursSinceLastCommit = 227 | (now.getTime() - this.lastCommitTime.getTime()) / (60 * 60 * 1000); 228 | 229 | // If minimal activity and not enough time has passed, skip 230 | if ( 231 | activityMetrics.activeTime < this.options.minActiveTimeForCommit && 232 | hoursSinceLastCommit < 1 && 233 | changedFiles.length < 3 234 | ) { 235 | this.outputChannel.appendLine( 236 | `Scheduler: Skipping commit due to minimal activity (${Math.round(activityMetrics.activeTime)} seconds).` 237 | ); 238 | this.resetTimer(); 239 | return; 240 | } 241 | 242 | try { 243 | this.isCommitting = true; 244 | this.statusBarItem.text = `$(sync~spin) Committing...`; 245 | 246 | // Add activity metrics to the summary 247 | const enrichedSummary = await this.summaryGenerator.generateSummary( 248 | changedFiles, 249 | activityMetrics 250 | ); 251 | 252 | const config = vscode.workspace.getConfiguration('devtrack'); 253 | if (config.get('confirmBeforeCommit', true)) { 254 | // Create a condensed version of the commit message for the dialog 255 | const condensedMessage = 256 | this.createCondensedCommitMessage(enrichedSummary); 257 | 258 | const userResponse = await vscode.window.showInformationMessage( 259 | `DevTrack: A commit will be made with the following changes:\n"${condensedMessage}"`, 260 | { modal: true }, 261 | 'Proceed', 262 | 'Cancel' 263 | ); 264 | 265 | if (userResponse !== 'Proceed') { 266 | this.outputChannel.appendLine( 267 | 'Scheduler: Commit canceled by the user.' 268 | ); 269 | this.isCommitting = false; 270 | this.resetTimer(); 271 | return; 272 | } 273 | } 274 | 275 | await this.gitService.commitAndPush(enrichedSummary); 276 | this.tracker.clearChanges(); 277 | this.tracker.resetMetrics(); 278 | this.lastCommitTime = new Date(); 279 | this.outputChannel.appendLine( 280 | `Scheduler: Committed changes with metrics: ${Math.round(activityMetrics.activeTime / 60)} minutes active, ${changedFiles.length} files changed` 281 | ); 282 | } catch (error: any) { 283 | this.outputChannel.appendLine( 284 | `Scheduler: Failed to commit changes. ${error.message}` 285 | ); 286 | vscode.window.showErrorMessage( 287 | `DevTrack: Commit failed. ${error.message}` 288 | ); 289 | } finally { 290 | this.isCommitting = false; 291 | this.resetTimer(); 292 | this.updateCountdown(); 293 | 294 | if (this.pendingChanges) { 295 | this.pendingChanges = false; 296 | this.outputChannel.appendLine( 297 | 'Scheduler: Processing pending changes...' 298 | ); 299 | setTimeout(() => this.commitChanges(), 5000); // Wait 5 seconds before processing pending changes 300 | } 301 | } 302 | } 303 | 304 | // Manually trigger a commit now 305 | async commitNow() { 306 | if (this.isCommitting) { 307 | vscode.window.showInformationMessage( 308 | 'DevTrack: A commit is already in progress.' 309 | ); 310 | return; 311 | } 312 | 313 | const changedFiles = this.tracker.getChangedFiles(); 314 | if (changedFiles.length === 0) { 315 | vscode.window.showInformationMessage('DevTrack: No changes to commit.'); 316 | return; 317 | } 318 | 319 | // Reset the timer and commit 320 | if (this.timer) { 321 | clearTimeout(this.timer); 322 | this.timer = null; 323 | } 324 | 325 | await this.commitChanges(); 326 | } 327 | 328 | private createCondensedCommitMessage(fullMessage: string): string { 329 | // Extract just the first part of the message (before code snippets) 330 | const parts = fullMessage.split('Code Snippets:'); 331 | let condensed = parts[0].trim(); 332 | 333 | // Add a count of affected files instead of showing all snippets 334 | const codeBlockCount = (fullMessage.match(/```/g) || []).length / 2; 335 | condensed += `\n(${codeBlockCount} file${codeBlockCount !== 1 ? 's' : ''} modified)`; 336 | 337 | // Limit to a reasonable length 338 | if (condensed.length > 500) { 339 | condensed = condensed.substring(0, 497) + '...'; 340 | } 341 | 342 | return condensed; 343 | } 344 | 345 | updateFrequency(newFrequency: number) { 346 | this.commitFrequency = newFrequency; 347 | this.options.commitFrequency = newFrequency; 348 | 349 | if (this.isRunning) { 350 | this.resetTimer(); // Restart the scheduler with the new frequency 351 | this.outputChannel.appendLine( 352 | `Scheduler: Updated commit frequency to ${newFrequency} minutes.` 353 | ); 354 | } 355 | } 356 | 357 | updateOptions(newOptions: Partial) { 358 | this.options = { ...this.options, ...newOptions }; 359 | this.outputChannel.appendLine('Scheduler: Updated options'); 360 | 361 | if (this.isRunning) { 362 | this.resetTimer(); // Apply any new settings 363 | } 364 | } 365 | 366 | dispose() { 367 | this.stop(); 368 | this.statusBarItem.dispose(); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/services/statisticsProvider.ts: -------------------------------------------------------------------------------- 1 | // src/services/statisticsProvider.ts 2 | import * as vscode from 'vscode'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | 6 | interface CodingStatistics { 7 | totalTime: number; 8 | filesModified: number; 9 | totalCommits: number; 10 | linesChanged: number; 11 | activityTimeline: ActivityData[]; 12 | timeDistribution: TimeDistribution[]; 13 | fileTypes: FileTypeStats[]; 14 | } 15 | 16 | interface ActivityData { 17 | date: string; 18 | commits: number; 19 | filesChanged: number; 20 | linesChanged: number; 21 | } 22 | 23 | interface TimeDistribution { 24 | hour: string; 25 | changes: number; 26 | } 27 | 28 | interface FileTypeStats { 29 | name: string; 30 | count: number; 31 | percentage: number; 32 | } 33 | 34 | export class StatisticsProvider { 35 | private outputChannel: vscode.OutputChannel; 36 | private trackingDir: string; 37 | 38 | constructor(outputChannel: vscode.OutputChannel, trackingDir: string) { 39 | this.outputChannel = outputChannel; 40 | this.trackingDir = trackingDir; 41 | } 42 | 43 | async getStatistics(): Promise { 44 | try { 45 | const changesDir = path.join(this.trackingDir, 'changes'); 46 | const files = await fs.promises.readdir(changesDir); 47 | 48 | // Process all change files 49 | const stats = await this.processChangeFiles(files, changesDir); 50 | 51 | return { 52 | totalTime: this.calculateTotalTime(stats.activityTimeline), 53 | filesModified: stats.filesModified, 54 | totalCommits: stats.totalCommits, 55 | linesChanged: stats.linesChanged, 56 | activityTimeline: stats.activityTimeline, 57 | timeDistribution: this.calculateTimeDistribution(stats.timeData), 58 | fileTypes: this.calculateFileTypeStats(stats.fileTypes), 59 | }; 60 | } catch (error) { 61 | this.outputChannel.appendLine(`Error getting statistics: ${error}`); 62 | throw error; 63 | } 64 | } 65 | 66 | private async processChangeFiles(files: string[], changesDir: string) { 67 | const stats = { 68 | filesModified: 0, 69 | totalCommits: 0, 70 | linesChanged: 0, 71 | activityTimeline: new Map(), 72 | timeData: new Map(), 73 | fileTypes: new Map(), 74 | }; 75 | 76 | for (const file of files) { 77 | if (!file.endsWith('.json')) { 78 | continue; 79 | } 80 | 81 | const filePath = path.join(changesDir, file); 82 | const content = await fs.promises.readFile(filePath, 'utf8'); 83 | const changeData = JSON.parse(content); 84 | 85 | this.processChangeData(changeData, stats); 86 | } 87 | 88 | return { 89 | ...stats, 90 | activityTimeline: Array.from(stats.activityTimeline.values()), 91 | }; 92 | } 93 | 94 | private processChangeData(changeData: any, stats: any) { 95 | const date = new Date(changeData.timestamp); 96 | const dateKey = date.toISOString().split('T')[0]; 97 | const hour = date.getHours(); 98 | 99 | // Update timeline data 100 | if (!stats.activityTimeline.has(dateKey)) { 101 | stats.activityTimeline.set(dateKey, { 102 | date: dateKey, 103 | commits: 0, 104 | filesChanged: 0, 105 | linesChanged: 0, 106 | }); 107 | } 108 | const timelineData = stats.activityTimeline.get(dateKey); 109 | timelineData.commits++; 110 | timelineData.filesChanged += changeData.files.length; 111 | // Estimate lines changed based on file size changes 112 | timelineData.linesChanged += this.estimateLineChanges(changeData); 113 | 114 | // Update hourly distribution 115 | stats.timeData.set(hour, (stats.timeData.get(hour) || 0) + 1); 116 | 117 | // Update file type statistics 118 | for (const file of changeData.files) { 119 | const ext = path.extname(file).toLowerCase(); 120 | if (ext) { 121 | stats.fileTypes.set(ext, (stats.fileTypes.get(ext) || 0) + 1); 122 | } 123 | } 124 | 125 | // Update total statistics 126 | stats.totalCommits++; 127 | stats.filesModified += changeData.files.length; 128 | stats.linesChanged += this.estimateLineChanges(changeData); 129 | } 130 | 131 | private calculateTotalTime(timeline: ActivityData[]): number { 132 | // Estimate coding time based on activity frequency 133 | const AVERAGE_SESSION_LENGTH = 30; // minutes 134 | return (timeline.length * AVERAGE_SESSION_LENGTH) / 60; // Convert to hours 135 | } 136 | 137 | private calculateTimeDistribution( 138 | timeData: Map 139 | ): TimeDistribution[] { 140 | return Array.from(timeData.entries()).map(([hour, changes]) => ({ 141 | hour: `${hour % 12 || 12}${hour < 12 ? 'AM' : 'PM'}`, 142 | changes, 143 | })); 144 | } 145 | 146 | private calculateFileTypeStats( 147 | fileTypes: Map 148 | ): FileTypeStats[] { 149 | const total = Array.from(fileTypes.values()).reduce((a, b) => a + b, 0); 150 | return Array.from(fileTypes.entries()) 151 | .map(([ext, count]) => ({ 152 | name: ext.slice(1).toUpperCase(), 153 | count, 154 | percentage: Math.round((count / total) * 100), 155 | })) 156 | .sort((a, b) => b.count - a.count); 157 | } 158 | 159 | private estimateLineChanges(changeData: any): number { 160 | // Simple estimation based on file content length 161 | return changeData.files.length * 10; // Rough estimate 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/services/statisticsView.ts: -------------------------------------------------------------------------------- 1 | // src/services/statisticsView.ts 2 | import * as vscode from 'vscode'; 3 | import { setInterval, clearInterval } from 'node:timers'; 4 | import { StatisticsProvider } from './statisticsProvider'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | export class StatisticsView { 9 | private panel: vscode.WebviewPanel | undefined; 10 | private disposables: vscode.Disposable[] = []; 11 | private updateInterval: ReturnType | undefined; 12 | private statsHtmlPath: string; 13 | private isFirstLoad: boolean = true; 14 | 15 | constructor( 16 | private context: vscode.ExtensionContext, 17 | private statisticsProvider: StatisticsProvider, 18 | private outputChannel: vscode.OutputChannel, 19 | private trackingDir: string 20 | ) { 21 | this.statsHtmlPath = path.join(this.trackingDir, 'stats', 'index.html'); 22 | } 23 | 24 | public show() { 25 | const columnToShowIn = vscode.window.activeTextEditor 26 | ? vscode.window.activeTextEditor.viewColumn 27 | : undefined; 28 | 29 | if (this.panel) { 30 | this.panel.reveal(columnToShowIn); 31 | return; 32 | } 33 | 34 | this.panel = vscode.window.createWebviewPanel( 35 | 'devtrackStats', 36 | 'DevTrack Coding Statistics', 37 | columnToShowIn || vscode.ViewColumn.One, 38 | { 39 | enableScripts: true, 40 | retainContextWhenHidden: true, 41 | localResourceRoots: [ 42 | vscode.Uri.file(this.trackingDir), 43 | vscode.Uri.file(path.join(this.context.extensionPath, 'media')), 44 | ], 45 | } 46 | ); 47 | 48 | // Set webview content 49 | this.updateWebview(); 50 | 51 | // Handle messages from the webview 52 | this.panel.webview.onDidReceiveMessage( 53 | async (message) => { 54 | switch (message.command) { 55 | case 'refresh': 56 | await this.updateStatistics(); 57 | break; 58 | case 'themeChanged': 59 | await this.context.globalState.update( 60 | 'devtrackTheme', 61 | message.theme 62 | ); 63 | break; 64 | } 65 | }, 66 | null, 67 | this.disposables 68 | ); 69 | 70 | // Handle panel disposal 71 | this.panel.onDidDispose( 72 | () => { 73 | this.panel = undefined; 74 | this.stopAutoRefresh(); 75 | this.dispose(); 76 | }, 77 | null, 78 | this.disposables 79 | ); 80 | 81 | // Start auto-refresh if not already running 82 | this.setupAutoRefresh(); 83 | } 84 | 85 | private async updateWebview() { 86 | if (!this.panel) { 87 | return; 88 | } 89 | 90 | try { 91 | const stats = await this.statisticsProvider.getStatistics(); 92 | const savedTheme = await this.context.globalState.get( 93 | 'devtrackTheme', 94 | 'system' 95 | ); 96 | 97 | // Get the dashboard HTML content 98 | const dashboardHtml = await fs.promises.readFile( 99 | this.statsHtmlPath, 100 | 'utf8' 101 | ); 102 | 103 | // Create webview content with current stats and theme 104 | const webviewContent = this.getWebviewContent( 105 | dashboardHtml, 106 | stats, 107 | savedTheme 108 | ); 109 | 110 | this.panel.webview.html = webviewContent; 111 | 112 | if (this.isFirstLoad) { 113 | this.isFirstLoad = false; 114 | this.outputChannel.appendLine('DevTrack: Statistics view initialized'); 115 | } 116 | } catch (error) { 117 | this.outputChannel.appendLine(`Error updating webview: ${error}`); 118 | this.panel.webview.html = this.getErrorContent(); 119 | } 120 | } 121 | 122 | private getWebviewContent( 123 | dashboardHtml: string, 124 | stats: any, 125 | theme: string 126 | ): string { 127 | // Get webview URIs for any local resources 128 | const scriptUri = this.panel!.webview.asWebviewUri( 129 | vscode.Uri.file(path.join(this.trackingDir, 'stats', 'dashboard.js')) 130 | ); 131 | 132 | return ` 133 | 134 | 135 | 136 | 137 | 138 | DevTrack Statistics 139 | 140 | 141 | 142 | 143 | 148 | 149 | 150 |
151 | 152 | 153 | 154 | `; 155 | } 156 | 157 | private getErrorContent(): string { 158 | return ` 159 | 160 | 161 | 162 |
163 |
164 |

Failed to load statistics

165 | 168 |
169 |
170 | 171 | 172 | `; 173 | } 174 | 175 | private setupAutoRefresh() { 176 | if (this.updateInterval) { 177 | clearInterval(this.updateInterval); 178 | } 179 | 180 | this.updateInterval = setInterval( 181 | () => { 182 | this.updateStatistics(); 183 | }, 184 | 5 * 60 * 1000 185 | ); // Update every 5 minutes 186 | } 187 | 188 | private stopAutoRefresh() { 189 | if (this.updateInterval) { 190 | clearInterval(this.updateInterval); 191 | this.updateInterval = undefined; 192 | } 193 | } 194 | 195 | private async updateStatistics() { 196 | if (!this.panel) { 197 | return; 198 | } 199 | 200 | try { 201 | const stats = await this.statisticsProvider.getStatistics(); 202 | this.panel.webview.postMessage({ command: 'updateStats', stats }); 203 | } catch (error) { 204 | this.outputChannel.appendLine(`Error updating statistics: ${error}`); 205 | } 206 | } 207 | 208 | public dispose() { 209 | this.stopAutoRefresh(); 210 | this.disposables.forEach((d) => d.dispose()); 211 | this.disposables = []; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/services/statusBarManager.ts: -------------------------------------------------------------------------------- 1 | // src/services/statusBarManager.ts 2 | import * as vscode from 'vscode'; 3 | 4 | export class StatusBarManager { 5 | private workspaceStatusBar: vscode.StatusBarItem; 6 | private trackingStatusBar: vscode.StatusBarItem; 7 | private authStatusBar: vscode.StatusBarItem; 8 | 9 | constructor() { 10 | // Create workspace status item 11 | this.workspaceStatusBar = vscode.window.createStatusBarItem( 12 | vscode.StatusBarAlignment.Right, 13 | 102 14 | ); 15 | this.workspaceStatusBar.text = '$(folder) Open Folder to Start'; 16 | this.workspaceStatusBar.command = 'vscode.openFolder'; 17 | 18 | // Create tracking status item 19 | this.trackingStatusBar = vscode.window.createStatusBarItem( 20 | vscode.StatusBarAlignment.Right, 21 | 101 22 | ); 23 | this.trackingStatusBar.text = '$(circle-slash) DevTrack: Stopped'; 24 | this.trackingStatusBar.command = 'devtrack.startTracking'; 25 | 26 | // Create auth status item 27 | this.authStatusBar = vscode.window.createStatusBarItem( 28 | vscode.StatusBarAlignment.Right, 29 | 100 30 | ); 31 | this.authStatusBar.text = '$(mark-github) DevTrack: Not Connected'; 32 | this.authStatusBar.command = 'devtrack.login'; 33 | 34 | // Initial update 35 | this.updateVisibility(); 36 | 37 | // Listen for workspace folder changes 38 | vscode.workspace.onDidChangeWorkspaceFolders(() => { 39 | this.updateVisibility(); 40 | }); 41 | } 42 | 43 | private hasWorkspace(): boolean { 44 | return (vscode.workspace.workspaceFolders ?? []).length > 0; 45 | } 46 | 47 | private updateVisibility() { 48 | if (this.hasWorkspace()) { 49 | this.workspaceStatusBar.hide(); 50 | this.trackingStatusBar.show(); 51 | this.authStatusBar.show(); 52 | } else { 53 | this.workspaceStatusBar.show(); 54 | this.trackingStatusBar.hide(); 55 | this.authStatusBar.hide(); 56 | } 57 | } 58 | 59 | public updateTrackingStatus(isTracking: boolean) { 60 | this.trackingStatusBar.text = isTracking 61 | ? '$(clock) DevTrack: Tracking' 62 | : '$(circle-slash) DevTrack: Stopped'; 63 | this.trackingStatusBar.tooltip = isTracking 64 | ? 'Click to stop tracking' 65 | : 'Click to start tracking'; 66 | this.trackingStatusBar.command = isTracking 67 | ? 'devtrack.stopTracking' 68 | : 'devtrack.startTracking'; 69 | } 70 | 71 | public updateAuthStatus(isConnected: boolean) { 72 | this.authStatusBar.text = isConnected 73 | ? '$(check) DevTrack: Connected' 74 | : '$(mark-github) DevTrack: Not Connected'; 75 | this.authStatusBar.tooltip = isConnected 76 | ? 'Connected to GitHub' 77 | : 'Click to connect to GitHub'; 78 | this.authStatusBar.command = isConnected 79 | ? 'devtrack.logout' 80 | : 'devtrack.login'; 81 | } 82 | 83 | public dispose() { 84 | this.workspaceStatusBar.dispose(); 85 | this.trackingStatusBar.dispose(); 86 | this.authStatusBar.dispose(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/services/summaryGenerator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-useless-escape */ 3 | // src/services/summaryGenerator.ts 4 | import * as vscode from 'vscode'; 5 | import * as path from 'path'; 6 | import { Change } from './tracker'; 7 | import { ProjectContext } from './projectContext'; 8 | import { ActivityMetrics } from './tracker'; 9 | import { ChangeAnalyzer, ChangeType, ChangeAnalysis } from './changeAnalyzer'; 10 | 11 | export class SummaryGenerator { 12 | private outputChannel: vscode.OutputChannel; 13 | private projectContext: ProjectContext; 14 | private changeAnalyzer: ChangeAnalyzer; 15 | 16 | constructor( 17 | outputChannel: vscode.OutputChannel, 18 | extensionContext: vscode.ExtensionContext 19 | ) { 20 | this.outputChannel = outputChannel; 21 | this.projectContext = new ProjectContext(outputChannel, extensionContext); 22 | this.changeAnalyzer = new ChangeAnalyzer(outputChannel); 23 | } 24 | 25 | private async getFileContent(uri: vscode.Uri): Promise { 26 | try { 27 | const document = await vscode.workspace.openTextDocument(uri); 28 | return document.getText(); 29 | } catch (error) { 30 | this.outputChannel.appendLine(`Error reading file content: ${error}`); 31 | return ''; 32 | } 33 | } 34 | 35 | private async getFileChanges( 36 | change: Change 37 | ): Promise<{ details: string; snippet: string }> { 38 | try { 39 | const oldUri = change.type === 'added' ? undefined : change.uri; 40 | const newUri = change.type === 'deleted' ? undefined : change.uri; 41 | 42 | if (!oldUri && !newUri) { 43 | return { details: '', snippet: '' }; 44 | } 45 | 46 | const gitExt = vscode.extensions.getExtension('vscode.git'); 47 | if (!gitExt) { 48 | return { details: '', snippet: '' }; 49 | } 50 | 51 | const git = gitExt.exports.getAPI(1); 52 | if (!git.repositories.length) { 53 | return { details: '', snippet: '' }; 54 | } 55 | 56 | const repo = git.repositories[0]; 57 | const diff = await repo.diff(oldUri, newUri); 58 | const parsedChanges = await this.parseDiff(diff, change.uri); 59 | 60 | // Get the current content of the file for the snippet 61 | const currentContent = 62 | change.type !== 'deleted' ? await this.getFileContent(change.uri) : ''; 63 | 64 | return { 65 | details: parsedChanges, 66 | snippet: this.formatCodeSnippet( 67 | currentContent, 68 | path.basename(change.uri.fsPath) 69 | ), 70 | }; 71 | } catch (error) { 72 | this.outputChannel.appendLine(`Error getting file changes: ${error}`); 73 | return { details: '', snippet: '' }; 74 | } 75 | } 76 | 77 | private formatCodeSnippet(content: string, filename: string): string { 78 | // Only include up to 50 lines of code to keep commits reasonable 79 | const lines = content.split('\n').slice(0, 50); 80 | if (content.split('\n').length > 50) { 81 | lines.push('... (truncated for brevity)'); 82 | } 83 | 84 | return `\`\`\`\n${filename}:\n${lines.join('\n')}\n\`\`\``; 85 | } 86 | 87 | private parseDiff(diff: string, uri: vscode.Uri): string { 88 | if (!diff) { 89 | return path.basename(uri.fsPath); 90 | } 91 | 92 | const lines = diff.split('\n'); 93 | const changes: { 94 | modified: Set; 95 | added: Set; 96 | removed: Set; 97 | } = { 98 | modified: new Set(), 99 | added: new Set(), 100 | removed: new Set(), 101 | }; 102 | 103 | let currentFunction = ''; 104 | 105 | for (const line of lines) { 106 | if (!line.trim() || line.match(/^[\+\-]\s*\/\//)) { 107 | continue; 108 | } 109 | 110 | const functionMatch = line.match( 111 | /^([\+\-])\s*(async\s+)?((function|class|const|let|var)\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)/ 112 | ); 113 | 114 | if (functionMatch) { 115 | const [_, changeType, _async, _keyword, _type, name] = functionMatch; 116 | 117 | if (changeType === '+') { 118 | changes.added.add(name); 119 | } else if (changeType === '-') { 120 | changes.removed.add(name); 121 | } 122 | 123 | if (changes.added.has(name) && changes.removed.has(name)) { 124 | changes.modified.add(name); 125 | changes.added.delete(name); 126 | changes.removed.delete(name); 127 | } 128 | } 129 | } 130 | 131 | const descriptions: string[] = []; 132 | const filename = path.basename(uri.fsPath); 133 | 134 | if (changes.modified.size > 0) { 135 | descriptions.push(`modified ${Array.from(changes.modified).join(', ')}`); 136 | } 137 | if (changes.added.size > 0) { 138 | descriptions.push(`added ${Array.from(changes.added).join(', ')}`); 139 | } 140 | if (changes.removed.size > 0) { 141 | descriptions.push(`removed ${Array.from(changes.removed).join(', ')}`); 142 | } 143 | 144 | return descriptions.length > 0 145 | ? `${filename} (${descriptions.join('; ')})` 146 | : filename; 147 | } 148 | 149 | private formatDuration(seconds: number): string { 150 | if (seconds < 60) { 151 | return `${seconds} seconds`; 152 | } 153 | 154 | const minutes = Math.floor(seconds / 60); 155 | if (minutes < 60) { 156 | return `${minutes} minute${minutes !== 1 ? 's' : ''}`; 157 | } 158 | 159 | const hours = Math.floor(minutes / 60); 160 | const remainingMinutes = minutes % 60; 161 | 162 | if (remainingMinutes === 0) { 163 | return `${hours} hour${hours !== 1 ? 's' : ''}`; 164 | } else { 165 | return `${hours} hour${hours !== 1 ? 's' : ''} ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}`; 166 | } 167 | } 168 | 169 | private formatActivitySummary( 170 | metrics: ActivityMetrics, 171 | changeType: ChangeType 172 | ): string { 173 | const formattedDuration = this.formatDuration(metrics.activeTime); 174 | 175 | let activitySummary = `Active coding time: ${formattedDuration}`; 176 | 177 | if (metrics.keystrokes > 0) { 178 | activitySummary += `, ${metrics.keystrokes} keystrokes`; 179 | } 180 | 181 | if (metrics.fileChanges > 0) { 182 | activitySummary += `, ${metrics.fileChanges} file change events`; 183 | } 184 | 185 | // Add change type description 186 | let changeDescription = ''; 187 | switch (changeType) { 188 | case 'feature': 189 | changeDescription = '✨ Feature development'; 190 | break; 191 | case 'bugfix': 192 | changeDescription = '🐛 Bug fixing'; 193 | break; 194 | case 'refactor': 195 | changeDescription = '♻️ Code refactoring'; 196 | break; 197 | case 'docs': 198 | changeDescription = '📝 Documentation'; 199 | break; 200 | case 'style': 201 | changeDescription = '💄 Styling/formatting'; 202 | break; 203 | default: 204 | changeDescription = '👨‍💻 Coding session'; 205 | } 206 | 207 | return `${changeDescription} (${activitySummary})`; 208 | } 209 | 210 | async generateSummary( 211 | changedFiles: Change[], 212 | activityMetrics?: ActivityMetrics 213 | ): Promise { 214 | try { 215 | const timestamp = new Date().toISOString(); 216 | const localTime = new Date().toLocaleString(); 217 | let summary = `DevTrack Update - ${localTime}\n\n`; 218 | 219 | // Analyze the type of changes 220 | const changeAnalysis = 221 | await this.changeAnalyzer.analyzeChanges(changedFiles); 222 | 223 | // Add activity metrics if available 224 | if (activityMetrics) { 225 | summary += 226 | this.formatActivitySummary(activityMetrics, changeAnalysis.type) + 227 | '\n\n'; 228 | } 229 | 230 | // Get project context 231 | const projectContext = 232 | this.projectContext.getContextForSummary(changedFiles); 233 | if (projectContext) { 234 | summary += projectContext + '\n'; 235 | } 236 | 237 | // Get detailed file changes and snippets 238 | const changePromises = changedFiles.map(async (change) => { 239 | const { details, snippet } = await this.getFileChanges(change); 240 | return { 241 | details, 242 | snippet, 243 | type: change.type, 244 | lineCount: change.lineCount || 0, 245 | charCount: change.charCount || 0, 246 | }; 247 | }); 248 | 249 | const changes = await Promise.all(changePromises); 250 | 251 | // Add change details 252 | summary += 'Changes:\n'; 253 | changes.forEach((change) => { 254 | if (change.details) { 255 | const metrics = change.lineCount 256 | ? ` (${change.lineCount} lines)` 257 | : ''; 258 | summary += `- ${change.type}: ${change.details}${metrics}\n`; 259 | } 260 | }); 261 | 262 | // Add code snippets 263 | summary += '\nCode Snippets:\n'; 264 | changes.forEach((change) => { 265 | if (change.snippet) { 266 | summary += `\n${change.snippet}\n`; 267 | } 268 | }); 269 | 270 | // Add analysis details if confidence is high enough 271 | if ( 272 | changeAnalysis.confidence > 0.6 && 273 | changeAnalysis.details.length > 0 274 | ) { 275 | summary += '\nAnalysis:\n'; 276 | changeAnalysis.details.forEach((detail) => { 277 | summary += `- ${detail}\n`; 278 | }); 279 | } 280 | 281 | // Save commit info 282 | await this.projectContext.addCommit(summary, changedFiles); 283 | this.outputChannel.appendLine( 284 | `DevTrack: Generated commit summary with code snippets and activity metrics` 285 | ); 286 | 287 | return summary; 288 | } catch (error) { 289 | this.outputChannel.appendLine( 290 | `DevTrack: Error generating summary: ${error}` 291 | ); 292 | return `DevTrack Update - ${new Date().toISOString()}\nUpdated files`; 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/services/tracker.ts: -------------------------------------------------------------------------------- 1 | // src/services/tracker.ts 2 | import { setTimeout, clearTimeout } from 'node:timers'; 3 | import * as vscode from 'vscode'; 4 | import { EventEmitter } from 'events'; 5 | import { minimatch } from 'minimatch'; 6 | import { OutputChannel } from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | export interface Change { 10 | uri: vscode.Uri; 11 | timestamp: Date; 12 | type: 'added' | 'changed' | 'deleted'; 13 | // Added metrics to each change 14 | lineCount?: number; 15 | charCount?: number; 16 | } 17 | 18 | export interface ActivityMetrics { 19 | activeTime: number; // in seconds 20 | fileChanges: number; 21 | keystrokes: number; 22 | lastActiveTimestamp: Date; 23 | } 24 | 25 | export class Tracker extends EventEmitter { 26 | private changes: Map = new Map(); 27 | private watcher!: vscode.FileSystemWatcher; 28 | private excludePatterns: string[] = []; 29 | private outputChannel: OutputChannel; 30 | private trackingDir: string; 31 | private isInitialized: boolean = false; 32 | private isTracking: boolean = false; 33 | 34 | // New activity tracking 35 | private activityTimeout: ReturnType | null = null; 36 | private keystrokeCount: number = 0; 37 | private activeStartTime: Date | null = null; 38 | private totalActiveTime: number = 0; // in seconds 39 | private activityMetrics: ActivityMetrics = { 40 | activeTime: 0, 41 | fileChanges: 0, 42 | keystrokes: 0, 43 | lastActiveTimestamp: new Date(), 44 | }; 45 | 46 | // Track idle time (default 5 minutes) 47 | private readonly IDLE_TIMEOUT_MS = 5 * 60 * 1000; 48 | // Suppress frequent logging 49 | private lastLogTimestamp: number = 0; 50 | private readonly LOG_THROTTLE_MS = 5000; // 5 seconds 51 | 52 | constructor(outputChannel: OutputChannel, trackingDir: string) { 53 | super(); 54 | this.outputChannel = outputChannel; 55 | this.trackingDir = trackingDir; 56 | this.initialize(); 57 | } 58 | 59 | private async initialize() { 60 | try { 61 | // Wait for workspace to be fully loaded 62 | if (!vscode.workspace.workspaceFolders?.length) { 63 | this.log('Waiting for workspace to load...'); 64 | const disposable = vscode.workspace.onDidChangeWorkspaceFolders(() => { 65 | if (vscode.workspace.workspaceFolders?.length) { 66 | this.initializeWatcher(); 67 | disposable.dispose(); 68 | } 69 | }); 70 | } else { 71 | await this.initializeWatcher(); 72 | this.setupActivityTracking(); 73 | } 74 | } catch (error) { 75 | this.outputChannel.appendLine( 76 | `DevTrack: Initialization error - ${error}` 77 | ); 78 | } 79 | } 80 | 81 | private async initializeWatcher() { 82 | try { 83 | const config = vscode.workspace.getConfiguration('devtrack'); 84 | this.excludePatterns = config.get('exclude') || []; 85 | 86 | // Log current workspace state 87 | const workspaceFolders = vscode.workspace.workspaceFolders; 88 | if (!workspaceFolders || workspaceFolders.length === 0) { 89 | this.log('No workspace folder found'); 90 | return; 91 | } 92 | 93 | const workspaceFolder = workspaceFolders[0]; 94 | this.log( 95 | `Initializing watcher for workspace: ${workspaceFolder.uri.fsPath}` 96 | ); 97 | 98 | // Create watcher with specific glob pattern for code files 99 | const filePattern = new vscode.RelativePattern( 100 | workspaceFolder, 101 | '**/*.{ts,js,py,java,c,cpp,h,hpp,css,scss,html,jsx,tsx,vue,php,rb,go,rs,swift,md,json,yml,yaml}' 102 | ); 103 | 104 | // Dispose existing watcher if it exists 105 | if (this.watcher) { 106 | this.watcher.dispose(); 107 | } 108 | 109 | this.watcher = vscode.workspace.createFileSystemWatcher( 110 | filePattern, 111 | false, // Don't ignore creates 112 | false, // Don't ignore changes 113 | false // Don't ignore deletes 114 | ); 115 | 116 | // Set up event handlers with logging (but throttled) 117 | this.watcher.onDidChange((uri) => { 118 | this.logThrottled(`Change detected in file: ${uri.fsPath}`); 119 | this.handleChange(uri, 'changed'); 120 | this.recordActivity(); 121 | }); 122 | 123 | this.watcher.onDidCreate((uri) => { 124 | this.logThrottled(`New file created: ${uri.fsPath}`); 125 | this.handleChange(uri, 'added'); 126 | this.recordActivity(); 127 | }); 128 | 129 | this.watcher.onDidDelete((uri) => { 130 | this.logThrottled(`File deleted: ${uri.fsPath}`); 131 | this.handleChange(uri, 'deleted'); 132 | this.recordActivity(); 133 | }); 134 | 135 | // Verify the watcher is active 136 | this.isInitialized = true; 137 | this.log('File system watcher successfully initialized'); 138 | } catch (error) { 139 | this.outputChannel.appendLine( 140 | `DevTrack: Failed to initialize watcher - ${error}` 141 | ); 142 | this.isInitialized = false; 143 | } 144 | } 145 | 146 | private setupActivityTracking() { 147 | // Track text document changes 148 | vscode.workspace.onDidChangeTextDocument((event) => { 149 | if (event.contentChanges.length > 0) { 150 | this.keystrokeCount += event.contentChanges.reduce( 151 | (total, change) => total + (change.text?.length || 0), 152 | 0 153 | ); 154 | this.recordActivity(); 155 | } 156 | }); 157 | 158 | // Track when editor becomes active 159 | vscode.window.onDidChangeActiveTextEditor(() => { 160 | this.recordActivity(); 161 | }); 162 | 163 | // Track cursor movements & selection changes 164 | vscode.window.onDidChangeTextEditorSelection(() => { 165 | this.recordActivity(); 166 | }); 167 | 168 | // Start active time tracking 169 | this.startActivityTracking(); 170 | } 171 | 172 | public startTracking() { 173 | this.isTracking = true; 174 | this.startActivityTracking(); 175 | this.log('Tracking started'); 176 | } 177 | 178 | public stopTracking() { 179 | this.isTracking = false; 180 | this.pauseActivityTracking(); 181 | this.log('Tracking stopped'); 182 | } 183 | 184 | private startActivityTracking() { 185 | if (!this.isTracking) { 186 | return; 187 | } 188 | 189 | if (!this.activeStartTime) { 190 | this.activeStartTime = new Date(); 191 | this.logThrottled('Starting activity tracking'); 192 | } 193 | 194 | // Clear any existing timeout 195 | if (this.activityTimeout) { 196 | clearTimeout(this.activityTimeout); 197 | } 198 | 199 | // Set a new timeout to detect inactivity 200 | this.activityTimeout = setTimeout(() => { 201 | this.pauseActivityTracking(); 202 | }, this.IDLE_TIMEOUT_MS); 203 | } 204 | 205 | private pauseActivityTracking() { 206 | if (this.activeStartTime) { 207 | // Calculate active time 208 | const now = new Date(); 209 | const activeTime = 210 | (now.getTime() - this.activeStartTime.getTime()) / 1000; 211 | this.totalActiveTime += activeTime; 212 | this.activeStartTime = null; 213 | 214 | this.activityMetrics.activeTime = this.totalActiveTime; 215 | this.activityMetrics.keystrokes = this.keystrokeCount; 216 | 217 | this.log( 218 | `Activity paused. Active time: ${Math.round(this.totalActiveTime / 60)} minutes` 219 | ); 220 | 221 | // Emit metrics event so other services can use it 222 | this.emit('activityMetrics', this.activityMetrics); 223 | } 224 | 225 | if (this.activityTimeout) { 226 | clearTimeout(this.activityTimeout); 227 | this.activityTimeout = null; 228 | } 229 | } 230 | 231 | private recordActivity() { 232 | if (!this.isTracking) { 233 | return; 234 | } 235 | 236 | this.activityMetrics.lastActiveTimestamp = new Date(); 237 | this.startActivityTracking(); // Restart the idle timer 238 | } 239 | 240 | private async analyzeFileContent( 241 | uri: vscode.Uri 242 | ): Promise<{ lineCount: number; charCount: number }> { 243 | try { 244 | const document = await vscode.workspace.openTextDocument(uri); 245 | return { 246 | lineCount: document.lineCount, 247 | charCount: document.getText().length, 248 | }; 249 | } catch (error) { 250 | return { lineCount: 0, charCount: 0 }; 251 | } 252 | } 253 | 254 | private shouldTrackFile(filePath: string): boolean { 255 | // Skip files in tracking directory 256 | if (filePath.includes(this.trackingDir)) { 257 | return false; 258 | } 259 | 260 | // Check exclusions 261 | const relativePath = vscode.workspace.asRelativePath(filePath); 262 | const isExcluded = this.excludePatterns.some((pattern) => 263 | minimatch(relativePath, pattern) 264 | ); 265 | if (isExcluded) { 266 | return false; 267 | } 268 | 269 | // Check file extension 270 | const fileExt = path.extname(filePath).toLowerCase().slice(1); 271 | const trackedExtensions = [ 272 | 'ts', 273 | 'js', 274 | 'py', 275 | 'java', 276 | 'c', 277 | 'cpp', 278 | 'h', 279 | 'hpp', 280 | 'css', 281 | 'scss', 282 | 'html', 283 | 'jsx', 284 | 'tsx', 285 | 'vue', 286 | 'php', 287 | 'rb', 288 | 'go', 289 | 'rs', 290 | 'swift', 291 | 'md', 292 | 'json', 293 | 'yml', 294 | 'yaml', 295 | ]; 296 | 297 | return Boolean(fileExt) && trackedExtensions.includes(fileExt); 298 | } 299 | 300 | private async handleChange( 301 | uri: vscode.Uri, 302 | type: 'added' | 'changed' | 'deleted' 303 | ) { 304 | try { 305 | if (!this.isInitialized) { 306 | this.log('Watcher not initialized, reinitializing...'); 307 | this.initialize(); 308 | return; 309 | } 310 | 311 | if (!this.shouldTrackFile(uri.fsPath)) { 312 | return; 313 | } 314 | 315 | // Analyze file for metrics 316 | let metrics = { lineCount: 0, charCount: 0 }; 317 | if (type !== 'deleted') { 318 | metrics = await this.analyzeFileContent(uri); 319 | } 320 | 321 | // Check if this is a meaningful change 322 | const existingChange = this.changes.get(uri.fsPath); 323 | if (existingChange) { 324 | if (existingChange.type === 'deleted' && type === 'added') { 325 | type = 'added'; 326 | } else if (existingChange.type === 'added' && type === 'changed') { 327 | type = 'added'; 328 | } 329 | } 330 | 331 | // Update or add the change 332 | const change: Change = { 333 | uri, 334 | timestamp: new Date(), 335 | type, 336 | lineCount: metrics.lineCount, 337 | charCount: metrics.charCount, 338 | }; 339 | 340 | this.changes.set(uri.fsPath, change); 341 | this.activityMetrics.fileChanges++; 342 | this.emit('change', change); 343 | } catch (error) { 344 | this.outputChannel.appendLine( 345 | `DevTrack: Error handling file change: ${error}` 346 | ); 347 | } 348 | } 349 | 350 | getChangedFiles(): Change[] { 351 | const changes = Array.from(this.changes.values()); 352 | return changes; 353 | } 354 | 355 | clearChanges(): void { 356 | const previousCount = this.changes.size; 357 | this.changes.clear(); 358 | this.log(`Cleared ${previousCount} tracked changes`); 359 | } 360 | 361 | updateExcludePatterns(newPatterns: string[]) { 362 | this.excludePatterns = newPatterns; 363 | this.log(`Updated exclude patterns to: ${newPatterns.join(', ')}`); 364 | } 365 | 366 | async reinitialize() { 367 | this.log('Reinitializing tracker...'); 368 | await this.initialize(); 369 | } 370 | 371 | // Returns activity metrics for the current session 372 | getActivityMetrics(): ActivityMetrics { 373 | // If currently active, update the active time 374 | if (this.activeStartTime) { 375 | const now = new Date(); 376 | const currentActiveTime = 377 | (now.getTime() - this.activeStartTime.getTime()) / 1000; 378 | this.activityMetrics.activeTime = 379 | this.totalActiveTime + currentActiveTime; 380 | } else { 381 | this.activityMetrics.activeTime = this.totalActiveTime; 382 | } 383 | 384 | return this.activityMetrics; 385 | } 386 | 387 | // Reset metrics after they've been committed 388 | resetMetrics() { 389 | this.totalActiveTime = 0; 390 | this.keystrokeCount = 0; 391 | this.activityMetrics = { 392 | activeTime: 0, 393 | fileChanges: 0, 394 | keystrokes: 0, 395 | lastActiveTimestamp: new Date(), 396 | }; 397 | } 398 | 399 | private log(message: string) { 400 | this.outputChannel.appendLine(`DevTrack: ${message}`); 401 | } 402 | 403 | private logThrottled(message: string) { 404 | const now = Date.now(); 405 | if (now - this.lastLogTimestamp > this.LOG_THROTTLE_MS) { 406 | this.log(message); 407 | this.lastLogTimestamp = now; 408 | } 409 | } 410 | 411 | dispose() { 412 | if (this.watcher) { 413 | this.watcher.dispose(); 414 | this.isInitialized = false; 415 | this.log('Disposed file system watcher'); 416 | } 417 | 418 | if (this.activityTimeout) { 419 | clearTimeout(this.activityTimeout); 420 | this.activityTimeout = null; 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/services/websiteGenerator.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // src/services/websiteGenerator.ts 3 | import * as vscode from 'vscode'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | import { OutputChannel } from 'vscode'; 7 | 8 | // Sample data for initial website 9 | const DEFAULT_STATS = { 10 | activityTimeline: [ 11 | { date: '2025-01-01', commits: 5, filesChanged: 12, linesChanged: 102 }, 12 | { date: '2025-01-02', commits: 3, filesChanged: 7, linesChanged: 64 }, 13 | { date: '2025-01-03', commits: 6, filesChanged: 14, linesChanged: 155 }, 14 | { date: '2025-01-05', commits: 2, filesChanged: 5, linesChanged: 43 }, 15 | { date: '2025-01-07', commits: 4, filesChanged: 9, linesChanged: 89 }, 16 | ], 17 | fileTypes: [ 18 | { type: 'JS', count: 12 }, 19 | { type: 'TS', count: 18 }, 20 | { type: 'CSS', count: 5 }, 21 | { type: 'HTML', count: 3 }, 22 | { type: 'JSON', count: 7 }, 23 | ], 24 | timeDistribution: [ 25 | { hour: '9AM', changes: 12 }, 26 | { hour: '12PM', changes: 5 }, 27 | { hour: '3PM', changes: 8 }, 28 | { hour: '6PM', changes: 15 }, 29 | { hour: '9PM', changes: 20 }, 30 | ], 31 | totalTime: 24.5, 32 | filesModified: 54, 33 | totalCommits: 82, 34 | linesChanged: 1146, 35 | }; 36 | 37 | export class WebsiteGenerator { 38 | private outputChannel: OutputChannel; 39 | private trackingDir: string; 40 | 41 | constructor(outputChannel: OutputChannel, trackingDir: string) { 42 | this.outputChannel = outputChannel; 43 | this.trackingDir = trackingDir; 44 | } 45 | 46 | /** 47 | * Generate all necessary files for the statistics website in the tracking repository 48 | */ 49 | public async generateWebsite(): Promise { 50 | try { 51 | const statsDir = path.join(this.trackingDir, 'stats'); 52 | await fs.promises.mkdir(statsDir, { recursive: true }); 53 | 54 | // Create basic website structure 55 | await this.createBaseStructure(statsDir); 56 | 57 | // Create React component directories 58 | const srcDir = path.join(statsDir, 'src'); 59 | const componentsDir = path.join(srcDir, 'components'); 60 | const uiDir = path.join(componentsDir, 'ui'); 61 | 62 | await fs.promises.mkdir(srcDir, { recursive: true }); 63 | await fs.promises.mkdir(componentsDir, { recursive: true }); 64 | await fs.promises.mkdir(uiDir, { recursive: true }); 65 | 66 | // Copy existing component files or create new ones 67 | await this.copyDashboardComponent(componentsDir); 68 | await this.createUIComponents(uiDir); 69 | 70 | // Create GitHub Actions workflow for deployment 71 | await this.createGitHubWorkflow(); 72 | 73 | this.outputChannel.appendLine( 74 | 'DevTrack: Statistics website files generated successfully' 75 | ); 76 | } catch (error) { 77 | this.outputChannel.appendLine( 78 | `DevTrack: Error generating website files - ${error}` 79 | ); 80 | throw error; 81 | } 82 | } 83 | 84 | /** 85 | * Create UI components needed for the dashboard 86 | */ 87 | private async createUIComponents(uiDir: string): Promise { 88 | // Create card.tsx component 89 | const cardComponent = ` 90 | import * as React from "react"; 91 | 92 | interface CardProps { 93 | children: React.ReactNode; 94 | className?: string; 95 | } 96 | 97 | export function Card({ 98 | children, 99 | className = '', 100 | ...props 101 | }: CardProps & React.ComponentProps<'div'>) { 102 | return ( 103 |
104 | {children} 105 |
106 | ); 107 | } 108 | 109 | interface CardHeaderProps { 110 | children: React.ReactNode; 111 | className?: string; 112 | } 113 | 114 | export function CardHeader({ 115 | children, 116 | className = '', 117 | ...props 118 | }: CardHeaderProps & React.ComponentProps<'div'>) { 119 | return ( 120 |
121 | {children} 122 |
123 | ); 124 | } 125 | 126 | interface CardTitleProps { 127 | children: React.ReactNode; 128 | className?: string; 129 | } 130 | 131 | export function CardTitle({ 132 | children, 133 | className = '', 134 | ...props 135 | }: CardTitleProps & React.ComponentProps<'h3'>) { 136 | return ( 137 |

141 | {children} 142 |

143 | ); 144 | } 145 | 146 | interface CardContentProps { 147 | children: React.ReactNode; 148 | className?: string; 149 | } 150 | 151 | export function CardContent({ 152 | children, 153 | className = '', 154 | ...props 155 | }: CardContentProps & React.ComponentProps<'div'>) { 156 | return ( 157 |
158 | {children} 159 |
160 | ); 161 | } 162 | `; 163 | 164 | await fs.promises.writeFile(path.join(uiDir, 'card.tsx'), cardComponent); 165 | } 166 | 167 | /** 168 | * Create GitHub Actions workflow for deployment 169 | */ 170 | private async createGitHubWorkflow(): Promise { 171 | try { 172 | // Create .github/workflows directory 173 | const workflowsDir = path.join(this.trackingDir, '.github', 'workflows'); 174 | await fs.promises.mkdir(workflowsDir, { recursive: true }); 175 | 176 | // Create GitHub Pages deployment workflow 177 | const workflowContent = `name: Deploy Stats Website 178 | 179 | on: 180 | push: 181 | branches: [ main ] 182 | paths: 183 | - 'stats/**' 184 | - 'public/data/**' 185 | 186 | permissions: 187 | contents: read 188 | pages: write 189 | id-token: write 190 | 191 | # Allow only one concurrent deployment 192 | concurrency: 193 | group: "pages" 194 | cancel-in-progress: true 195 | 196 | jobs: 197 | build: 198 | runs-on: ubuntu-latest 199 | steps: 200 | - name: Checkout 201 | uses: actions/checkout@v3 202 | 203 | - name: Setup Node 204 | uses: actions/setup-node@v3 205 | with: 206 | node-version: "18" 207 | cache: 'npm' 208 | cache-dependency-path: './stats/package-lock.json' 209 | 210 | - name: Setup Pages 211 | uses: actions/configure-pages@v3 212 | 213 | - name: Install dependencies 214 | run: | 215 | cd stats 216 | npm ci 217 | 218 | - name: Build 219 | run: | 220 | cd stats 221 | npm run build 222 | 223 | - name: Upload artifact 224 | uses: actions/upload-pages-artifact@v2 225 | with: 226 | path: './stats/dist' 227 | 228 | deploy: 229 | environment: 230 | name: github-pages 231 | url: \${{ steps.deployment.outputs.page_url }} 232 | runs-on: ubuntu-latest 233 | needs: build 234 | steps: 235 | - name: Deploy to GitHub Pages 236 | id: deployment 237 | uses: actions/deploy-pages@v2 238 | `; 239 | 240 | const workflowPath = path.join(workflowsDir, 'deploy-stats.yml'); 241 | await fs.promises.writeFile(workflowPath, workflowContent); 242 | 243 | this.outputChannel.appendLine( 244 | 'DevTrack: Created GitHub Actions workflow for website deployment' 245 | ); 246 | } catch (error) { 247 | this.outputChannel.appendLine( 248 | `DevTrack: Error creating GitHub workflow - ${error}` 249 | ); 250 | } 251 | } 252 | 253 | /** 254 | * Create base structure for the React application 255 | */ 256 | private async createBaseStructure(statsDir: string): Promise { 257 | // Create package.json 258 | const packageJson = { 259 | name: 'devtrack-stats', 260 | private: true, 261 | version: '1.0.0', 262 | type: 'module', 263 | scripts: { 264 | dev: 'vite', 265 | build: 'vite build', 266 | preview: 'vite preview', 267 | }, 268 | dependencies: { 269 | react: '^18.2.0', 270 | 'react-dom': '^18.2.0', 271 | recharts: '^2.12.0', 272 | 'lucide-react': '^0.330.0', 273 | }, 274 | devDependencies: { 275 | '@types/react': '^18.2.55', 276 | '@types/react-dom': '^18.2.19', 277 | '@vitejs/plugin-react': '^4.2.1', 278 | typescript: '^5.2.2', 279 | vite: '^5.1.0', 280 | autoprefixer: '^10.4.17', 281 | postcss: '^8.4.35', 282 | tailwindcss: '^3.4.1', 283 | }, 284 | }; 285 | 286 | await fs.promises.writeFile( 287 | path.join(statsDir, 'package.json'), 288 | JSON.stringify(packageJson, null, 2) 289 | ); 290 | 291 | // Create vite.config.ts 292 | const viteConfig = ` 293 | import { defineConfig } from 'vite'; 294 | import react from '@vitejs/plugin-react'; 295 | 296 | // https://vitejs.dev/config/ 297 | export default defineConfig({ 298 | plugins: [react()], 299 | base: './', // Makes it work in GitHub Pages 300 | }); 301 | `; 302 | 303 | await fs.promises.writeFile( 304 | path.join(statsDir, 'vite.config.ts'), 305 | viteConfig 306 | ); 307 | 308 | // Create index.html 309 | const indexHtml = ` 310 | 311 | 312 | 313 | 314 | DevTrack - Coding Statistics 315 | 316 | 317 | 318 |
319 | 320 | 321 | 322 | `; 323 | 324 | await fs.promises.writeFile(path.join(statsDir, 'index.html'), indexHtml); 325 | 326 | // Create tsconfig.json 327 | const tsConfig = { 328 | compilerOptions: { 329 | target: 'ES2020', 330 | useDefineForClassFields: true, 331 | lib: ['ES2020', 'DOM', 'DOM.Iterable'], 332 | module: 'ESNext', 333 | skipLibCheck: true, 334 | moduleResolution: 'bundler', 335 | allowImportingTsExtensions: true, 336 | resolveJsonModule: true, 337 | isolatedModules: true, 338 | noEmit: true, 339 | jsx: 'react-jsx', 340 | strict: true, 341 | noUnusedLocals: true, 342 | noUnusedParameters: true, 343 | noFallthroughCasesInSwitch: true, 344 | }, 345 | include: ['src'], 346 | references: [{ path: './tsconfig.node.json' }], 347 | }; 348 | 349 | await fs.promises.writeFile( 350 | path.join(statsDir, 'tsconfig.json'), 351 | JSON.stringify(tsConfig, null, 2) 352 | ); 353 | 354 | // Create tsconfig.node.json 355 | const tsNodeConfig = { 356 | compilerOptions: { 357 | composite: true, 358 | skipLibCheck: true, 359 | module: 'ESNext', 360 | moduleResolution: 'bundler', 361 | allowSyntheticDefaultImports: true, 362 | }, 363 | include: ['vite.config.ts'], 364 | }; 365 | 366 | await fs.promises.writeFile( 367 | path.join(statsDir, 'tsconfig.node.json'), 368 | JSON.stringify(tsNodeConfig, null, 2) 369 | ); 370 | 371 | // Create postcss.config.js 372 | const postcssConfig = ` 373 | export default { 374 | plugins: { 375 | tailwindcss: {}, 376 | autoprefixer: {}, 377 | }, 378 | } 379 | `; 380 | 381 | await fs.promises.writeFile( 382 | path.join(statsDir, 'postcss.config.js'), 383 | postcssConfig 384 | ); 385 | 386 | // Create tailwind.config.js 387 | const tailwindConfig = ` 388 | /** @type {import('tailwindcss').Config} */ 389 | export default { 390 | content: [ 391 | "./index.html", 392 | "./src/**/*.{js,ts,jsx,tsx}", 393 | ], 394 | theme: { 395 | extend: {}, 396 | }, 397 | plugins: [], 398 | darkMode: 'class', 399 | } 400 | `; 401 | 402 | await fs.promises.writeFile( 403 | path.join(statsDir, 'tailwind.config.js'), 404 | tailwindConfig 405 | ); 406 | 407 | // Create src/index.css 408 | const srcDir = path.join(statsDir, 'src'); 409 | await fs.promises.mkdir(srcDir, { recursive: true }); 410 | 411 | const indexCss = ` 412 | @tailwind base; 413 | @tailwind components; 414 | @tailwind utilities; 415 | 416 | :root { 417 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 418 | line-height: 1.5; 419 | font-weight: 400; 420 | } 421 | 422 | body { 423 | margin: 0; 424 | min-width: 320px; 425 | min-height: 100vh; 426 | } 427 | 428 | /* Dark mode styles */ 429 | .dark { 430 | color-scheme: dark; 431 | } 432 | `; 433 | 434 | await fs.promises.writeFile(path.join(srcDir, 'index.css'), indexCss); 435 | 436 | // Create src/main.tsx 437 | const mainTsx = ` 438 | import React from 'react' 439 | import ReactDOM from 'react-dom/client' 440 | import CodingStatsDashboard from './components/CodingStatsDashboard' 441 | import './index.css' 442 | 443 | ReactDOM.createRoot(document.getElementById('root')!).render( 444 | 445 | 446 | , 447 | ) 448 | `; 449 | 450 | await fs.promises.writeFile(path.join(srcDir, 'main.tsx'), mainTsx); 451 | 452 | // Create a types file for data 453 | const typesFile = ` 454 | export interface ActivityData { 455 | date: string; 456 | commits: number; 457 | filesChanged: number; 458 | linesChanged: number; 459 | } 460 | 461 | export interface FileStats { 462 | type: string; 463 | count: number; 464 | } 465 | 466 | export interface TimeDistribution { 467 | hour: string; 468 | changes: number; 469 | } 470 | 471 | export interface CodingStats { 472 | activityTimeline: ActivityData[]; 473 | fileTypes: FileStats[]; 474 | timeDistribution: TimeDistribution[]; 475 | totalTime?: number; 476 | filesModified?: number; 477 | totalCommits?: number; 478 | linesChanged?: number; 479 | } 480 | `; 481 | 482 | await fs.promises.writeFile(path.join(srcDir, 'types.ts'), typesFile); 483 | 484 | // Create a data loader utility 485 | const dataUtil = ` 486 | import { CodingStats } from './types'; 487 | 488 | // Default sample data in case stats.json doesn't exist yet 489 | const DEFAULT_STATS: CodingStats = ${JSON.stringify(DEFAULT_STATS, null, 2)}; 490 | 491 | export async function loadStats(): Promise { 492 | try { 493 | // Try to load stats.json 494 | const response = await fetch('./data/stats.json'); 495 | if (response.ok) { 496 | return await response.json(); 497 | } 498 | console.warn('Stats data not found, using default data'); 499 | return DEFAULT_STATS; 500 | } catch (error) { 501 | console.error('Error loading stats:', error); 502 | return DEFAULT_STATS; 503 | } 504 | } 505 | `; 506 | 507 | await fs.promises.writeFile(path.join(srcDir, 'dataUtils.ts'), dataUtil); 508 | 509 | // Create data directory 510 | const dataDir = path.join(statsDir, 'public', 'data'); 511 | await fs.promises.mkdir(dataDir, { recursive: true }); 512 | 513 | // Create sample stats.json 514 | await fs.promises.writeFile( 515 | path.join(dataDir, 'stats.json'), 516 | JSON.stringify(DEFAULT_STATS, null, 2) 517 | ); 518 | } 519 | 520 | /** 521 | * Copy and adapt the existing dashboard component 522 | */ 523 | private async copyDashboardComponent(componentsDir: string): Promise { 524 | // Get workspace folders 525 | const workspaceFolders = vscode.workspace.workspaceFolders; 526 | if (!workspaceFolders || workspaceFolders.length === 0) { 527 | throw new Error('No workspace folder is open'); 528 | } 529 | 530 | // Try to find the existing component 531 | const workspaceRoot = workspaceFolders[0].uri.fsPath; 532 | const possiblePaths = [ 533 | path.join(workspaceRoot, 'src', 'components', 'CodingStatsDashboard.tsx'), 534 | path.join( 535 | workspaceRoot, 536 | 'src', 537 | 'components', 538 | 'ui', 539 | 'CodingStatsDashboard.tsx' 540 | ), 541 | ]; 542 | 543 | let sourceContent = ''; 544 | for (const possiblePath of possiblePaths) { 545 | try { 546 | sourceContent = await fs.promises.readFile(possiblePath, 'utf8'); 547 | this.outputChannel.appendLine( 548 | `DevTrack: Found dashboard component at ${possiblePath}` 549 | ); 550 | break; 551 | } catch (error) { 552 | // Continue trying other paths 553 | } 554 | } 555 | 556 | // If we couldn't find the component, create a new one based on what we know 557 | if (!sourceContent) { 558 | sourceContent = this.generateDashboardComponent(); 559 | this.outputChannel.appendLine( 560 | 'DevTrack: Created new dashboard component' 561 | ); 562 | } else { 563 | // Adapt the component to the new environment 564 | sourceContent = this.adaptDashboardComponent(sourceContent); 565 | this.outputChannel.appendLine( 566 | 'DevTrack: Adapted existing dashboard component' 567 | ); 568 | } 569 | 570 | await fs.promises.writeFile( 571 | path.join(componentsDir, 'CodingStatsDashboard.tsx'), 572 | sourceContent 573 | ); 574 | } 575 | 576 | /** 577 | * Adapt the existing dashboard component to work in the standalone website 578 | */ 579 | private adaptDashboardComponent(content: string): string { 580 | // Replace type imports with local type definitions 581 | content = content.replace( 582 | /import.*\{.*Card.*\}.*from.*/, 583 | "import { Card, CardContent, CardHeader, CardTitle } from './ui/card';" 584 | ); 585 | 586 | // Add import for the data utilities 587 | content = content.replace( 588 | /import React.*/, 589 | `import React, { useState, useEffect } from 'react';\nimport { loadStats } from '../dataUtils';` 590 | ); 591 | 592 | // Replace VSCode-specific global declarations with our own 593 | const vscodeGlobalReplacement = ` 594 | // Type definitions for our data 595 | import { ActivityData, FileStats, TimeDistribution } from '../types'; 596 | 597 | // Global window types for our app 598 | declare global { 599 | interface Window { 600 | initialStats?: { 601 | activityTimeline: ActivityData[]; 602 | fileTypes: FileStats[]; 603 | timeDistribution: TimeDistribution[]; 604 | }; 605 | } 606 | }`; 607 | 608 | content = content.replace( 609 | /\/\/ Declare global types for VSCode webview[\s\S]*?interface Window {[\s\S]*?}\s*}/, 610 | vscodeGlobalReplacement 611 | ); 612 | 613 | // Update useEffect to use our data loader 614 | const loadDataEffect = ` 615 | useEffect(() => { 616 | // Load statistics data 617 | const loadStatsData = async () => { 618 | try { 619 | const stats = await loadStats(); 620 | setActivityData(stats.activityTimeline || []); 621 | setFileStats(stats.fileTypes || []); 622 | setTimeDistribution(stats.timeDistribution || []); 623 | setLoading(false); 624 | } catch (error) { 625 | console.error('Failed to load statistics:', error); 626 | setLoading(false); 627 | } 628 | }; 629 | 630 | loadStatsData(); 631 | }, []);`; 632 | 633 | // Replace existing useEffect that loads data 634 | content = content.replace( 635 | /useEffect\(\s*\(\)\s*=>\s*{[\s\S]*?Load initial stats[\s\S]*?}\s*\), \[\]/, 636 | loadDataEffect 637 | ); 638 | 639 | if (content.indexOf(loadDataEffect) === -1) { 640 | // If we couldn't find the pattern to replace, try a more general approach 641 | content = content.replace( 642 | /useEffect\(\s*\(\)\s*=>\s*{[\s\S]*?window\.addEventListener[\s\S]*?}\s*\), \[\]/, 643 | loadDataEffect 644 | ); 645 | } 646 | 647 | return content; 648 | } 649 | 650 | /** 651 | * Generate a new dashboard component if we couldn't find the existing one 652 | */ 653 | private generateDashboardComponent(): string { 654 | return `import React, { useState, useEffect } from 'react'; 655 | import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; 656 | import { loadStats } from '../dataUtils'; 657 | import { 658 | LineChart, 659 | Line, 660 | XAxis, 661 | YAxis, 662 | CartesianGrid, 663 | Tooltip, 664 | Legend, 665 | ResponsiveContainer, 666 | BarChart, 667 | Bar, 668 | } from 'recharts'; 669 | import { 670 | Clock, 671 | FileCode, 672 | GitBranch, 673 | ArrowUpDown, 674 | Moon, 675 | Sun, 676 | } from 'lucide-react'; 677 | 678 | // Type definitions for our data 679 | import { ActivityData, FileStats, TimeDistribution } from '../types'; 680 | 681 | // Global window types for our app 682 | declare global { 683 | interface Window { 684 | initialStats?: { 685 | activityTimeline: ActivityData[]; 686 | fileTypes: FileStats[]; 687 | timeDistribution: TimeDistribution[]; 688 | }; 689 | } 690 | } 691 | 692 | const CodingStatsDashboard = () => { 693 | const [activityData, setActivityData] = useState([]); 694 | const [fileStats, setFileStats] = useState([]); 695 | const [timeDistribution, setTimeDistribution] = useState( 696 | [] 697 | ); 698 | const [loading, setLoading] = useState(true); 699 | const [isDarkMode, setIsDarkMode] = useState(() => { 700 | // Check stored preference 701 | const stored = localStorage.getItem('devtrack-dashboard-theme'); 702 | if (stored) { 703 | return stored === 'dark'; 704 | } 705 | 706 | // Fallback to system preference 707 | return window.matchMedia('(prefers-color-scheme: dark)').matches; 708 | }); 709 | 710 | useEffect(() => { 711 | // Save theme preference 712 | localStorage.setItem( 713 | 'devtrack-dashboard-theme', 714 | isDarkMode ? 'dark' : 'light' 715 | ); 716 | // Apply theme classes 717 | document.body.classList.toggle('dark', isDarkMode); 718 | }, [isDarkMode]); 719 | 720 | useEffect(() => { 721 | // Load statistics data 722 | const loadStatsData = async () => { 723 | try { 724 | const stats = await loadStats(); 725 | setActivityData(stats.activityTimeline || []); 726 | setFileStats(stats.fileTypes || []); 727 | setTimeDistribution(stats.timeDistribution || []); 728 | setLoading(false); 729 | } catch (error) { 730 | console.error('Failed to load statistics:', error); 731 | setLoading(false); 732 | } 733 | }; 734 | 735 | loadStatsData(); 736 | }, []); 737 | 738 | const themeColors = { 739 | text: isDarkMode ? 'text-gray-100' : 'text-gray-900', 740 | subtext: isDarkMode ? 'text-gray-300' : 'text-gray-500', 741 | background: isDarkMode ? 'bg-gray-900' : 'bg-white', 742 | cardBg: isDarkMode ? 'bg-gray-800' : 'bg-white', 743 | border: isDarkMode ? 'border-gray-700' : 'border-gray-200', 744 | chartColors: { 745 | grid: isDarkMode ? '#374151' : '#e5e7eb', 746 | text: isDarkMode ? '#e5e7eb' : '#4b5563', 747 | line1: isDarkMode ? '#93c5fd' : '#3b82f6', 748 | line2: isDarkMode ? '#86efac' : '#22c55e', 749 | line3: isDarkMode ? '#fde047' : '#eab308', 750 | bar: isDarkMode ? '#93c5fd' : '#3b82f6', 751 | }, 752 | }; 753 | 754 | if (loading) { 755 | return ( 756 |
759 |
Loading statistics...
760 |
761 | ); 762 | } 763 | 764 | return ( 765 |
768 | {/* Theme Toggle */} 769 |
770 | 783 |
784 | 785 | {/* Overview Cards */} 786 |
787 | 788 | 789 |
790 | 791 |
792 |

793 | Total Coding Hours 794 |

795 |

796 | 24.5 797 |

798 |
799 |
800 |
801 |
802 | 803 | 804 | 805 |
806 | 807 |
808 |

809 | Files Modified 810 |

811 |

54

812 |
813 |
814 |
815 |
816 | 817 | 818 | 819 |
820 | 821 |
822 |

823 | Total Commits 824 |

825 |

82

826 |
827 |
828 |
829 |
830 | 831 | 832 | 833 |
834 | 835 |
836 |

837 | Lines Changed 838 |

839 |

840 | 1,146 841 |

842 |
843 |
844 |
845 |
846 |
847 | 848 | {/* Activity Timeline */} 849 | 850 | 851 | 852 | Coding Activity Timeline 853 | 854 | 855 | 856 |
857 | 858 | 859 | 863 | 868 | 873 | 879 | 887 | 890 | 897 | 904 | 911 | 912 | 913 |
914 |
915 |
916 | 917 | {/* File Type Distribution */} 918 | 919 | 920 | 921 | File Type Distribution 922 | 923 | 924 | 925 |
926 | 927 | 928 | 932 | 937 | 941 | 949 | 952 | 957 | 958 | 959 |
960 |
961 |
962 | 963 | {/* Daily Distribution */} 964 | 965 | 966 | 967 | Daily Coding Distribution 968 | 969 | 970 | 971 |
972 | 973 | 974 | 978 | 983 | 987 | 995 | 998 | 1003 | 1004 | 1005 |
1006 |
1007 |
1008 |
1009 | ); 1010 | }; 1011 | 1012 | export default CodingStatsDashboard;`; 1013 | } 1014 | } 1015 | -------------------------------------------------------------------------------- /src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-unused-vars */ 3 | import { strictEqual } from 'assert'; 4 | import { ExtensionContext, window } from 'vscode'; 5 | import * as extension from '../extension'; 6 | 7 | suite('Extension Test Suite', () => { 8 | window.showInformationMessage('Start all tests.'); 9 | 10 | test('Sample test', () => { 11 | strictEqual(-1, [1, 2, 3].indexOf(5)); 12 | strictEqual(-1, [1, 2, 3].indexOf(0)); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-undef */ 3 | import { resolve } from 'path'; 4 | import type { TestOptions } from '@vscode/test-electron'; 5 | import { runTests } from '@vscode/test-electron'; 6 | 7 | async function main(): Promise { 8 | try { 9 | // The folder containing the Extension Manifest package.json 10 | const extensionPath = resolve(__dirname, '../../'); 11 | 12 | const testRunnerOptions: TestOptions = { 13 | extensionDevelopmentPath: extensionPath, 14 | launchArgs: [], 15 | version: 'stable', 16 | extensionTestsPath: '', 17 | }; 18 | 19 | // Download VS Code, unzip it and run the integration test 20 | await runTests(testRunnerOptions); 21 | } catch (err: unknown) { 22 | if (err instanceof Error) { 23 | console.error('Failed to run tests:', err.message); 24 | } 25 | process.exit(1); 26 | } 27 | } 28 | 29 | void main(); 30 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-undef */ 3 | import * as path from 'path'; 4 | import Mocha from 'mocha'; 5 | import { glob } from 'glob'; 6 | 7 | export async function run(): Promise { 8 | // Create the mocha test 9 | const mocha = new Mocha({ 10 | ui: 'tdd', 11 | color: true, 12 | }); 13 | 14 | const testsRoot = path.resolve(__dirname, '..'); 15 | const options = { cwd: testsRoot }; 16 | 17 | try { 18 | // Find test files 19 | const files = await glob('**/**.test.ts', options); 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | // Run the mocha test 25 | return new Promise((resolve, reject) => { 26 | try { 27 | mocha.run((failures: number) => { 28 | if (failures > 0) { 29 | reject(new Error(`${failures} tests failed.`)); 30 | } else { 31 | resolve(); 32 | } 33 | }); 34 | } catch (err) { 35 | console.error( 36 | 'Test execution error:', 37 | err instanceof Error ? err.message : String(err) 38 | ); 39 | reject(err); 40 | } 41 | }); 42 | } catch (err) { 43 | console.error( 44 | 'Test setup error:', 45 | err instanceof Error ? err.message : String(err) 46 | ); 47 | throw err; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /studio-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Teamial/DevTrack/08314eb19138782b1f53077260c5f86bb37fa976/studio-128x128.png -------------------------------------------------------------------------------- /temp-test/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "lib": [ 6 | "ES2020", 7 | "dom", 8 | "dom.iterable", 9 | "webworker", 10 | "scripthost" 11 | ], 12 | "outDir": "dist", 13 | "rootDir": "src", 14 | "strict": true, 15 | "jsx": "react", 16 | "types": ["vscode", "mocha", "node"], 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "esModuleInterop": true, 20 | "skipLibCheck": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "sourceMap": true, 23 | "resolveJsonModule": true 24 | }, 25 | "include": [ 26 | "src" 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | ".vscode-test" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher, ms-vscode.extension-test-runner, and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Install the [Extension Test Runner](https://marketplace.visualstudio.com/items?itemName=ms-vscode.extension-test-runner) 37 | * Run the "watch" task via the **Tasks: Run Task** command. Make sure this is running, or tests might not be discovered. 38 | * Open the Testing view from the activity bar and click the Run Test" button, or use the hotkey `Ctrl/Cmd + ; A` 39 | * See the output of the test result in the Test Results view. 40 | * Make changes to `src/test/extension.test.ts` or create new test files inside the `test` folder. 41 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 42 | * You can create folders inside the `test` folder to structure your tests any way you want. 43 | 44 | ## Go further 45 | 46 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 47 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 48 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 49 | --------------------------------------------------------------------------------