diff --git a/.gitignore b/.gitignore index d43d60f..93a6a78 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ dist build coverage .vscode -.DS_Store \ No newline at end of file +.DS_Store + +# VS Code extension packages +*.vsix diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ed533c --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2026 M K Subrahmanya +All rights reserved. + +PROPRIETARY LICENSE + +This software and all associated source code, object code, documentation, and other materials (the "Software") are proprietary. + +1. Limited permission to use +Subject to your compliance with this license, you are granted a limited, non-exclusive, non-transferable, revocable permission to use the Software for your own personal or internal business purposes. + +2. Restrictions +You may NOT, and you may not permit others to: +- copy, reproduce, modify, translate, adapt, or create derivative works of the Software; +- distribute, publish, sell, sublicense, rent, lease, lend, or otherwise make the Software available to any third party; +- reverse engineer, decompile, or disassemble the Software, except to the extent such restriction is prohibited by applicable law; +- remove, alter, or obscure any proprietary notices. + +3. No trademark rights +This license does not grant any rights to use the names, logos, or trademarks of the copyright holder. + +4. Termination +This license is effective until terminated. It terminates automatically if you breach any term of this license. Upon termination, you must stop using the Software and destroy all copies in your possession. + +5. Disclaimer +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + +6. Limitation of liability +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/backend/package-lock.json b/backend/package-lock.json index 6fe9bc9..30c7cc5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,9 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@google/genai": "^1.45.0", "axios": "^1.13.4", "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", @@ -20,12 +23,226 @@ "mongodb": "^7.0.0", "mongoose": "^9.1.5", "multer": "^2.0.2", - "socket.io": "^4.8.3" + "simple-git": "^3.27.0", + "socket.io": "^4.8.3", + "tree-sitter": "^0.25.0", + "tree-sitter-go": "^0.25.0", + "tree-sitter-java": "^0.23.5", + "tree-sitter-javascript": "^0.25.0", + "tree-sitter-python": "^0.25.0", + "tree-sitter-typescript": "^0.23.2" }, "devDependencies": { "nodemon": "^3.1.11" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@google/genai": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.45.0.tgz", + "integrity": "sha512-+sNRWhKiRibVgc4OKi7aBJJ0A7RcoVD8tGG+eFkqxAWRjASDW+ktS9lLwTDnAxZICzCVoeAdu8dYLJVTX60N9w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", @@ -35,6 +252,80 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -59,6 +350,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -87,6 +384,39 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -128,7 +458,26 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "license": "MIT" }, "node_modules/base64id": { @@ -163,6 +512,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -319,6 +677,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -429,6 +805,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -490,6 +889,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -505,6 +910,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -689,6 +1100,35 @@ "url": "https://opencollective.com/express" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -743,6 +1183,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -780,6 +1236,18 @@ "node": ">= 0.6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -822,6 +1290,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -859,6 +1356,27 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -872,13 +1390,63 @@ "node": ">= 6" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.1.tgz", + "integrity": "sha512-5awwuLrzNol+pFDmKJd0dKtZ0fPLAtoA5p7YO4ODsDu6ONJUVqbYwvv8y2ZBO5MBNp9TJXigB19710kYpBPdtA==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "7.1.3", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -953,6 +1521,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1014,6 +1595,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1043,6 +1633,54 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -1137,6 +1775,18 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1220,6 +1870,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -1415,6 +2074,44 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -1507,6 +2204,25 @@ "wrappy": "1" } }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1516,6 +2232,31 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -1526,6 +2267,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1539,6 +2286,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1640,6 +2411,30 @@ "node": ">=8.10.0" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1745,6 +2540,27 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1823,6 +2639,33 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-git": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.30.0.tgz", + "integrity": "sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1955,6 +2798,102 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2012,6 +2951,132 @@ "node": ">=18" } }, + "node_modules/tree-sitter": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.25.0.tgz", + "integrity": "sha512-PGZZzFW63eElZJDe/b/R/LbsjDDYJa5UEjLZJB59RQsMX+fo0j54fqBPn1MGKav/QNa0JR0zBiVaikYDWCj5KQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + } + }, + "node_modules/tree-sitter-go": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.25.0.tgz", + "integrity": "sha512-APBc/Dq3xz/e35Xpkhb1blu5UgW+2E3RyGWawZSCNcbGwa7jhSQPS8KsUupuzBla8PCo8+lz9W/JDJjmfRa2tw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-java": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.23.5.tgz", + "integrity": "sha512-Yju7oQ0Xx7GcUT01mUglPP+bYfvqjNCGdxqigTnew9nLGoII42PNVP3bHrYeMxswiCRM0yubWmN5qk+zsg0zMA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-javascript": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.25.0.tgz", + "integrity": "sha512-1fCbmzAskZkxcZzN41sFZ2br2iqTYP3tKls1b/HKGNPQUVOpsUxpmGxdN/wMqAk3jYZnYBR1dd/y/0avMeU7dw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-python": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", + "integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.25.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-typescript": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", + "integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2", + "tree-sitter-javascript": "^0.23.1" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-typescript/node_modules/tree-sitter-javascript": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", + "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -2069,6 +3134,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -2091,6 +3165,112 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 58bb028..a9fb221 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,6 +11,9 @@ "author": "M K Subrahmanya", "license": "MIT", "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@google/genai": "^1.45.0", "axios": "^1.13.4", "bcrypt": "^6.0.0", "bcryptjs": "^3.0.3", @@ -22,7 +25,14 @@ "mongodb": "^7.0.0", "mongoose": "^9.1.5", "multer": "^2.0.2", - "socket.io": "^4.8.3" + "simple-git": "^3.27.0", + "socket.io": "^4.8.3", + "tree-sitter": "^0.25.0", + "tree-sitter-go": "^0.25.0", + "tree-sitter-java": "^0.23.5", + "tree-sitter-javascript": "^0.25.0", + "tree-sitter-python": "^0.25.0", + "tree-sitter-typescript": "^0.23.2" }, "devDependencies": { "nodemon": "^3.1.11" diff --git a/backend/src/controllers/auth.controller.js b/backend/src/controllers/auth.controller.js index 61ea814..e0f95dd 100644 --- a/backend/src/controllers/auth.controller.js +++ b/backend/src/controllers/auth.controller.js @@ -8,13 +8,24 @@ import { User } from "../models/user.model.js"; export const githubLogin = (req, res) => { const redirectUrl = `${process.env.BASE_URL}/api/auth/github/callback`; + // If the login flow was initiated by the VS Code extension, we need to deep-link + // back into VS Code after OAuth completes. + const source = req.query.source; + const state = source === "vscode" ? "vscode" : "web"; + + // GitHub supports prompt=login to force account selection / re-auth. + const prompt = (req.query.prompt || "").toString(); + const promptParam = prompt === "login" ? `&prompt=login` : ""; + const githubAuthUrl = `https://github.com/login/oauth/authorize` + `?client_id=${process.env.GITHUB_CLIENT_ID}` + `&redirect_uri=${redirectUrl}` + - `&scope=read:user user:email`; + `&scope=read:user user:email repo` + + `&state=${encodeURIComponent(state)}` + + promptParam; - res.redirect(githubAuthUrl); + res.redirect(githubAuthUrl); }; /** @@ -28,9 +39,16 @@ export const githubLogin = (req, res) => { * - VS Code deep-link redirect */ export const githubCallback = async (req, res) => { - const { code } = req.query; + const { code, state } = req.query; if (!code) { + // If this was initiated from VS Code, we can't rely on the web UI. + if (state === "vscode") { + return res.redirect( + "vscode://MKSubrahmanya.codechat/auth?error=missing_code" + ); + } + return res.redirect( `${process.env.CLIENT_URL}/login?error=missing_code` ); @@ -82,19 +100,52 @@ export const githubCallback = async (req, res) => { /* ---------------------------------------------------- 3. Upsert user in DB (token always refreshed) ---------------------------------------------------- */ - const user = await User.findOneAndUpdate( - { githubId: id.toString() }, - { - username: login, - avatarUrl: avatar_url, - email, - githubToken: accessToken, // server-only usage - }, - { - new: true, - upsert: true, + let user = await User.findOne({ githubId: id.toString() }); + + if (user) { + user = await User.findByIdAndUpdate( + user._id, + { + username: login, + avatarUrl: avatar_url, + email, + githubToken: accessToken, + status: "active" + }, + { new: true } + ); + } else { + const shadow = await User.findOne({ username: login, status: "shadow" }); + if (shadow) { + user = await User.findByIdAndUpdate( + shadow._id, + { + githubId: id.toString(), + username: login, + avatarUrl: avatar_url, + email, + githubToken: accessToken, + status: "active" + }, + { new: true } + ); + } else { + user = await User.findOneAndUpdate( + { githubId: id.toString() }, + { + username: login, + avatarUrl: avatar_url, + email, + githubToken: accessToken, + status: "active" + }, + { + new: true, + upsert: true, + } + ); } - ); + } /* ---------------------------------------------------- 4. Generate JWT @@ -116,13 +167,17 @@ export const githubCallback = async (req, res) => { }); /* ---------------------------------------------------- - 6. Redirect to VS Code extension + 6. Redirect + - Web app: go to the SPA + - VS Code extension: deep-link back into VS Code with the JWT ---------------------------------------------------- */ - const encodedToken = encodeURIComponent(jwtToken); + if (state === "vscode") { + const vscodeUri = new URL("vscode://MKSubrahmanya.codechat/auth"); + vscodeUri.searchParams.set("token", jwtToken); + return res.redirect(vscodeUri.toString()); + } - res.redirect( - `vscode://MKSubrahmanya.codechat/auth?token=${encodedToken}` - ); + res.redirect(`${process.env.CLIENT_URL}/home`); } catch (error) { console.error("GitHub OAuth Callback Error:", error); diff --git a/backend/src/controllers/hackathon.controller.js b/backend/src/controllers/hackathon.controller.js new file mode 100644 index 0000000..ec6f0ad --- /dev/null +++ b/backend/src/controllers/hackathon.controller.js @@ -0,0 +1,483 @@ +import { GoogleGenAI } from "@google/genai"; +import dotenv from "dotenv"; +import axios from "axios"; +import { User } from "../models/user.model.js"; +import Ideation from "../models/ideation.model.js"; +import ProjectCache from "../models/projectCache.model.js"; +import { generateFingerprint, checkCache, saveToCache } from "../services/cache.service.js"; + +dotenv.config(); + +let currentKeyIndex = 0; + +const getValidKeys = () => { + const keys = []; + if (process.env.GEMINI_API_KEY) keys.push(process.env.GEMINI_API_KEY); + for (let i = 1; i <= 20; i++) { + const key = process.env[`GEMINI_API_KEY_${i}`]; + if (key) keys.push(key); + } + return keys; +}; + +const executeWithKeyRotation = async (promptText) => { + const keys = getValidKeys(); + if (keys.length === 0) throw new Error("GEMINI_API_KEY is missing in .env"); + + let attempts = 0; + while (attempts < keys.length) { + try { + const ai = new GoogleGenAI({ apiKey: keys[currentKeyIndex] }); + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: promptText, + config: { temperature: 0.7 } + }); + return response.text; + } catch (e) { + console.error(`Gemini API Error with key index ${currentKeyIndex}:`, e?.message); + currentKeyIndex = (currentKeyIndex + 1) % keys.length; + attempts++; + } + } + throw new Error("All Gemini API keys exhausted or rate-limited."); +}; + +export const chatWithAi = async (req, res) => { + try { + const { messages } = req.body; + + if (!process.env.GEMINI_API_KEY && getValidKeys().length === 0) { + return res.status(500).json({ error: "GEMINI_API_KEY is missing in .env" }); + } + + const systemInstruction = `You are a visionary Principal Architect and Hackathon CTO. + Your job is to help the user refine their hackathon idea step-by-step and produce a live architecture specification. + + CONVERSATIONAL FLOW: + 1. **Validate & Elevate:** When the user pitches an idea, respond with 2-3 sentences MAX. Suggest ONE killer feature that makes it win. Ask: "Do you like this direction, or should we twist it?" + 2. **Lock Tech Stack:** Once the idea is agreed upon, propose a short, concrete MERN/Fullstack tech stack. List it briefly and ask: "Are we good to generate the architecture graph?" + 3. **Generate Graph:** When the user agrees or says "generate graph", output the ReactFlow JSON graph. + + CRITICAL: Every single response MUST be a valid JSON object in this EXACT format. No markdown outside the JSON: + \`\`\`json + { + "reply": "Your short chat response here.", + "blueprint": { + "techStack": ["React + Vite", "Node.js + Express", "MongoDB Atlas", "Socket.io", "Redis"], + "folderStructure": "frontend/\\n src/\\n pages/\\n components/\\nbackend/\\n src/\\n routes/\\n controllers/\\n models/", + "hostingInstructions": "Frontend: Vercel. Backend: Railway. DB: MongoDB Atlas free tier. WebSockets: Railway keeps persistent connections.", + "codeMePreview": "Build a MERN stack app for real-time logistics fraud detection. Use Socket.io for live driver tracking...", + "graph": null + } + } + \`\`\` + + RULES: + - "reply": Short, punchy chat response ONLY. No essays. + - "blueprint": Update ALL fields progressively as the conversation develops. Always fill in what you know so far. Leave fields as null or empty array if not yet discussed. + - "graph": Set to null UNLESS the user asks to generate the graph. When generating: set to { "nodes": [...], "edges": [...] } with nodes logically positioned (x/y spaced 200px apart). Node format: { "id": "1", "data": { "label": "Driver App", "kind": "client", "tech": "React Native" }, "position": { "x": 0, "y": 0 } }. Edge format: { "id": "e1-2", "source": "1", "target": "2", "label": "GPS ping" } + - Return ONLY valid JSON in the code block. No text before or after.`; + + // Format history for the GenAI SDK + let promptText = systemInstruction + "\n\n--- User Conversation ---\n"; + messages.forEach(m => { + promptText += `${m.role.toUpperCase()}: ${m.content}\n`; + }); + + const rawReply = await executeWithKeyRotation(promptText); + + // Extract JSON from markdown code block + const jsonMatch = rawReply.match(/```json\n([\s\S]*?)\n```/); + if (jsonMatch) { + try { + const parsed = JSON.parse(jsonMatch[1]); + return res.status(200).json(parsed); + } catch (parseErr) { + // If JSON parse fails, fall back to raw text + console.error("JSON parse failed, falling back:", parseErr.message); + } + } + + // Fallback: wrap raw text in expected format + res.status(200).json({ reply: rawReply, blueprint: null }); + + } catch (e) { + console.error("AI Chat Error:", e); + res.status(500).json({ error: "AI Generation failed: " + e.message }); + } +}; + + +// ??$$$ — SHARED: The actual "God Prompt" builder for CodeME.md +// ??$$$ — Now accepts blueprint so all user-collected sections feed into the prompt +const buildFullCodeMe = async (messages, nodes, edges, blueprint = {}) => { + const graphSummary = nodes.map(n => + `- [${(n.data?.kind || "service").toUpperCase()}] ${n.data?.label || n.id}${n.data?.tech ? ` (${n.data.tech})` : ""}` + ).join("\n"); + + const edgeSummary = edges.map(e => + `- ${e.source} \u2192 ${e.target}${e.label ? `: ${e.label}` : ""}` + ).join("\n"); + + const conversation = messages.map(m => `${m.role.toUpperCase()}: ${m.content}`).join("\n"); + + // ??$$$ — Inject live blueprint sections the UI collected during chat + const stackSection = Array.isArray(blueprint.techStack) && blueprint.techStack.length > 0 + ? `AGREED TECH STACK (from CTO session):\n${blueprint.techStack.join(", ")}` + : ""; + const folderSection = blueprint.folderStructure + ? `AGREED FOLDER STRUCTURE (from CTO session):\n${blueprint.folderStructure.replace(/\\n/g, "\n")}` + : ""; + const hostingSection = blueprint.hostingInstructions + ? `AGREED HOSTING PLAN (from CTO session):\n${blueprint.hostingInstructions}` + : ""; + + const prompt = `You are a world-class Principal Engineer and Technical Architect. You are writing a CODEME.MD file — a "God Prompt" so detailed and complete that an AI coding agent (like Antigravity, Devin, or Cursor) can build the ENTIRE application from it in one single pass without needing any clarification whatsoever. + +Architecture graph: +NODES: +${graphSummary || "(Infer from conversation)"} + +EDGES (data flow): +${edgeSummary || "(Infer from conversation)"} + +${stackSection} +${folderSection} +${hostingSection} + +--- CONVERSATION --- +${conversation} +--- ARCHITECTURE GRAPH (NODES & EDGES) --- +${JSON.stringify(nodes)} +${JSON.stringify(edges)} +--- VISION MOCKUP --- +${blueprint.uiPreview || "Not generated yet"} +--- TECH STACK & FOLDER STRUCTURE --- +${JSON.stringify(blueprint.techStack || [])} +${blueprint.folderStructure || ""} +--- END CONTEXT --- + +You are a world-class CTO and Software Architect. Generate the ULTIMATE CodeME.md for this project. +It MUST include ALL 12 sections below with COMPLETE, UNAMBIGUOUS detail. NEVER skip or abbreviate. +This document serves as the "God Key" for another AI agent (Antigravity) to build the entire app. + +# [App Name] — CodeME.md + +## 1. Context & Business Goal +Detailed Problem Statement (the "Why"). Business Goal (the "What"). Key product value propositions. MVP scope for a 24-48h hackathon. + +## 2. Exact Tech Stack (High Fidelity) +List every package, library, and tool. Frontend, Backend, Database, Auth, Real-time, Maps, CI/CD. + +## 3. Project Folder Structure +A COMPLETE, directory-by-directory tree. Include comments for every file's specific responsibility. Ensure it follows a clean MVC or Feature-based architecture. + +## 4. Database Schemas (Implementation Ready) +Write out the Mongoose schemas (or equivalent) in code blocks. Include every field, type, index, and relationship (refs). No "..." - write everything out. + +## 5. API Endpoint Definitions (Comprehensive) +List EVERY route. Include: +- Method, Path +- Required Payload shape (JSON) +- Success & Error response shapes +- Middleware logic (e.g. "Admin only", "Token required") + +## 6. Real-time Events (WebSockets / Socket.io) +Emit events, listen events, payload shapes, and the logic flow between client/server. + +## 7. Frontend Pages & Components +List every page (/login, /dashboard, etc.). Describe UI components, state management (Redux/Zustand/Context), and which APIs each page interacts with. + +## 8. Authentication & Authorization Flow +The exact logic: Passport/JWT/Clerk. How tokens are saved, guarded, and removed. + +## 9. Environment Variables (.env) +A complete template with descriptions for every key (GEMINI_API_KEY, MONGO_URI, etc). + +## 10. Hosting & Deployment Launchpad +Provide instructions for Vercel (frontend) and Render/Railway (backend/DB). Include CORS settings and environment variable syncing. + +## 11. Dev Setup & Run Commands +The exact shell commands to get it running from a fresh clone. + +## 12. Agent Task Checklist (The Master Plan) +A numbered, step-by-step build order. Be RUTHLESSLY granular. +Each item should be a single, testable developer task (e.g., "Implement POST /api/v1/auth/signup with email verification"). + +Write in professional, dense technical language. Return ONLY the Markdown. Start directly with the # heading.`; + + return await executeWithKeyRotation(prompt); +}; + +// ??$$$ — Standalone endpoint: generate full CodeME.md without pushing to GitHub +export const generateCodeMe = async (req, res) => { + try { + const { messages, nodes, edges, blueprint } = req.body; + if (!messages || messages.length < 2) { + return res.status(400).json({ error: "Not enough conversation context. Chat more with the CTO first." }); + } + + // ========================================================== + // ??$$$ — SCALING LOGIC: THE SEMANTIC CACHE CHECK + // ========================================================== + const fingerprint = generateFingerprint(messages); + const cachedBlueprint = await checkCache(fingerprint); + + if (cachedBlueprint) { + console.log(`[Cache Hit] Serving blueprint for: ${fingerprint}`); + return res.status(200).json({ + content: cachedBlueprint.generatedCodeMe, + isCached: true // Inform frontend this was instant + }); + } + + // ========================================================== + // [Cache Miss] Only now do we spend money/tokens on AI + // ========================================================== + console.log(`[Cache Miss] Generating new blueprint for: ${fingerprint}`); + const content = await buildFullCodeMe(messages, nodes || [], edges || [], blueprint || {}); + + // Save this new "Master Blueprint" for future users + await saveToCache(fingerprint, { + generatedCodeMe: content, + techStack: blueprint?.techStack || [], + originalPrompt: messages[messages.length-1].content + }); + + res.status(200).json({ content }); + } catch (e) { + console.error("Generate CodeME Error:", e); + res.status(500).json({ error: "CodeME generation failed: " + e.message }); + } +}; + +export const compileAndPushRepo = async (req, res) => { + + try { + const user = await User.findById(req.user._id); + if (!user || !user.githubToken) { + return res.status(401).json({ error: "No GitHub token. Please login again." }); + } + + // ??$$$ — Accept user-edited CodeME override; pass blueprint for enriched AI gen + const { messages, nodes, edges, newRepo, customCodeMe, blueprint } = req.body; + if (!newRepo || !newRepo.name) return res.status(400).json({ error: "Repository configuration missing." }); + if (!nodes || nodes.length === 0) return res.status(400).json({ error: "Graph is empty." }); + + let codeMeContent = customCodeMe || null; + + // ??$$$ — Only call AI if user didn't provide their own CodeME content + if (!codeMeContent) { + codeMeContent = await buildFullCodeMe(messages, nodes, edges, blueprint || {}); + } + + // ??$$$ — Create the Repo on GitHub + const githubHeaders = { + Authorization: `Bearer ${user.githubToken}`, + Accept: "application/vnd.github+json" + }; + + const ownerType = newRepo.ownerType || "user"; + const repoName = String(newRepo.name || "").trim(); + const urlCreate = ownerType === "org" + ? `https://api.github.com/orgs/${String(newRepo.org).trim()}/repos` + : "https://api.github.com/user/repos"; + + const payloadCreate = { + name: repoName, + description: newRepo.description || "", + private: newRepo.visibility === "private" || newRepo.visibility === "internal", + auto_init: true + }; + + let repo; + let repoOwnerLogin; + + try { + const createRes = await axios.post(urlCreate, payloadCreate, { headers: githubHeaders }); + repo = createRes.data; + repoOwnerLogin = repo.owner.login; + } catch (e) { + // ??$$$ — Handle case where repo already exists (422 error) + if (e.response?.status === 422) { + // If it exists, we need to find the owner login (could be user or the specific org) + repoOwnerLogin = ownerType === "org" ? String(newRepo.org).trim() : user.username; + try { + const getRes = await axios.get(`https://api.github.com/repos/${repoOwnerLogin}/${repoName}`, { headers: githubHeaders }); + repo = getRes.data; + } catch (getErr) { + throw new Error("Repository already exists but couldn't be accessed. Check permissions."); + } + } else { + throw e; + } + } + + // ??$$$ — Push CodeME.md. If it exists, we must get the SHA to update it. + const urlFile = `https://api.github.com/repos/${repoOwnerLogin}/${repoName}/contents/CodeME.md`; + let existingSha = null; + try { + const fileRes = await axios.get(urlFile, { headers: githubHeaders }); + existingSha = fileRes.data.sha; + } catch {} + + await axios.put(urlFile, { + message: existingSha ? "🚀 Update Hackathon Blueprint (CodeME.md)" : "🚀 Initial commit: Hackathon Blueprint (CodeME.md)", + content: Buffer.from(codeMeContent).toString("base64"), + sha: existingSha || undefined + }, { headers: githubHeaders }); + + res.status(200).json({ + message: existingSha ? "Repository updated and CodeME.md refreshed!" : "Repository created and CodeME.md pushed!", + repo + }); + } catch (e) { + console.error("Compile & Push Error:", e?.response?.data || e); + res.status(500).json({ error: e?.response?.data?.message || e.message || "Compilation failed." }); + } +}; + +// ??$$$ — NEW: AI Scorecard endpoint — rates the hackathon idea on 4 axes +export const getScorecard = async (req, res) => { + try { + const { messages, blueprint } = req.body; + + const prompt = `You are a senior hackathon judge. Based on the conversation and blueprint below, rate the project on exactly these 4 axes: + 1. Feasibility (can a 2-4 person team build a working demo in 24 hours?) + 2. Originality (how unique is this idea compared to typical hackathon projects?) + 3. Technical Depth (does it use interesting technology, ML, real-time data, etc.?) + 4. Demo-ability (will this impress judges in a 3-minute demo?) + + Return ONLY a valid JSON object (no markdown wrapper): + { "feasibility": 8, "originality": 7, "technicalDepth": 9, "demoability": 8, "summary": "One sentence overall verdict." } + + Blueprint so far: ${JSON.stringify(blueprint)} + --- Conversation --- + ${messages.map(m => `${m.role.toUpperCase()}: ${m.content}`).join("\n")}`; + + const raw = await executeWithKeyRotation(prompt); + // Strip any accidental markdown + const clean = raw.replace(/```json\n?|```/g, "").trim(); + const scores = JSON.parse(clean); + res.status(200).json(scores); + } catch (e) { + console.error("Scorecard Error:", e); + res.status(500).json({ error: "Scorecard generation failed: " + e.message }); + } +}; + +// ??$$$ — NEW: AI File drafter (Lovable.dev style) +// Generates the actual code for a specific file based on the project context. +export const draftFileCode = async (req, res) => { + try { + const { messages, blueprint, fileName } = req.body; + + const prompt = `You are a world-class ${blueprint.techStack?.join(", ") || "Fullstack"} engineer. + Based on our brainstorm, write the COMPLETE, production-ready code for the file: "${fileName}". + + Project Goal: ${messages[messages.length-1].content} + Agreed Stack: ${JSON.stringify(blueprint.techStack)} + + Return ONLY the code. No markdown formatting, no explanations. Just the raw code for ${fileName}.`; + + const code = await executeWithKeyRotation(prompt); + // Clean up any accidental markdown wrappers + const cleanCode = code.replace(/^```[a-z]*\n|```$/gi, "").trim(); + + res.status(200).json({ code: cleanCode }); + } catch (e) { + console.error("Drafting Error:", e); + res.status(500).json({ error: "Failed to draft file: " + e.message }); + } +}; + +// ??$$$ — NEW: AI UI Previewer (Lovable.dev style) +// Generates a single-file Tailwind CSS mockup of the app's main dashboard. +export const generateUiPreview = async (req, res) => { + try { + const { messages, blueprint } = req.body; + + const prompt = `You are a world-class UI/UX designer. Create a STUNNING, modern, high-fidelity single-file HTML/Tailwind CSS mockup of the main dashboard for this project. + + Project: ${messages[messages.length-1].content} + Theme: Modern, Dark Mode, Premium, Glassmorphism. + + Requirements: + 1. Use ONLY Tailwind CSS (via CDN). + 2. Use Lucide Icons (via CDN). + 3. Include mock data (charts, user list, etc.). + 4. Make it fully responsive. + 5. Return ONLY the HTML code. No markdown formatting.`; + + const html = await executeWithKeyRotation(prompt); + // Clean up markdown + const cleanHtml = html.replace(/^```html\n|```$/gi, "").trim(); + + res.status(200).json({ html: cleanHtml }); + } catch (e) { + console.error("UI Preview Error:", e); + res.status(500).json({ error: "UI generation failed." }); + } +}; + +// ??$$$ — Save/Update Ideation Session in MongoDB +export const saveIdeationSession = async (req, res) => { + try { + const { repoName, messages, blueprint, nodes, edges, teamSize, hackHours, fileDrafts, uiPreview } = req.body; + const userId = req.user._id; + + const session = await Ideation.findOneAndUpdate( + { userId, repoName }, + { + messages, + blueprint, + nodes, + edges, + teamSize, + hackHours, + fileDrafts, + uiPreview, + updatedAt: Date.now() + }, + { upsert: true, new: true } + ); + + res.status(200).json({ message: "Session saved to cloud", session }); + } catch (e) { + console.error("Save Session Error:", e); + res.status(500).json({ error: "Failed to save session" }); + } +}; + +// ??$$$ — Fetch Ideation Session from MongoDB +export const getIdeationSession = async (req, res) => { + try { + const { repoName } = req.params; + const userId = req.user._id; + + const session = await Ideation.findOne({ userId, repoName }); + if (!session) { + return res.status(404).json({ error: "No session found" }); + } + + res.status(200).json(session); + } catch (e) { + console.error("Get Session Error:", e); + res.status(500).json({ error: "Failed to fetch session" }); + } +}; + +// ??$$$ — Clear Ideation Session +export const clearIdeationSession = async (req, res) => { + try { + const { repoName } = req.params; + const userId = req.user._id; + + await Ideation.deleteOne({ userId, repoName }); + res.status(200).json({ message: "Session cleared" }); + } catch (e) { + console.error("Clear Session Error:", e); + res.status(500).json({ error: "Failed to clear session" }); + } +}; diff --git a/backend/src/controllers/invite.controller.js b/backend/src/controllers/invite.controller.js index ac69bb3..841dfd9 100644 --- a/backend/src/controllers/invite.controller.js +++ b/backend/src/controllers/invite.controller.js @@ -1,28 +1,108 @@ +import axios from "axios"; +import crypto from "crypto"; import { User } from "../models/user.model.js"; import { Invite } from "../models/invite.model.js"; +const getClientUrl = () => process.env.CLIENT_URL || "http://localhost:5173"; + +const upsertSharedRepo = async (userId, shared) => { + // 1) If the repo is already present, update in place (prevents duplicates) + const updated = await User.findOneAndUpdate( + { _id: userId, "sharedRepos.repoId": String(shared.repoId) }, + { + $set: { + "sharedRepos.$": { + repoId: String(shared.repoId), + name: shared.name, + owner: shared.owner, + description: shared.description + } + } + }, + { new: true } + ); + + if (updated) return updated; + + // 2) Otherwise push once + return await User.findByIdAndUpdate( + userId, + { + $push: { + sharedRepos: { + repoId: String(shared.repoId), + name: shared.name, + owner: shared.owner, + description: shared.description + } + } + }, + { new: true } + ); +}; + // 1. Send an Invite export const sendInvite = async (req, res) => { try { const { receiverUsername, repoId, repoName, repoOwner } = req.body; const sender = req.user.username; - const receiver = await User.findOne({ username: receiverUsername }); - if (!receiver) return res.status(404).json({ error: "User not found" }); + if (!receiverUsername || !repoId || !repoName || !repoOwner) { + return res.status(400).json({ error: "Missing invite data" }); + } + + if (receiverUsername === sender) return res.status(400).json({ error: "Cannot invite yourself" }); + + // Validate GitHub username exists + try { + await axios.get(`https://api.github.com/users/${receiverUsername}`, { + headers: { Authorization: `Bearer ${req.user.githubToken}` } + }); + } catch (e) { + return res.status(404).json({ error: "GitHub user not found" }); + } - if (receiver.username === sender) return res.status(400).json({ error: "Cannot invite yourself" }); + let receiver = await User.findOne({ username: receiverUsername }); + if (!receiver) { + receiver = await User.create({ username: receiverUsername, status: "shadow" }); + } const exists = await Invite.findOne({ sender, receiver: receiverUsername, repoId, status: "pending" }); if (exists) return res.status(400).json({ error: "Invite already pending" }); - await Invite.create({ sender, receiver: receiverUsername, repoId, repoName, repoOwner }); + const inviteToken = crypto.randomBytes(24).toString("hex"); + const invite = await Invite.create({ sender, receiver: receiverUsername, repoId, repoName, repoOwner, inviteToken }); - res.status(200).json({ message: "Invite sent!" }); + const inviteLink = `${getClientUrl()}/invite/${invite.inviteToken}`; + + res.status(200).json({ message: "Invite sent!", inviteLink, inviteToken: invite.inviteToken }); } catch (error) { res.status(500).json({ error: "Failed to send invite" }); } }; +// 1.5 Search GitHub Users +export const searchGithubUsers = async (req, res) => { + try { + const q = (req.query.q || "").trim(); + if (!q) return res.status(200).json([]); + + const ghRes = await axios.get(`https://api.github.com/search/users?q=${encodeURIComponent(q)}&per_page=10`, { + headers: { Authorization: `Bearer ${req.user.githubToken}` } + }); + + const users = ghRes.data.items.map(u => ({ + login: u.login, + id: u.id, + avatar_url: u.avatar_url + })); + + res.status(200).json(users); + } catch (error) { + res.status(500).json({ error: "Failed to search users" }); + } +}; + // 2. Get My Invites export const getMyInvites = async (req, res) => { try { @@ -33,6 +113,34 @@ export const getMyInvites = async (req, res) => { } }; +// 2.5 View Invite details by token (without accepting) +export const getInviteByToken = async (req, res) => { + try { + const { token } = req.params; + const invite = await Invite.findOne({ inviteToken: token }); + if (!invite) return res.status(404).json({ error: "Invite not found" }); + + if (invite.receiver !== req.user.username) { + return res.status(403).json({ error: "Not authorized to view this invite" }); + } + + res.status(200).json({ + invite: { + _id: invite._id, + sender: invite.sender, + receiver: invite.receiver, + repoId: invite.repoId, + repoName: invite.repoName, + repoOwner: invite.repoOwner, + status: invite.status, + createdAt: invite.createdAt + } + }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch invite" }); + } +}; + // 3. Accept Invite export const acceptInvite = async (req, res) => { try { @@ -40,23 +148,61 @@ export const acceptInvite = async (req, res) => { const invite = await Invite.findById(inviteId); if (!invite) return res.status(404).json({ error: "Invite not found" }); - // Add to User's Shared Repos - await User.findByIdAndUpdate(req.user._id, { - $addToSet: { - sharedRepos: { - repoId: invite.repoId, - name: invite.repoName, - owner: invite.repoOwner, - description: "Shared with you" - } - } + if (invite.receiver !== req.user.username) { + return res.status(403).json({ error: "Not authorized to accept this invite" }); + } + + // Add to User's Shared Repos (idempotent) + await upsertSharedRepo(req.user._id, { + repoId: invite.repoId, + name: invite.repoName, + owner: invite.repoOwner, + description: "Shared with you" }); // Delete Invite await Invite.findByIdAndDelete(inviteId); - res.status(200).json({ message: "Invite accepted" }); + res.status(200).json({ + message: "Invite accepted", + repoId: invite.repoId, + repoName: invite.repoName, + repoOwner: invite.repoOwner, + sender: invite.sender + }); } catch (error) { res.status(500).json({ error: "Failed to accept invite" }); } -}; \ No newline at end of file +}; + +// 4. Accept Invite by Token +export const acceptInviteByToken = async (req, res) => { + try { + const { token } = req.params; + const invite = await Invite.findOne({ inviteToken: token }); + if (!invite) return res.status(404).json({ error: "Invite not found" }); + + if (invite.receiver !== req.user.username) { + return res.status(403).json({ error: "Not authorized to accept this invite" }); + } + + await upsertSharedRepo(req.user._id, { + repoId: invite.repoId, + name: invite.repoName, + owner: invite.repoOwner, + description: "Shared with you" + }); + + await Invite.findByIdAndDelete(invite._id); + + res.status(200).json({ + message: "Invite accepted", + repoId: invite.repoId, + repoName: invite.repoName, + repoOwner: invite.repoOwner, + sender: invite.sender + }); + } catch (error) { + res.status(500).json({ error: "Failed to accept invite" }); + } +}; diff --git a/backend/src/controllers/repo.controller.js b/backend/src/controllers/repo.controller.js index 8541729..714eb98 100644 --- a/backend/src/controllers/repo.controller.js +++ b/backend/src/controllers/repo.controller.js @@ -1,25 +1,70 @@ import axios from "axios"; +import path from "path"; +import fs from "fs"; +import simpleGit from "simple-git"; import { User } from "../models/user.model.js"; import { Message } from "../models/message.model.js"; +const DEFAULT_REPO_BASE_PATH = process.env.DEFAULT_REPO_BASE_PATH || "C:\\Users\\User\\Repo"; + +const getRepoDir = (basePath, owner, repoName) => { + const safeOwner = owner.replace(/[^a-zA-Z0-9-_]/g, ""); + const safeRepo = repoName.replace(/[^a-zA-Z0-9-_]/g, ""); + return path.join(basePath, `${safeOwner}__${safeRepo}`); +}; + +const getAuthedRepoUrl = (token, owner, repoName) => { + return `https://oauth2:${token}@github.com/${owner}/${repoName}.git`; +}; + export const getRepos = async (req, res) => { try { const user = await User.findById(req.user._id); if (!user || !user.githubToken) return res.status(401).json({ error: "No Token" }); - // 1. Fetch GitHub Repos - const githubRes = await axios.get("https://api.github.com/user/repos?sort=updated&per_page=10", { - headers: { Authorization: `Bearer ${user.githubToken}` } - }); + // 1. Fetch GitHub Repos (best-effort) + let githubRepos = []; + try { + const githubRes = await axios.get("https://api.github.com/user/repos?sort=updated&per_page=10", { + headers: { Authorization: `Bearer ${user.githubToken}` } + }); + githubRepos = githubRes.data || []; + } catch (e) { + // If GitHub call fails (bad scope, rate limit, etc), still return shared repos. + console.error("GitHub repo list failed:", e?.response?.data || e?.message); + githubRepos = []; + } // 2. Get Shared Repos (from DB) - const sharedRepos = user.sharedRepos || []; + const sharedReposRaw = user.sharedRepos || []; + + // Deduplicate by repoId (fixes double-invite / double-accept issues) + const sharedById = new Map(); + for (const r of sharedReposRaw) { + const rid = r?.repoId ? String(r.repoId) : null; + if (!rid) continue; + sharedById.set(rid, r); + } + const sharedRepos = Array.from(sharedById.values()); + + // Also avoid duplicates if the repo already appears in the user's normal GitHub repos. + const githubIds = new Set((githubRepos || []).map(r => (r?.id !== undefined ? String(r.id) : ""))); + const sharedFiltered = sharedRepos.filter(r => { + const rid = r?.repoId ? String(r.repoId) : ""; + return rid && !githubIds.has(rid); + }); // 3. Combine them // We add a flag 'isShared' to style them differently if we want const combined = [ - ...githubRes.data, - ...sharedRepos.map(r => ({ ...r, id: r.repoId, isShared: true })) + ...githubRepos, + ...sharedFiltered.map(r => ({ + ...r, + id: String(r.repoId), + name: r.name || "(shared repo)", + owner: r.owner || "unknown", + isShared: true + })) ]; res.status(200).json(combined); @@ -97,4 +142,317 @@ export const getFileContent = async (req, res) => { console.error("Content Fetch Error:", error.message); res.status(500).json({ error: "Failed to fetch content" }); } -}; \ No newline at end of file +}; + +export const setRepoPath = async (req, res) => { + try { + const { repoBasePath } = req.body; + if (!repoBasePath) return res.status(400).json({ error: "repoBasePath is required" }); + const user = await User.findByIdAndUpdate(req.user._id, { repoBasePath }, { new: true }); + res.status(200).json({ repoBasePath: user.repoBasePath }); + } catch (error) { + res.status(500).json({ error: "Failed to set repo path" }); + } +}; + +export const pullRepo = async (req, res) => { + try { + const { owner, repoName } = req.body; + const user = await User.findById(req.user._id); + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + if (!owner || !repoName) return res.status(400).json({ error: "Missing repo info" }); + + if (!fs.existsSync(basePath)) { + fs.mkdirSync(basePath, { recursive: true }); + } + + const repoDir = getRepoDir(basePath, owner, repoName); + const git = simpleGit(); + + if (!fs.existsSync(repoDir)) { + const url = getAuthedRepoUrl(user.githubToken, owner, repoName); + await git.clone(url, repoDir); + return res.status(200).json({ message: "Repo cloned", repoDir }); + } + + const repoGit = simpleGit(repoDir); + await repoGit.pull(); + res.status(200).json({ message: "Repo pulled", repoDir }); + } catch (error) { + res.status(500).json({ error: "Failed to pull repo" }); + } +}; + +export const pushRepo = async (req, res) => { + try { + const { owner, repoName, commitMessage } = req.body; + const user = await User.findById(req.user._id); + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + if (!owner || !repoName) return res.status(400).json({ error: "Missing repo info" }); + + const repoDir = getRepoDir(basePath, owner, repoName); + if (!fs.existsSync(repoDir)) return res.status(404).json({ error: "Repo not found locally" }); + + const repoGit = simpleGit(repoDir); + const status = await repoGit.status(); + const isClean = status.isClean(); + const hasAheadCommits = typeof status.ahead === "number" && status.ahead > 0; + + if (isClean && !hasAheadCommits) { + return res.status(200).json({ message: "Nothing to push" }); + } + + if (!isClean && commitMessage && commitMessage.trim()) { + await repoGit.add("./*"); + await repoGit.commit(commitMessage.trim()); + } + + const url = getAuthedRepoUrl(user.githubToken, owner, repoName); + await repoGit.remote(["set-url", "origin", url]); + await repoGit.push(); + res.status(200).json({ message: "Repo pushed" }); + } catch (error) { + console.error("Push Repo Error:", error); + res.status(500).json({ error: "Failed to push repo", details: error?.message || String(error) }); + } +}; + +export const getLocalFilePath = async (req, res) => { + try { + const { owner, repoName, filePath } = req.query; + const user = await User.findById(req.user._id); + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + if (!owner || !repoName || !filePath) return res.status(400).json({ error: "Missing file info" }); + + const repoDir = getRepoDir(basePath, owner, repoName); + const fullPath = path.normalize(path.join(repoDir, filePath)); + if (!fullPath.startsWith(repoDir)) return res.status(400).json({ error: "Invalid path" }); + if (!fs.existsSync(fullPath)) return res.status(404).json({ error: "File not found" }); + + res.status(200).json({ path: fullPath }); + } catch (error) { + res.status(500).json({ error: "Failed to resolve local file path" }); + } +}; + +export const getLocalFileContent = async (req, res) => { + try { + const { owner, repoName, filePath } = req.query; + const user = await User.findById(req.user._id); + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + if (!owner || !repoName || !filePath) return res.status(400).json({ error: "Missing file info" }); + + const repoDir = getRepoDir(basePath, owner, repoName); + const fullPath = path.normalize(path.join(repoDir, filePath)); + if (!fullPath.startsWith(repoDir)) return res.status(400).json({ error: "Invalid path" }); + if (!fs.existsSync(fullPath)) return res.status(404).json({ error: "File not found locally. Pull repo first." }); + + const content = fs.readFileSync(fullPath, "utf-8"); + res.status(200).json({ content }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch local file content" }); + } +}; + +export const updateLocalFileContent = async (req, res) => { + try { + const { owner, repoName, filePath, content } = req.body; + const user = await User.findById(req.user._id); + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + if (!owner || !repoName || !filePath) return res.status(400).json({ error: "Missing file info" }); + if (typeof content !== "string") return res.status(400).json({ error: "Content must be a string" }); + + const repoDir = getRepoDir(basePath, owner, repoName); + const fullPath = path.normalize(path.join(repoDir, filePath)); + if (!fullPath.startsWith(repoDir)) return res.status(400).json({ error: "Invalid path" }); + if (!fs.existsSync(fullPath)) return res.status(404).json({ error: "File not found locally. Pull repo first." }); + + fs.writeFileSync(fullPath, content, "utf-8"); + res.status(200).json({ message: "File saved" }); + } catch (error) { + res.status(500).json({ error: "Failed to save local file content" }); + } +}; +export const getCodeAnchor = async (req, res) => { + try { + const { url } = req.body; + if (!url) return res.status(400).json({ error: "URL is required" }); + + const match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+?)#L(\d+)(?:-L(\d+))?/); + if (!match) return res.status(400).json({ error: "Invalid GitHub blob URL" }); + + const [, owner, repoName, branch, filePath, startRaw, endRaw] = match; + const startLine = parseInt(startRaw, 10); + const endLine = parseInt(endRaw || startRaw, 10); + + const user = await User.findById(req.user._id); + if (!user || !user.githubToken) return res.status(401).json({ error: "No Token" }); + + const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/contents/${filePath}`; + const response = await axios.get(apiUrl, { + headers: { + Authorization: `Bearer ${user.githubToken}`, + Accept: "application/vnd.github.v3+json" + }, + params: { ref: branch } + }); + + const content = Buffer.from(response.data.content, "base64").toString("utf-8"); + const lines = content.split("\n"); + const snippet = lines.slice(startLine - 1, endLine).join("\n"); + + const filename = path.basename(filePath); + const ext = path.extname(filePath).replace(".", "").toLowerCase(); + + res.status(200).json({ + filename, + language: ext || "text", + startLine, + endLine, + code: snippet + }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch code anchor" }); + } +}; + +const pickDefined = (obj) => { + return Object.fromEntries( + Object.entries(obj).filter(([, v]) => v !== undefined) + ); +}; + +export const createRepo = async (req, res) => { + try { + const user = await User.findById(req.user._id); + if (!user || !user.githubToken) { + return res.status(401).json({ error: "No GitHub token. Please login again." }); + } + + const { + ownerType = "user", // 'user' | 'org' + org, + name, + description, + homepage, + visibility = "public", // 'public' | 'private' | 'internal' + + // init / templates + auto_init, + gitignore_template, + license_template, + is_template, + + // common toggles + has_issues, + has_projects, + has_wiki, + has_discussions, + + // merge settings + allow_squash_merge, + allow_merge_commit, + allow_rebase_merge, + delete_branch_on_merge, + + // Optional: create repo from template repo + template_owner, + template_repo, + include_all_branches + } = req.body || {}; + + const repoName = String(name || "").trim(); + if (!repoName) return res.status(400).json({ error: "Repository name is required" }); + if (repoName.length > 100) return res.status(400).json({ error: "Repository name is too long (max 100 chars)" }); + if (!/^[A-Za-z0-9_.-]+$/.test(repoName)) { + return res.status(400).json({ + error: "Invalid repository name. Use letters, numbers, '.', '_' or '-' (no spaces)." + }); + } + + if (ownerType === "org" && !String(org || "").trim()) { + return res.status(400).json({ error: "org is required when ownerType is 'org'" }); + } + + if (ownerType !== "user" && ownerType !== "org") { + return res.status(400).json({ error: "ownerType must be 'user' or 'org'" }); + } + + if (visibility === "internal" && ownerType !== "org") { + return res.status(400).json({ error: "'internal' visibility is only valid for organization repositories" }); + } + + const desiredPrivate = visibility === "private" || visibility === "internal"; + const githubHeaders = { + Authorization: `Bearer ${user.githubToken}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28" + }; + + // ------------------------------- + // Template-based repo creation + // ------------------------------- + if (template_owner && template_repo) { + const targetOwner = ownerType === "org" ? String(org).trim() : user.username; + const url = `https://api.github.com/repos/${template_owner}/${template_repo}/generate`; + + const payload = pickDefined({ + owner: targetOwner, + name: repoName, + description, + private: desiredPrivate, + include_all_branches: include_all_branches === true + }); + + const response = await axios.post(url, payload, { headers: githubHeaders }); + return res.status(201).json({ repo: response.data }); + } + + // ------------------------------- + // Normal repo creation + // ------------------------------- + const url = ownerType === "org" + ? `https://api.github.com/orgs/${String(org).trim()}/repos` + : "https://api.github.com/user/repos"; + + // GitHub only applies license/gitignore when the repo is initialized. + const shouldAutoInit = auto_init === true || Boolean(gitignore_template) || Boolean(license_template); + + const payload = pickDefined({ + name: repoName, + description, + homepage, + private: desiredPrivate, + + ...(ownerType === "org" ? { visibility } : {}), + + auto_init: shouldAutoInit, + gitignore_template, + license_template, + is_template, + + has_issues, + has_projects, + has_wiki, + has_discussions, + + allow_squash_merge, + allow_merge_commit, + allow_rebase_merge, + delete_branch_on_merge + }); + + const response = await axios.post(url, payload, { headers: githubHeaders }); + return res.status(201).json({ repo: response.data }); + + } catch (error) { + const status = error?.response?.status || 500; + const gh = error?.response?.data; + + // Normalize GitHub error payloads for the UI + return res.status(status).json({ + error: gh?.message || "Failed to create repository", + details: gh?.errors || undefined + }); + } +}; diff --git a/backend/src/controllers/user.controller.js b/backend/src/controllers/user.controller.js new file mode 100644 index 0000000..54abe65 --- /dev/null +++ b/backend/src/controllers/user.controller.js @@ -0,0 +1,18 @@ +import { User } from "../models/user.model.js"; + +export const setRepoBasePath = async (req, res) => { + try { + const { repoBasePath } = req.body; + if (!repoBasePath) return res.status(400).json({ error: "repoBasePath is required" }); + + const updated = await User.findByIdAndUpdate( + req.user._id, + { repoBasePath }, + { new: true } + ); + + res.status(200).json({ repoBasePath: updated.repoBasePath }); + } catch (error) { + res.status(500).json({ error: "Failed to save repo path" }); + } +}; diff --git a/backend/src/controllers/visualizer.controller.js b/backend/src/controllers/visualizer.controller.js new file mode 100644 index 0000000..58d06d6 --- /dev/null +++ b/backend/src/controllers/visualizer.controller.js @@ -0,0 +1,1628 @@ +import fs from "fs"; +import path from "path"; +import axios from "axios"; +import { parse } from "@babel/parser"; +import traverse from "@babel/traverse"; +import { User } from "../models/user.model.js"; +import { analyzeGenericFile } from "../services/astParser.service.js"; +import { parseGenericDataFlow } from "../services/dataFlowParser.service.js"; + +const DEFAULT_REPO_BASE_PATH = process.env.DEFAULT_REPO_BASE_PATH || "C:\\Users\\User\\Repo"; +const DEFAULT_MAX_NODES = 400; +const DEFAULT_EXPAND_DEPTH = 2; +const DEFAULT_REMOTE_DEPTH = 3; +const DEFAULT_SNIPPET_LINES = 10; +const MAX_FILE_BYTES_TO_PARSE = 1024 * 1024; +const MAX_NODES_UPPER_BOUND = 6000; + +const IGNORED_DIRS = new Set([ + ".git", + "node_modules", + "dist", + "build", + "out", + ".next", + ".turbo", + ".cache", + "coverage", + ".vscode", + ".idea" +]); + +const JS_LIKE_EXTS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]); +const TEXT_LIKE_EXTS = new Set([ + ...JS_LIKE_EXTS, + ".json", + ".md", + ".txt", + ".yml", + ".yaml", + ".html", + ".css", + ".scss", + ".py", + ".java", + ".go", + ".rs", + ".c", + ".cpp", + ".h", + ".hpp", + ".cs", + ".php" +]); + +const PARSER_PROFILES = { + "js-ts-deep": { + id: "js-ts-deep", + parseJsTs: true, + symbolsInDetail: true, + callsInDetail: true, + extendsInDetail: true + }, + balanced: { + id: "balanced", + parseJsTs: true, + symbolsInDetail: false, + callsInDetail: false, + extendsInDetail: false + }, + "structure-only": { + id: "structure-only", + parseJsTs: false, + symbolsInDetail: false, + callsInDetail: false, + extendsInDetail: false + } +}; + +const traverseFn = traverse?.default || traverse; + +const getRepoDir = (basePath, owner, repoName) => { + const safeOwner = String(owner || "").replace(/[^a-zA-Z0-9-_]/g, ""); + const safeRepo = String(repoName || "").replace(/[^a-zA-Z0-9-_]/g, ""); + return path.join(basePath, `${safeOwner}__${safeRepo}`); +}; + +const clampNum = (value, min, max, fallback) => { + const n = Number(value); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, Math.floor(n))); +}; + +const parseExpandDepth = (value, fallback = DEFAULT_EXPAND_DEPTH) => { + if (value === undefined || value === null || value === "") return fallback; + if (String(value).toLowerCase() === "all") return Number.POSITIVE_INFINITY; + const n = Number(value); + if (!Number.isFinite(n)) return fallback; + if (n <= 0) return Number.POSITIVE_INFINITY; + return Math.max(1, Math.min(100, Math.floor(n))); +}; + +const toPosixPath = (p) => String(p).split(path.sep).join("/"); + +const safeRepoRelative = (repoDir, absPath) => { + const rel = path.relative(repoDir, absPath); + const norm = toPosixPath(rel); + if (!norm || norm.startsWith("..")) return null; + return norm; +}; + +const inc = (obj, key) => { + obj[key] = (obj[key] || 0) + 1; +}; + +const isProbablyTextFile = (filePath) => { + const ext = path.extname(filePath).toLowerCase(); + return TEXT_LIKE_EXTS.has(ext); +}; + +const countLines = (content) => { + if (!content) return 0; + let lines = 1; + for (let i = 0; i < content.length; i++) { + if (content[i] === "\n") lines++; + } + return lines; +}; + +const safeReadUtf8 = (filePath, maxBytes = MAX_FILE_BYTES_TO_PARSE) => { + try { + const s = fs.statSync(filePath); + if (!s.isFile() || s.size > maxBytes) return null; + return fs.readFileSync(filePath, "utf-8"); + } catch { + return null; + } +}; + +const makeDocs = (partial = {}) => ({ + summary: partial.summary || "", + exports: Array.isArray(partial.exports) ? partial.exports : [], + imports: Array.isArray(partial.imports) ? partial.imports : [], + dependents: Array.isArray(partial.dependents) ? partial.dependents : [], + snippetPreview: partial.snippetPreview || "", + filePath: partial.filePath || null, + absolutePath: partial.absolutePath || null, + lineStart: Number.isFinite(Number(partial.lineStart)) ? Number(partial.lineStart) : null, + lineEnd: Number.isFinite(Number(partial.lineEnd)) ? Number(partial.lineEnd) : null +}); + +const getNodeStyle = (kind) => { + if (kind === "module") { + return { + background: "#16213e", + color: "white", + border: "1px solid rgba(96,165,250,0.55)", + width: 260, + fontSize: "12px", + borderRadius: 12 + }; + } + if (kind === "directory") { + return { + background: "#1f2937", + color: "white", + border: "1px solid rgba(148,163,184,0.45)", + width: 240, + fontSize: "11px", + borderRadius: 10 + }; + } + if (kind === "file") { + return { + background: "#111827", + color: "white", + border: "1px solid rgba(16,185,129,0.55)", + width: 300, + fontSize: "11px", + borderRadius: 10 + }; + } + return { + background: "#3b0764", + color: "white", + border: "1px solid rgba(192,132,252,0.6)", + width: 230, + fontSize: "11px", + borderRadius: 10 + }; +}; + +const getEdgeStyle = (relation) => { + if (relation === "contains") return { stroke: "#64748b", strokeWidth: 1.35 }; + if (relation === "imports") return { stroke: "#10b981", strokeWidth: 1.9 }; + if (relation === "extends") return { stroke: "#f59e0b", strokeWidth: 1.8, strokeDasharray: "5 4" }; + return { stroke: "#8b5cf6", strokeWidth: 1.8 }; +}; + +const makeNode = ({ id, label, kind, docs }) => ({ + id, + kind, + type: "default", + data: { label, kind, docs }, + docs, + position: { x: 0, y: 0 }, + style: getNodeStyle(kind) +}); + +const makeEdge = ({ source, target, relation }) => ({ + id: `${relation}:${source}->${target}`, + source, + target, + relation, + type: "smoothstep", + animated: relation === "calls", + style: getEdgeStyle(relation) +}); + +const getSnippet = (lines, lineStart, lineEnd, maxLines) => { + if (!Array.isArray(lines) || lines.length === 0) return ""; + const start = Math.max(1, Number(lineStart) || 1); + const rawEnd = Number(lineEnd) || start; + const end = Math.max(start, rawEnd); + const hardEnd = Math.min(lines.length, start + Math.max(1, maxLines) - 1, end); + return lines.slice(start - 1, hardEnd).join("\n").trim(); +}; + +const summarizeImportSpec = (spec) => { + if (!spec || typeof spec !== "string") return ""; + return spec.startsWith(".") ? `local:${spec}` : `package:${spec}`; +}; + +const resolveRelativeImport = (fromAbsFile, spec) => { + if (!spec || typeof spec !== "string") return null; + if (!spec.startsWith(".")) return null; + + const baseDir = path.dirname(fromAbsFile); + const rawTarget = path.resolve(baseDir, spec); + const ext = path.extname(rawTarget); + const candidates = []; + + if (ext) { + candidates.push(rawTarget); + } else { + for (const e of JS_LIKE_EXTS) candidates.push(rawTarget + e); + for (const e of JS_LIKE_EXTS) candidates.push(path.join(rawTarget, "index" + e)); + } + + for (const c of candidates) { + try { + if (fs.existsSync(c) && fs.statSync(c).isFile()) return c; + } catch { + // ignore + } + } + + return null; +}; + +const getCalleeName = (calleeNode) => { + if (!calleeNode) return null; + if (calleeNode.type === "Identifier") return calleeNode.name; + if (calleeNode.type === "MemberExpression" && calleeNode.property?.type === "Identifier") { + return calleeNode.property.name; + } + return null; +}; +const analyzeJsTsFile = (content, { snippetLines }) => { + const imports = []; + const exportsSet = new Set(); + const symbols = []; + const calls = []; + const extendsRels = []; + + let ast; + try { + ast = parse(content, { + sourceType: "unambiguous", + plugins: ["typescript", "jsx", "dynamicImport", "classProperties"] + }); + } catch { + return { imports, exports: [], symbols, calls, extendsRels, parseOk: false }; + } + + const lines = content.split(/\r?\n/); + const symbolStack = []; + + const addSymbol = (name, symbolType, node) => { + if (!name || typeof name !== "string") return null; + const lineStart = node?.loc?.start?.line || null; + const lineEnd = node?.loc?.end?.line || lineStart; + + const symbol = { + key: `${name}:${lineStart || 0}:${symbols.length}`, + name, + symbolType, + lineStart, + lineEnd, + snippetPreview: getSnippet(lines, lineStart || 1, lineEnd || lineStart || 1, snippetLines) + }; + symbols.push(symbol); + return symbol; + }; + + traverseFn(ast, { + ImportDeclaration(p) { + const source = p.node.source?.value; + if (typeof source === "string") imports.push(source); + }, + ExportNamedDeclaration(p) { + const source = p.node.source?.value; + if (typeof source === "string") imports.push(source); + + const decl = p.node.declaration; + if (decl?.type === "FunctionDeclaration" && decl.id?.name) exportsSet.add(decl.id.name); + if (decl?.type === "ClassDeclaration" && decl.id?.name) exportsSet.add(decl.id.name); + if (decl?.type === "VariableDeclaration") { + for (const d of decl.declarations || []) { + if (d.id?.type === "Identifier") exportsSet.add(d.id.name); + } + } + + for (const s of p.node.specifiers || []) { + if (s.exported?.name) exportsSet.add(s.exported.name); + } + }, + ExportDefaultDeclaration(p) { + const decl = p.node.declaration; + if (decl?.id?.name) exportsSet.add(`default(${decl.id.name})`); + else exportsSet.add("default"); + }, + FunctionDeclaration: { + enter(p) { + if (!p.node.id?.name) return; + const sym = addSymbol(p.node.id.name, "function", p.node); + if (sym) symbolStack.push(sym.key); + }, + exit(p) { + if (!p.node.id?.name) return; + symbolStack.pop(); + } + }, + VariableDeclarator: { + enter(p) { + const init = p.node.init; + if (!p.node.id?.name) return; + if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) { + const sym = addSymbol(p.node.id.name, "function", p.node); + if (sym) symbolStack.push(sym.key); + } + }, + exit(p) { + const init = p.node.init; + if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) { + symbolStack.pop(); + } + } + }, + ClassDeclaration: { + enter(p) { + if (!p.node.id?.name) return; + const sym = addSymbol(p.node.id.name, "class", p.node); + if (sym) { + symbolStack.push(sym.key); + const superClass = p.node.superClass; + if (superClass?.type === "Identifier") { + extendsRels.push({ fromSymbolKey: sym.key, toName: superClass.name }); + } + } + }, + exit(p) { + if (!p.node.id?.name) return; + symbolStack.pop(); + } + }, + ClassMethod: { + enter(p) { + if (!p.node.key?.name) return; + const sym = addSymbol(p.node.key.name, "method", p.node); + if (sym) symbolStack.push(sym.key); + }, + exit(p) { + if (!p.node.key?.name) return; + symbolStack.pop(); + } + }, + CallExpression(p) { + const calleeName = getCalleeName(p.node.callee); + if (!calleeName) return; + const fromSymbolKey = symbolStack.length ? symbolStack[symbolStack.length - 1] : null; + calls.push({ fromSymbolKey, toName: calleeName }); + } + }); + + return { + imports, + exports: Array.from(exportsSet), + symbols, + calls, + extendsRels, + parseOk: true + }; +}; + +const normalizeView = (viewRaw) => { + const value = String(viewRaw || "overview").toLowerCase(); + return value === "detail" ? "detail" : "overview"; +}; + +const resolveParserProfile = (langProfileRaw, view) => { + const key = String(langProfileRaw || "js-ts-deep").toLowerCase(); + const base = PARSER_PROFILES[key] || PARSER_PROFILES["js-ts-deep"]; + const isDetail = view === "detail"; + + return { + id: base.id, + parseJsTs: base.parseJsTs, + includeImports: base.parseJsTs, + includeSymbols: Boolean(base.parseJsTs && base.symbolsInDetail && isDetail), + includeCalls: Boolean(base.parseJsTs && base.callsInDetail && isDetail), + includeExtends: Boolean(base.parseJsTs && base.extendsInDetail && isDetail) + }; +}; + +const initializeGraph = ({ maxNodes }) => { + const nodesById = new Map(); + const edgesById = new Map(); + + const addNode = (node, stats) => { + if (!node?.id) return null; + if (nodesById.has(node.id)) return nodesById.get(node.id); + if (nodesById.size >= maxNodes) { + stats.truncated = true; + return null; + } + nodesById.set(node.id, node); + return node; + }; + + const patchNodeDocs = (nodeId, patch = {}) => { + const node = nodesById.get(nodeId); + if (!node) return; + const docs = makeDocs({ ...(node.docs || {}), ...patch }); + node.docs = docs; + node.data = { ...(node.data || {}), docs }; + }; + + const addEdge = ({ source, target, relation }) => { + if (!source || !target || !relation) return null; + if (!nodesById.has(source) || !nodesById.has(target)) return null; + const edge = makeEdge({ source, target, relation }); + if (!edgesById.has(edge.id)) edgesById.set(edge.id, edge); + return edgesById.get(edge.id); + }; + + return { nodesById, edgesById, addNode, patchNodeDocs, addEdge }; +}; +const createDependencyNode = ({ id, label, kind, docs }) => ({ + id, + kind, + type: "default", + data: { label, kind, docs }, + docs, + position: { x: 0, y: 0 }, + style: getNodeStyle(kind) +}); + +export const getDependencyGraph = async (req, res) => { + try { + const { owner, repo, repoName, path: filePath, snippetLines } = req.query; + const targetRepo = repo || repoName; + const user = await User.findById(req.user._id); + + if (!user || !owner || !targetRepo || !filePath) { + return res.status(400).json({ error: "Missing parameters" }); + } + + const snippetLineCount = clampNum(snippetLines, 3, 40, DEFAULT_SNIPPET_LINES); + + let content = null; + let contentSource = "local"; + let fullPath = null; + + if (user?.repoBasePath || DEFAULT_REPO_BASE_PATH) { + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + const repoDir = getRepoDir(basePath, owner, targetRepo); + fullPath = path.normalize(path.join(repoDir, filePath)); + + if (!fullPath.startsWith(repoDir)) { + return res.status(400).json({ error: "Invalid file path" }); + } + + if (!fs.existsSync(fullPath)) { + return res.status(404).json({ error: "File not found locally. Pull repo first." }); + } + content = fs.readFileSync(fullPath, "utf-8"); + } else { + contentSource = "github"; + } + + if (!content && contentSource === "github") { + const url = `https://api.github.com/repos/${owner}/${targetRepo}/contents/${filePath}`; + const ghRes = await axios.get(url, { + headers: { + Authorization: `Bearer ${user.githubToken}`, + Accept: "application/vnd.github.v3+json" + } + }); + + if (!ghRes.data.content) { + return res.status(400).json({ error: "File is too large or empty." }); + } + content = Buffer.from(ghRes.data.content, "base64").toString("utf-8"); + } + + const ext = path.extname(filePath).toLowerCase(); + if (!JS_LIKE_EXTS.has(ext)) { + return res.status(400).json({ error: "Unsupported file type for graph. Use JS/TS files." }); + } + + const parsed = analyzeJsTsFile(content, { snippetLines: snippetLineCount }); + const lines = content.split(/\r?\n/); + + const nodes = []; + const edges = []; + const fileNodeId = `file:${filePath}`; + + nodes.push(createDependencyNode({ + id: fileNodeId, + label: path.basename(filePath), + kind: "file", + docs: makeDocs({ + summary: "Selected file dependency graph", + filePath, + absolutePath: fullPath, + snippetPreview: getSnippet(lines, 1, snippetLineCount, snippetLineCount) + }) + })); + + const symbolByName = new Map(); + const symbolByKey = new Map(); + + for (const symbol of parsed.symbols) { + const symbolId = `sym:${filePath}:${symbol.name}:${symbol.lineStart || 0}:${symbol.symbolType}`; + symbolByName.set(symbol.name, symbolId); + symbolByKey.set(symbol.key, symbolId); + + nodes.push(createDependencyNode({ + id: symbolId, + label: symbol.name, + kind: "symbol", + docs: makeDocs({ + summary: `${symbol.symbolType} in ${path.basename(filePath)}`, + filePath, + absolutePath: fullPath, + lineStart: symbol.lineStart, + lineEnd: symbol.lineEnd, + snippetPreview: symbol.snippetPreview + }) + })); + + edges.push({ + id: `contains:${fileNodeId}->${symbolId}`, + source: fileNodeId, + target: symbolId, + relation: "contains", + style: getEdgeStyle("contains") + }); + } + + const edgeIds = new Set(edges.map((e) => e.id)); + for (const call of parsed.calls) { + if (!call.fromSymbolKey) continue; + const sourceId = symbolByKey.get(call.fromSymbolKey); + const targetId = symbolByName.get(call.toName); + if (!sourceId || !targetId) continue; + + const id = `calls:${sourceId}->${targetId}`; + if (edgeIds.has(id)) continue; + edgeIds.add(id); + edges.push({ + id, + source: sourceId, + target: targetId, + relation: "calls", + animated: true, + style: getEdgeStyle("calls") + }); + } + + return res.json({ + nodes, + edges, + stats: { + source: contentSource, + totalFiles: 1, + totalDirs: 0, + totalLines: countLines(content), + graphNodes: nodes.length, + graphEdges: edges.length + }, + capabilities: { + view: "detail", + langProfile: "js-ts-deep", + supportedLangProfiles: Object.keys(PARSER_PROFILES), + parsers: ["js-ts-parser"], + relations: ["contains", "calls"], + localAnalysis: contentSource === "local" + } + }); + } catch (error) { + console.error("Viz Error:", error); + return res.status(500).json({ error: error.message }); + } +}; +const buildLocalDocumentationGraph = (repoDir, options) => { + const { maxNodes, expandDepth, snippetLines, view, parserProfile } = options; + + const stats = { + source: "local", + totalFiles: 0, + totalDirs: 0, + totalLines: 0, + extensionCounts: {}, + truncated: false, + note: null + }; + + const graph = initializeGraph({ maxNodes }); + const visibleFiles = []; + const fileNodeByRel = new Map(); + + const rootId = "root"; + graph.addNode(makeNode({ + id: rootId, + label: "/", + kind: "directory", + docs: makeDocs({ summary: "Repository root", filePath: "/" }) + }), stats); + + const walk = (dirAbs, relDir, depth, parentVisibleId) => { + let entries = []; + try { + entries = fs.readdirSync(dirAbs, { withFileTypes: true }); + } catch { + return; + } + + for (const ent of entries) { + if (ent.isDirectory() && IGNORED_DIRS.has(ent.name)) continue; + + const childAbs = path.join(dirAbs, ent.name); + const childRel = relDir ? `${relDir}/${ent.name}` : ent.name; + const childDepth = depth + 1; + + if (ent.isDirectory()) { + stats.totalDirs++; + let nextParentVisibleId = parentVisibleId; + + if (childDepth <= expandDepth) { + const kind = childDepth === 1 ? "module" : "directory"; + const dirNodeId = `dir:${childRel}`; + const node = graph.addNode(makeNode({ + id: dirNodeId, + label: ent.name, + kind, + docs: makeDocs({ + summary: kind === "module" ? "Module namespace" : "Directory", + filePath: childRel, + absolutePath: childAbs + }) + }), stats); + + if (node) { + graph.addEdge({ source: parentVisibleId, target: dirNodeId, relation: "contains" }); + nextParentVisibleId = dirNodeId; + } + } + + walk(childAbs, childRel, childDepth, nextParentVisibleId); + continue; + } + + if (!ent.isFile()) continue; + + stats.totalFiles++; + const ext = path.extname(ent.name).toLowerCase() || "(none)"; + inc(stats.extensionCounts, ext); + + const maybeContent = isProbablyTextFile(childAbs) ? safeReadUtf8(childAbs) : null; + if (typeof maybeContent === "string") { + stats.totalLines += countLines(maybeContent); + } + + if (childDepth > expandDepth) continue; + + const fileNodeId = `file:${childRel}`; + const node = graph.addNode(makeNode({ + id: fileNodeId, + label: ent.name, + kind: "file", + docs: makeDocs({ + summary: `File${ext !== "(none)" ? ` (${ext})` : ""}`, + filePath: childRel, + absolutePath: childAbs, + snippetPreview: typeof maybeContent === "string" + ? getSnippet(maybeContent.split(/\r?\n/), 1, snippetLines, snippetLines) + : "" + }) + }), stats); + + if (!node) continue; + + fileNodeByRel.set(childRel, fileNodeId); + graph.addEdge({ source: parentVisibleId, target: fileNodeId, relation: "contains" }); + + visibleFiles.push({ + relPath: childRel, + absPath: childAbs, + nodeId: fileNodeId, + ext, + content: typeof maybeContent === "string" ? maybeContent : null + }); + } + }; + + walk(repoDir, "", 0, rootId); + + const dependents = new Map(); + + if (parserProfile.parseJsTs) { + for (const file of visibleFiles) { + const isJs = JS_LIKE_EXTS.has(file.ext); + const isSupportedGeneric = [".py", ".java", ".go", ".cs"].includes(file.ext); + if (!isJs && !isSupportedGeneric) continue; + + const content = file.content ?? safeReadUtf8(file.absPath); + if (!content) continue; + + const parsed = isJs ? analyzeJsTsFile(content, { snippetLines }) : analyzeGenericFile(content, file.ext, snippetLines); + if (!parsed || !parsed.parseOk) continue; + + const importsResolved = []; + for (const spec of parsed.imports) { + let resolvedAbs = null; + if (isJs) { + resolvedAbs = resolveRelativeImport(file.absPath, spec); + } else if (spec.startsWith(".")) { + try { + const baseDir = path.dirname(file.absPath); + const candidate = path.resolve(baseDir, spec); + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { + resolvedAbs = candidate; + } else if (fs.existsSync(candidate + file.ext)) { + resolvedAbs = candidate + file.ext; + } + } catch(e) {} + } + + const resolvedRel = resolvedAbs ? safeRepoRelative(repoDir, resolvedAbs) : null; + const targetNodeId = resolvedRel ? fileNodeByRel.get(resolvedRel) : null; + + importsResolved.push(resolvedRel || summarizeImportSpec(spec)); + + if (parserProfile.includeImports && targetNodeId) { + graph.addEdge({ source: file.nodeId, target: targetNodeId, relation: "imports" }); + if (!dependents.has(targetNodeId)) dependents.set(targetNodeId, new Set()); + dependents.get(targetNodeId).add(file.relPath); + } + } + + graph.patchNodeDocs(file.nodeId, { + imports: importsResolved.slice(0, 50), + exports: parsed.exports.slice(0, 50) + }); + + if (!parserProfile.includeSymbols) continue; + + const localByName = new Map(); + const localByKey = new Map(); + + for (const symbol of parsed.symbols) { + const symbolNodeId = `sym:${file.relPath}:${symbol.name}:${symbol.lineStart || 0}:${symbol.symbolType}`; + const symbolNode = graph.addNode(makeNode({ + id: symbolNodeId, + label: symbol.name, + kind: "symbol", + docs: makeDocs({ + summary: `${symbol.symbolType} in ${path.basename(file.relPath)}`, + filePath: file.relPath, + absolutePath: file.absPath, + lineStart: symbol.lineStart, + lineEnd: symbol.lineEnd, + snippetPreview: symbol.snippetPreview + }) + }), stats); + + if (!symbolNode) continue; + + graph.addEdge({ source: file.nodeId, target: symbolNodeId, relation: "contains" }); + if (!localByName.has(symbol.name)) localByName.set(symbol.name, symbolNodeId); + localByKey.set(symbol.key, symbolNodeId); + } + + if (parserProfile.includeCalls) { + for (const call of parsed.calls) { + if (!call.fromSymbolKey) continue; + const sourceId = localByKey.get(call.fromSymbolKey); + const targetId = localByName.get(call.toName); + if (!sourceId || !targetId) continue; + graph.addEdge({ source: sourceId, target: targetId, relation: "calls" }); + } + } + + if (parserProfile.includeExtends) { + for (const extRel of parsed.extendsRels) { + const sourceId = localByKey.get(extRel.fromSymbolKey); + const targetId = localByName.get(extRel.toName); + if (!sourceId || !targetId) continue; + graph.addEdge({ source: sourceId, target: targetId, relation: "extends" }); + } + } + } + } + + for (const [nodeId, refs] of dependents.entries()) { + graph.patchNodeDocs(nodeId, { + dependents: Array.from(refs).slice(0, 50) + }); + } + + if (view === "overview") { + stats.note = "Overview mode: structure-first. Switch to detail mode for symbols and call relations."; + } + + const nodes = Array.from(graph.nodesById.values()); + const edges = Array.from(graph.edgesById.values()); + + return { + nodes, + edges, + stats: { + ...stats, + graphNodes: nodes.length, + graphEdges: edges.length + }, + capabilities: { + view, + langProfile: parserProfile.id, + supportedLangProfiles: Object.keys(PARSER_PROFILES), + parsers: parserProfile.parseJsTs ? ["js-ts-parser", "fallback-structure"] : ["fallback-structure"], + relations: [ + "contains", + ...(parserProfile.includeImports ? ["imports"] : []), + ...(parserProfile.includeCalls ? ["calls"] : []), + ...(parserProfile.includeExtends ? ["extends"] : []) + ], + localAnalysis: true + } + }; +}; +const buildRemoteTreeDocumentationGraph = async (owner, repoName, githubToken, options) => { + const { maxNodes, expandDepth, view, parserProfile } = options; + + const headers = { + Authorization: `Bearer ${githubToken}`, + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28" + }; + + const repoRes = await axios.get(`https://api.github.com/repos/${owner}/${repoName}`, { headers }); + const defaultBranch = repoRes.data?.default_branch || "main"; + + let tree = []; + try { + const treeRes = await axios.get( + `https://api.github.com/repos/${owner}/${repoName}/git/trees/${encodeURIComponent(defaultBranch)}?recursive=1`, + { headers } + ); + tree = treeRes.data?.tree || []; + } catch (e) { + if (e?.response?.status === 409) tree = []; + else throw e; + } + + const stats = { + source: "github", + defaultBranch, + totalFiles: 0, + totalDirs: 0, + totalLines: null, + extensionCounts: {}, + truncated: false, + note: "Remote mode is structural only. Pull repository locally for deep symbol/import/call documentation." + }; + + for (const item of tree) { + if (!item?.path || (item.type !== "tree" && item.type !== "blob")) continue; + if (item.type === "tree") stats.totalDirs++; + if (item.type === "blob") { + stats.totalFiles++; + inc(stats.extensionCounts, path.extname(item.path).toLowerCase() || "(none)"); + } + } + + const graph = initializeGraph({ maxNodes }); + const rootId = "root"; + graph.addNode(makeNode({ + id: rootId, + label: "/", + kind: "directory", + docs: makeDocs({ summary: "Repository root", filePath: "/" }) + }), stats); + + const depthLimit = Math.max(1, Number(expandDepth) || DEFAULT_REMOTE_DEPTH); + + for (const item of tree) { + if (!item?.path || (item.type !== "tree" && item.type !== "blob")) continue; + + const parts = String(item.path).split("/"); + if (parts.length > depthLimit) continue; + + let parentId = rootId; + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const prefix = parts.slice(0, i + 1).join("/"); + const isLeaf = i === parts.length - 1; + const kind = isLeaf + ? (item.type === "blob" ? "file" : (i === 0 ? "module" : "directory")) + : (i === 0 ? "module" : "directory"); + const nodeId = `${kind === "file" ? "file" : "dir"}:${prefix}`; + + const node = graph.addNode(makeNode({ + id: nodeId, + label: part, + kind, + docs: makeDocs({ + summary: kind === "file" ? "Remote file metadata" : "Directory", + filePath: prefix + }) + }), stats); + + if (node) { + graph.addEdge({ source: parentId, target: nodeId, relation: "contains" }); + parentId = nodeId; + } + + if (stats.truncated) break; + } + + if (stats.truncated) break; + } + + const nodes = Array.from(graph.nodesById.values()); + const edges = Array.from(graph.edgesById.values()); + + return { + nodes, + edges, + stats: { + ...stats, + graphNodes: nodes.length, + graphEdges: edges.length + }, + capabilities: { + view, + langProfile: parserProfile.id, + supportedLangProfiles: Object.keys(PARSER_PROFILES), + parsers: ["remote-tree-only"], + relations: ["contains"], + localAnalysis: false + } + }; +}; + +export const getAppStructure = async (req, res) => { + try { + const { + owner, + repo, + repoName, + view: viewRaw, + langProfile: langProfileRaw, + maxNodes, + expandDepth, + depth, + snippetLines + } = req.query; + + const targetRepo = repo || repoName; + const view = normalizeView(viewRaw); + const parserProfile = resolveParserProfile(langProfileRaw, view); + const maxNodesNum = clampNum(maxNodes, 50, MAX_NODES_UPPER_BOUND, DEFAULT_MAX_NODES); + const expandDepthNum = parseExpandDepth(expandDepth ?? depth, DEFAULT_EXPAND_DEPTH); + const snippetLinesNum = clampNum(snippetLines, 3, 40, DEFAULT_SNIPPET_LINES); + + const user = await User.findById(req.user._id); + if (!user || !user.githubToken) { + return res.status(401).json({ error: "No GitHub token. Please login again." }); + } + + if (owner && targetRepo) { + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + const repoDir = getRepoDir(basePath, String(owner), String(targetRepo)); + + if (fs.existsSync(repoDir)) { + return res.json(buildLocalDocumentationGraph(repoDir, { + maxNodes: maxNodesNum, + expandDepth: expandDepthNum, + snippetLines: snippetLinesNum, + view, + parserProfile + })); + } + + const remote = await buildRemoteTreeDocumentationGraph(String(owner), String(targetRepo), user.githubToken, { + maxNodes: maxNodesNum, + expandDepth: expandDepthNum, + view, + parserProfile + }); + return res.json(remote); + } + + const localBase = path.join(process.cwd(), "src"); + if (!fs.existsSync(localBase)) { + return res.json({ + nodes: [], + edges: [], + stats: { source: "local", totalFiles: 0, totalDirs: 0, totalLines: 0, graphNodes: 0, graphEdges: 0 }, + capabilities: { + view, + langProfile: parserProfile.id, + supportedLangProfiles: Object.keys(PARSER_PROFILES), + parsers: ["fallback-structure"], + relations: ["contains"], + localAnalysis: true + } + }); + } + + return res.json(buildLocalDocumentationGraph(localBase, { + maxNodes: maxNodesNum, + expandDepth: expandDepthNum, + snippetLines: snippetLinesNum, + view, + parserProfile + })); + } catch (error) { + const status = error?.response?.status; + const gh = error?.response?.data; + + if (status) { + return res.status(status).json({ + error: gh?.message || "Failed to visualize repository" + }); + } + + console.error("Visualizer Error:", error.message); + return res.status(500).json({ error: "Failed to visualize architecture: " + error.message }); + } +}; + +const readDirFiles = (dirAbs, filterFn) => { + const out = []; + const walk = (curr) => { + let entries = []; + try { + entries = fs.readdirSync(curr, { withFileTypes: true }); + } catch { + return; + } + + for (const ent of entries) { + if (ent.isDirectory() && IGNORED_DIRS.has(ent.name)) continue; + const abs = path.join(curr, ent.name); + if (ent.isDirectory()) { + walk(abs); + continue; + } + if (ent.isFile()) { + if (!filterFn || filterFn(abs)) out.push(abs); + } + } + }; + + walk(dirAbs); + return out; +}; + +const resolveImportPath = (fromAbsFile, importSpec) => { + if (!importSpec || typeof importSpec !== "string") return null; + if (!importSpec.startsWith(".")) return null; + const baseDir = path.dirname(fromAbsFile); + const rawTarget = path.resolve(baseDir, importSpec); + const ext = path.extname(rawTarget); + + const candidates = ext + ? [rawTarget] + : [ + `${rawTarget}.js`, + `${rawTarget}.ts`, + `${rawTarget}.mjs`, + `${rawTarget}.cjs`, + path.join(rawTarget, "index.js"), + path.join(rawTarget, "index.ts") + ]; + + for (const candidate of candidates) { + try { + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate; + } catch { + // ignore + } + } + + return null; +}; + +const findBackendSrcDir = (repoDir) => { + const candidates = [ + path.join(repoDir, "backend", "src"), + path.join(repoDir, "server", "src"), + path.join(repoDir, "api", "src"), + path.join(repoDir, "backend"), + path.join(repoDir, "server"), + path.join(repoDir, "api"), + path.join(repoDir, "src"), + repoDir + ]; + + for (const dir of candidates) { + if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) { + const entryFiles = ["index.js", "index.ts", "server.js", "server.ts", "app.js", "app.ts", "main.js", "main.ts"]; + if (entryFiles.some(f => fs.existsSync(path.join(dir, f)))) return dir; + } + } + + for (const dir of candidates) { + if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) return dir; + } + + return repoDir; +}; + +const extractLocalImports = (content, absFile) => { + const byIdentifier = new Map(); + let ast; + try { + ast = parse(content, { + sourceType: "unambiguous", + plugins: ["typescript", "jsx", "dynamicImport"] + }); + } catch { + return byIdentifier; + } + + traverseFn(ast, { + ImportDeclaration(p) { + const src = p.node.source?.value; + if (typeof src !== "string") return; + const resolved = resolveImportPath(absFile, src); + if (!resolved) return; + + for (const spec of p.node.specifiers || []) { + if (spec.local?.name) { + byIdentifier.set(spec.local.name, { resolved, source: src }); + } + } + }, + VariableDeclarator(p) { + if (p.node.init?.type !== "CallExpression") return; + if (p.node.init.callee?.type !== "Identifier" || p.node.init.callee.name !== "require") return; + const arg0 = p.node.init.arguments?.[0]; + if (arg0?.type !== "StringLiteral") return; + const src = arg0.value; + const resolved = resolveImportPath(absFile, src); + if (!resolved) return; + + if (p.node.id?.type === "Identifier") { + byIdentifier.set(p.node.id.name, { resolved, source: src }); + return; + } + if (p.node.id?.type === "ObjectPattern") { + for (const prop of p.node.id.properties || []) { + if (prop.type === "ObjectProperty" && prop.value?.type === "Identifier") { + byIdentifier.set(prop.value.name, { resolved, source: src }); + } + } + } + } + }); + + return byIdentifier; +}; + +const normalizeJoinedPath = (prefix, routePath) => { + const p1 = String(prefix || "").trim(); + const p2 = String(routePath || "").trim(); + + const left = p1.endsWith("/") ? p1.slice(0, -1) : p1; + const right = p2.startsWith("/") ? p2 : `/${p2}`; + const joined = `${left}${right}`.replace(/\/{2,}/g, "/"); + return joined || "/"; +}; + +const parseGlobalDataFlow = (repoDir) => { + const jsFiles = readDirFiles(repoDir, (abs) => [".js", ".ts", ".mjs", ".cjs"].includes(path.extname(abs).toLowerCase())); + const pythonFiles = readDirFiles(repoDir, (abs) => abs.endsWith(".py")); + const javaFiles = readDirFiles(repoDir, (abs) => abs.endsWith(".java")); + const goFiles = readDirFiles(repoDir, (abs) => abs.endsWith(".go")); + + const genericRoutes = [ + ...parseGenericDataFlow(repoDir, ".py", pythonFiles), + ...parseGenericDataFlow(repoDir, ".java", javaFiles), + ...parseGenericDataFlow(repoDir, ".go", goFiles) + ]; + + const fileMap = new Map(); + + for (const abs of jsFiles) { + const content = safeReadUtf8(abs, 2 * 1024 * 1024); + if (!content) continue; + if (!/\b(use|get|post|put|patch|delete|all|require|import)\b/i.test(content)) continue; + + const imports = extractLocalImports(content, abs); + const routes = []; + const mounts = []; + + let ast; + try { + ast = parse(content, { sourceType: "unambiguous", plugins: ["typescript", "jsx", "dynamicImport"] }); + } catch { + continue; + } + + traverseFn(ast, { + CallExpression(p) { + const callee = p.node.callee; + if (callee?.type !== "MemberExpression") return; + const propName = callee.property?.name; + if (!propName) return; + + const args = p.node.arguments || []; + if (propName === "use" && args.length >= 2 && args[0].type === "StringLiteral") { + const mountPrefix = args[0].value; + const arg1 = args[1]; + let targetResolved = null; + if (arg1.type === "Identifier") { + targetResolved = imports.get(arg1.name)?.resolved; + } else if (arg1.type === "CallExpression" && arg1.callee?.name === "require") { + const reqArg = arg1.arguments?.[0]; + if (reqArg?.type === "StringLiteral") { + targetResolved = resolveImportPath(abs, reqArg.value); + } + } + if (targetResolved) mounts.push({ mountPrefix, targetResolved }); + } + + const method = String(propName).toUpperCase(); + if (["GET", "POST", "PUT", "PATCH", "DELETE", "ALL"].includes(method)) { + if (args.length >= 2 && args[0].type === "StringLiteral") { + const routePath = args[0].value; + const objName = callee.object?.name; + if (objName && ["axios", "request", "agent", "supertest", "fetch", "http", "httpClient"].includes(objName)) return; + + const handlers = []; + for (let i = 1; i < args.length; i++) { + const h = args[i]; + if (h.type === "Identifier") { + handlers.push({ name: h.name, controllerFile: imports.get(h.name)?.resolved || abs }); + } else if (h.type === "MemberExpression" && h.property?.type === "Identifier") { + const oName = h.object?.name; + handlers.push({ name: h.property.name, controllerFile: oName ? (imports.get(oName)?.resolved || abs) : abs }); + } else if (["ArrowFunctionExpression", "FunctionExpression"].includes(h.type)) { + handlers.push({ name: "inline_handler", controllerFile: abs }); + } + } + if (handlers.length > 0) { + routes.push({ method, path: routePath, handlers }); + } + } + } + } + }); + + if (routes.length > 0 || mounts.length > 0) { + fileMap.set(abs, { absPath: abs, routes, mounts, content }); + } + } + + const globalPrefixes = new Map(); + let changed = true; + while(changed) { + changed = false; + for (const [abs, data] of fileMap.entries()) { + for (const mount of data.mounts) { + const target = mount.targetResolved; + const parentPrefix = globalPrefixes.get(abs) || ""; + const absolutePrefix = normalizeJoinedPath(parentPrefix, mount.mountPrefix); + + const currentPrefix = globalPrefixes.get(target); + if (currentPrefix !== absolutePrefix) { + globalPrefixes.set(target, absolutePrefix); + changed = true; + } + } + } + } + + const allRoutes = [...genericRoutes]; + for (const [abs, data] of fileMap.entries()) { + const filePrefix = globalPrefixes.get(abs) || ""; + for (const r of data.routes) { + allRoutes.push({ + method: r.method, + path: normalizeJoinedPath(filePrefix, r.path), + routeFile: abs, + handlers: r.handlers + }); + } + } + + return allRoutes; +}; + +const parseControllerDetails = (controllerAbs) => { + const content = safeReadUtf8(controllerAbs, 2 * 1024 * 1024); + if (!content) return { functions: new Map() }; + + const imports = extractLocalImports(content, controllerAbs); + const modelIdentifiers = new Set(); + for (const [ident, imp] of imports.entries()) { + if (imp.resolved.includes(`${path.sep}models${path.sep}`)) modelIdentifiers.add(ident); + } + + const functionMap = new Map(); + let ast; + try { + ast = parse(content, { + sourceType: "unambiguous", + plugins: ["typescript", "jsx", "dynamicImport"] + }); + } catch { + return { functions: functionMap }; + } + + const lines = content.split(/\r?\n/); + + const registerFn = (name, node) => { + if (!name || !node) return; + const lineStart = node.loc?.start?.line || null; + const lineEnd = node.loc?.end?.line || lineStart; + const snippetPreview = getSnippet(lines, lineStart || 1, lineEnd || lineStart || 1, 16); + + const usedModels = new Set(); + const calledServices = new Set(); + const fnText = getSnippet(lines, lineStart || 1, lineEnd || lineStart || 1, 200); + for (const modelName of modelIdentifiers) { + const modelRegex = new RegExp(`\\b${modelName}\\b`); + if (modelRegex.test(fnText)) usedModels.add(modelName); + } + if (/\baxios\b/.test(fnText)) calledServices.add("axios"); + + functionMap.set(name, { + name, + lineStart, + lineEnd, + snippetPreview, + usedModels: Array.from(usedModels), + calledServices: Array.from(calledServices) + }); + }; + + traverseFn(ast, { + ExportNamedDeclaration(p) { + const decl = p.node.declaration; + if (!decl) return; + if (decl.type === "FunctionDeclaration" && decl.id?.name) { + registerFn(decl.id.name, decl); + } + if (decl.type === "VariableDeclaration") { + for (const d of decl.declarations || []) { + if (d.id?.type === "Identifier" && d.init && (d.init.type === "ArrowFunctionExpression" || d.init.type === "FunctionExpression")) { + registerFn(d.id.name, d.init); + } + } + } + }, + FunctionDeclaration(p) { + if (p.node.id?.name && !functionMap.has(p.node.id.name)) { + registerFn(p.node.id.name, p.node); + } + }, + VariableDeclarator(p) { + if (p.node.id?.type !== "Identifier" || functionMap.has(p.node.id.name)) return; + if (p.node.init && (p.node.init.type === "ArrowFunctionExpression" || p.node.init.type === "FunctionExpression")) { + registerFn(p.node.id.name, p.node.init); + } + } + }); + + return { functions: functionMap }; +}; + +const buildDataFlowGraph = (repoDir) => { + let allRoutes = []; + try { + allRoutes = parseGlobalDataFlow(repoDir); + } catch (e) { + console.error(e); + } + + if (allRoutes.length === 0) { + return { + nodes: [], + edges: [], + flows: [], + routes: [], + stats: { totalRoutes: 0, totalControllers: 0, totalModels: 0, source: "local", note: "No API routes detected globally via AST." } + }; + } + + const nodes = []; + const edges = []; + const flows = []; + const routesMeta = []; + const nodeIds = new Set(); + const edgeIds = new Set(); + + const pushNode = (node) => { + if (!node || !node.id || nodeIds.has(node.id)) return; + nodeIds.add(node.id); + nodes.push(node); + }; + + const pushEdge = (edge) => { + if (!edge || !edge.id || edgeIds.has(edge.id)) return; + edgeIds.add(edge.id); + edges.push(edge); + }; + + const clientNodeId = "client:external"; + pushNode(makeNode({ + id: clientNodeId, + label: "Client", + kind: "module", + docs: makeDocs({ summary: "Entry point for incoming requests", filePath: "external" }) + })); + + const controllerCache = new Map(); + const modelNodeIds = new Set(); + + for (const routeDef of allRoutes) { + const routeNodeId = `route:${routeDef.method}:${routeDef.path}`; + pushNode(makeNode({ + id: routeNodeId, + label: `${routeDef.method} ${routeDef.path}`, + kind: "file", + docs: makeDocs({ + summary: `Route defined in ${safeRepoRelative(repoDir, routeDef.routeFile) || routeDef.routeFile}`, + filePath: safeRepoRelative(repoDir, routeDef.routeFile) || routeDef.routeFile + }) + })); + pushEdge(makeEdge({ source: clientNodeId, target: routeNodeId, relation: "calls" })); + + const flowPath = [clientNodeId, routeNodeId]; + const controllerNodeIds = []; + + for (const handler of routeDef.handlers) { + if (!handler.controllerFile || handler.name === "inline_handler") continue; + + if (!controllerCache.has(handler.controllerFile)) { + controllerCache.set(handler.controllerFile, parseControllerDetails(handler.controllerFile)); + } + const details = controllerCache.get(handler.controllerFile); + const fn = details.functions.get(handler.name); + + const relController = safeRepoRelative(repoDir, handler.controllerFile) || handler.controllerFile; + const controllerNodeId = `controller:${relController}:${handler.name}`; + controllerNodeIds.push(controllerNodeId); + + pushNode(makeNode({ + id: controllerNodeId, + label: handler.name, + kind: "symbol", + docs: makeDocs({ + summary: `Controller handler in ${path.basename(relController)}`, + filePath: relController, + absolutePath: handler.controllerFile, + lineStart: fn?.lineStart || null, + lineEnd: fn?.lineEnd || null, + snippetPreview: fn?.snippetPreview || "" + }) + })); + pushEdge(makeEdge({ source: routeNodeId, target: controllerNodeId, relation: "calls" })); + + if (!flowPath.includes(controllerNodeId)) flowPath.push(controllerNodeId); + + const usedModels = Array.isArray(fn?.usedModels) ? fn.usedModels : []; + for (const modelName of usedModels) { + const modelNodeId = `model:${modelName}`; + modelNodeIds.add(modelNodeId); + pushNode(makeNode({ + id: modelNodeId, + label: modelName, + kind: "module", + docs: makeDocs({ summary: "Model used by controller", filePath: "models" }) + })); + pushEdge(makeEdge({ source: controllerNodeId, target: modelNodeId, relation: "imports" })); + if (!flowPath.includes(modelNodeId)) flowPath.push(modelNodeId); + } + + if (Array.isArray(fn?.calledServices) && fn.calledServices.includes("axios")) { + const serviceNodeId = "service:external-api"; + pushNode(makeNode({ + id: serviceNodeId, + label: "External API", + kind: "module", + docs: makeDocs({ summary: "Outbound HTTP dependency" }) + })); + pushEdge(makeEdge({ source: controllerNodeId, target: serviceNodeId, relation: "extends" })); + if (!flowPath.includes(serviceNodeId)) flowPath.push(serviceNodeId); + } + } + + const flowId = `flow:${routeDef.method}:${routeDef.path}`; + flows.push({ + id: flowId, + label: `${routeDef.method} ${routeDef.path}`, + nodePath: flowPath + }); + + routesMeta.push({ + id: flowId, + method: routeDef.method, + path: routeDef.path, + routeNodeId, + controllers: controllerNodeIds + }); + } + + return { + nodes, + edges, + flows, + routes: routesMeta, + stats: { + source: "local", + totalRoutes: routesMeta.length, + totalControllers: controllerCache.size, + totalModels: modelNodeIds.size, + graphNodes: nodes.length, + graphEdges: edges.length + } + }; +}; + +export const getDataFlowGraph = async (req, res) => { + try { + const { owner, repo, repoName } = req.query; + const targetRepo = repo || repoName; + const user = await User.findById(req.user._id); + + if (!user || !owner || !targetRepo) { + return res.status(400).json({ error: "Missing parameters" }); + } + + const basePath = user?.repoBasePath || DEFAULT_REPO_BASE_PATH; + const repoDir = getRepoDir(basePath, String(owner), String(targetRepo)); + if (!fs.existsSync(repoDir)) { + return res.status(404).json({ error: "Repository not found locally. Pull repo first." }); + } + + return res.json(buildDataFlowGraph(repoDir)); + } catch (error) { + console.error("Data Flow Viz Error:", error.message); + return res.status(500).json({ error: "Failed to build data flow graph: " + error.message }); + } +}; + +export const testDataFlowApi = async (req, res) => { + try { + const { + baseUrl = "http://localhost:5000", + method = "GET", + path: routePath = "/", + headers = {}, + query = {}, + body = null + } = req.body || {}; + + const m = String(method || "GET").toUpperCase(); + if (!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"].includes(m)) { + return res.status(400).json({ error: "Unsupported method" }); + } + + const normalizedBase = String(baseUrl || "").replace(/\/+$/, ""); + const normalizedPath = String(routePath || "/").startsWith("/") ? String(routePath) : `/${routePath}`; + const url = `${normalizedBase}${normalizedPath}`; + + const start = Date.now(); + const response = await axios.request({ + url, + method: m, + headers, + params: query, + data: body, + validateStatus: () => true + }); + const durationMs = Date.now() - start; + + return res.json({ + ok: response.status >= 200 && response.status < 300, + status: response.status, + durationMs, + headers: response.headers, + data: response.data + }); + } catch (error) { + const status = error?.response?.status || 500; + return res.status(status).json({ + ok: false, + status, + error: error?.response?.data || error.message + }); + } +}; diff --git a/backend/src/index.js b/backend/src/index.js index a456492..54c4d9c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -9,11 +9,13 @@ import fs from "fs"; import { connectDB } from "./lib/db.js"; import { Server } from "socket.io"; -import { Message } from "./models/message.model.js"; // 👈 CRITICAL IMPORT +import { Message } from "./models/message.model.js"; import authRoutes from "./routes/auth.route.js"; import repoRoutes from "./routes/repo.route.js"; import inviteRoutes from "./routes/invite.route.js"; +import userRoutes from "./routes/user.route.js"; +import hackathonRoutes from "./routes/hackathon.route.js"; dotenv.config(); @@ -28,6 +30,9 @@ const io = new Server(server, { }, }); +const presenceByRepo = new Map(); +const socketMeta = new Map(); + // File Upload Setup const uploadDir = path.join(process.cwd(), "src/uploads"); if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true }); @@ -39,18 +44,24 @@ const storage = multer.diskStorage({ const upload = multer({ storage }); // Middleware -app.use(express.json()); +app.use(cors({ origin: ["http://localhost:5173", "http://127.0.0.1:5173"], credentials: true })); // ??$$$ — Move CORS to top and include both local origins +app.use(express.json({ limit: "10mb" })); // ??$$$ — Increase limit for large blueprints/drafts app.use(cookieParser()); -app.use(cors({ origin: "http://localhost:5173", credentials: true })); app.use("/uploads", express.static(uploadDir)); // Routes app.use("/api/auth", authRoutes); app.use("/api/repos", repoRoutes); app.use("/api/invites", inviteRoutes); +app.use("/api/user", userRoutes); +app.use("/api/hackathon", hackathonRoutes); app.post("/api/upload", upload.single("file"), (req, res) => { if (!req.file) return res.status(400).json({ error: "No file uploaded" }); + const filename = req.file.originalname.toLowerCase(); + if (filename.endsWith(".exe") || filename.endsWith(".bat") || filename.endsWith(".cmd")) { + return res.status(400).json({ error: "File type not allowed" }); + } const fileUrl = `http://localhost:5000/uploads/${req.file.filename}`; res.json({ url: fileUrl, type: req.file.mimetype }); }); @@ -59,8 +70,48 @@ app.post("/api/upload", upload.single("file"), (req, res) => { io.on("connection", (socket) => { console.log("User connected:", socket.id); - socket.on("joinRepo", (repoId) => { + socket.on("joinRepo", ({ repoId, username }) => { + if (!repoId || !username) return; + + const existing = socketMeta.get(socket.id); + if (existing && existing.repoId && existing.repoId !== repoId) { + socket.leave(existing.repoId); + const repoMap = presenceByRepo.get(existing.repoId); + if (repoMap && repoMap.has(existing.username)) { + const entry = repoMap.get(existing.username); + entry.connections = Math.max(0, (entry.connections || 1) - 1); + if (entry.connections === 0) { + entry.status = "offline"; + entry.lastSeen = new Date().toISOString(); + io.to(existing.repoId).emit("presence_delta", { username: existing.username, status: entry.status, lastSeen: entry.lastSeen }); + } + } + } + socket.join(repoId); + socketMeta.set(socket.id, { repoId, username }); + + if (!presenceByRepo.has(repoId)) presenceByRepo.set(repoId, new Map()); + const repoMap = presenceByRepo.get(repoId); + const now = new Date().toISOString(); + + if (!repoMap.has(username)) { + repoMap.set(username, { status: "online", lastSeen: now, connections: 1 }); + } else { + const entry = repoMap.get(username); + entry.status = "online"; + entry.lastSeen = now; + entry.connections = (entry.connections || 0) + 1; + } + + const roster = Array.from(repoMap.entries()).map(([u, meta]) => ({ + username: u, + status: meta.status, + lastSeen: meta.lastSeen + })); + + socket.emit("presence_state", roster); + io.to(repoId).emit("presence_delta", { username, status: "online", lastSeen: now }); }); socket.on("typing", ({ repoId, username }) => { @@ -71,7 +122,7 @@ io.on("connection", (socket) => { socket.to(repoId).emit("userStoppedTyping", username); }); - socket.on("sendMessage", async ({ repoId, text, sender, replyTo, type }) => { + socket.on("sendMessage", async ({ repoId, text, sender, replyTo, type, codeSelection }) => { try { const newMessage = await Message.create({ repoId, @@ -79,7 +130,8 @@ io.on("connection", (socket) => { sender, replyTo, type: type || 'text', - status: 'delivered' + status: 'delivered', + codeSelection }); io.to(repoId).emit("receiveMessage", newMessage); } catch (error) { @@ -87,6 +139,24 @@ io.on("connection", (socket) => { } }); + socket.on("read_message", async ({ messageId, repoId, username }) => { + try { + if (!messageId || !repoId || !username) return; + const msg = await Message.findById(messageId); + if (!msg) return; + + const alreadyRead = msg.readBy?.some(r => r.username === username); + if (alreadyRead) return; + + msg.readBy.push({ username, at: new Date() }); + msg.status = "read"; + const updatedMsg = await msg.save(); + io.to(repoId).emit("messageUpdated", updatedMsg); + } catch (e) { + console.error("Read Receipt Error:", e.message); + } + }); + // 👇 FIXED: Message Actions (Delete/Edit/React) socket.on("messageAction", async ({ action, messageId, repoId, payload }) => { console.log(`Action Received: ${action} for msg ${messageId}`); // DEBUG LOG @@ -119,10 +189,26 @@ io.on("connection", (socket) => { } }); - socket.on("disconnect", () => console.log("User disconnected", socket.id)); + socket.on("disconnect", () => { + const meta = socketMeta.get(socket.id); + if (meta) { + const repoMap = presenceByRepo.get(meta.repoId); + if (repoMap && repoMap.has(meta.username)) { + const entry = repoMap.get(meta.username); + entry.connections = Math.max(0, (entry.connections || 1) - 1); + if (entry.connections === 0) { + entry.status = "offline"; + entry.lastSeen = new Date().toISOString(); + io.to(meta.repoId).emit("presence_delta", { username: meta.username, status: entry.status, lastSeen: entry.lastSeen }); + } + } + } + socketMeta.delete(socket.id); + console.log("User disconnected", socket.id); + }); }); server.listen(process.env.PORT || 5000, () => { console.log(`Server running on port ${process.env.PORT || 5000}`); connectDB(); -}); \ No newline at end of file +}); diff --git a/backend/src/models/ideation.model.js b/backend/src/models/ideation.model.js new file mode 100644 index 0000000..1b56cab --- /dev/null +++ b/backend/src/models/ideation.model.js @@ -0,0 +1,56 @@ +import mongoose from "mongoose"; + +const ideationSchema = new mongoose.Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + repoName: { + type: String, + default: "latest", // "latest" for the global resume, or the actual repo name + }, + messages: { + type: Array, + default: [], + }, + blueprint: { + type: Object, + default: {}, + }, + nodes: { + type: Array, + default: [], + }, + edges: { + type: Array, + default: [], + }, + teamSize: { + type: Number, + default: 2, + }, + hackHours: { + type: Number, + default: 24, + }, + // ??$$$ — Builder data + fileDrafts: { + type: Object, + default: {}, + }, + uiPreview: { + type: String, + default: "", + }, + updatedAt: { + type: Date, + default: Date.now, + } +}, { timestamps: true }); + +// Create a unique index for (userId, repoName) so we can easily find/update specific sessions +ideationSchema.index({ userId: 1, repoName: 1 }, { unique: true }); + +const Ideation = mongoose.model("Ideation", ideationSchema); +export default Ideation; diff --git a/backend/src/models/invite.model.js b/backend/src/models/invite.model.js index d25030d..e830a7c 100644 --- a/backend/src/models/invite.model.js +++ b/backend/src/models/invite.model.js @@ -6,7 +6,8 @@ const inviteSchema = new mongoose.Schema({ repoId: { type: String, required: true }, repoName: { type: String, required: true }, repoOwner: { type: String, required: true }, + inviteToken: { type: String, required: true, unique: true }, status: { type: String, enum: ["pending", "accepted", "rejected"], default: "pending" } }, { timestamps: true }); -export const Invite = mongoose.model("Invite", inviteSchema); \ No newline at end of file +export const Invite = mongoose.model("Invite", inviteSchema); diff --git a/backend/src/models/message.model.js b/backend/src/models/message.model.js index fce620d..0c810b1 100644 --- a/backend/src/models/message.model.js +++ b/backend/src/models/message.model.js @@ -6,7 +6,7 @@ const messageSchema = new mongoose.Schema({ text: { type: String, required: true }, // 👇 NEW: WhatsApp Features - type: { type: String, enum: ['text', 'image', 'system'], default: 'text' }, + type: { type: String, enum: ['text', 'image', 'file', 'system'], default: 'text' }, status: { type: String, enum: ['sent', 'delivered', 'read'], default: 'sent' }, replyTo: { id: { type: String }, @@ -18,7 +18,16 @@ const messageSchema = new mongoose.Schema({ user: String }], isEdited: { type: Boolean, default: false }, - readBy: [{ type: String }] // Array of usernames who read it + readBy: [{ + username: String, + at: Date + }], + codeSelection: { + filePath: String, + lineStart: Number, + lineEnd: Number, + code: String + } }, { timestamps: true }); -export const Message = mongoose.model("Message", messageSchema); \ No newline at end of file +export const Message = mongoose.model("Message", messageSchema); diff --git a/backend/src/models/projectCache.model.js b/backend/src/models/projectCache.model.js new file mode 100644 index 0000000..a71a353 --- /dev/null +++ b/backend/src/models/projectCache.model.js @@ -0,0 +1,43 @@ +import mongoose from "mongoose"; + +/** + * PROJECT CACHE MODEL + * Purpose: This acts as a "Library" of already-solves problems. + * Scaling logic: Instead of hitting Gemini ($$$) for every user, + * we check if someone else has already built something similar. + */ +const projectCacheSchema = new mongoose.Schema({ + // A simplified version of the project idea (e.g. "mern-ecommerce-realtime") + ideaFingerprint: { + type: String, + required: true, + unique: true, + index: true + }, + // The original raw prompt that triggered this cache entry + originalPrompt: String, + // The high-quality "God Prompt" output we want to reuse + generatedCodeMe: { + type: String, + required: true + }, + // The tech stack tags that go with this blueprint + techStack: [String], + // The proposed folder structure + folderStructure: String, + // Statistical tracking + usageCount: { + type: Number, + default: 1 + }, + lastUsed: { + type: Date, + default: Date.now + } +}, { timestamps: true }); + +// Optimize for fast lookup +projectCacheSchema.index({ ideaFingerprint: 1 }); + +const ProjectCache = mongoose.model("ProjectCache", projectCacheSchema); +export default ProjectCache; diff --git a/backend/src/models/user.model.js b/backend/src/models/user.model.js index a358fc9..da4c418 100644 --- a/backend/src/models/user.model.js +++ b/backend/src/models/user.model.js @@ -1,19 +1,26 @@ import mongoose from "mongoose"; -const userSchema = new mongoose.Schema({ - githubId: { type: String, required: true, unique: true }, +const userSchema = new mongoose.Schema( + { + githubId: { type: String, unique: true, sparse: true }, username: { type: String, required: true }, + status: { type: String, enum: ["active", "shadow"], default: "active" }, + repoBasePath: { type: String }, email: { type: String }, avatarUrl: { type: String }, githubToken: { type: String }, - + // 👇 NEW: Store repos shared by others - sharedRepos: [{ + sharedRepos: [ + { repoId: String, name: String, owner: String, - description: String - }] -}, { timestamps: true }); + description: String, + }, + ], + }, + { timestamps: true }, +); -export const User = mongoose.model("User", userSchema); \ No newline at end of file +export const User = mongoose.model("User", userSchema); diff --git a/backend/src/routes/hackathon.route.js b/backend/src/routes/hackathon.route.js new file mode 100644 index 0000000..eca639e --- /dev/null +++ b/backend/src/routes/hackathon.route.js @@ -0,0 +1,30 @@ +import express from "express"; +import { + chatWithAi, + compileAndPushRepo, + getScorecard, + generateCodeMe, + saveIdeationSession, + getIdeationSession, + clearIdeationSession, + draftFileCode, + generateUiPreview +} from "../controllers/hackathon.controller.js"; +import { protectRoute } from "../middleware/protectRoute.js"; + +const router = express.Router(); + +router.post("/chat", protectRoute, chatWithAi); +router.post("/compile-and-push", protectRoute, compileAndPushRepo); +router.post("/scorecard", protectRoute, getScorecard); +router.post("/generate-codeme", protectRoute, generateCodeMe); +router.post("/draft-file", protectRoute, draftFileCode); +router.post("/generate-ui-preview", protectRoute, generateUiPreview); + +// ??$$$ — Session management +router.post("/session", protectRoute, saveIdeationSession); +router.get("/session/:repoName", protectRoute, getIdeationSession); +router.delete("/session/:repoName", protectRoute, clearIdeationSession); + +export default router; + diff --git a/backend/src/routes/invite.route.js b/backend/src/routes/invite.route.js index 568a8e4..eefed8d 100644 --- a/backend/src/routes/invite.route.js +++ b/backend/src/routes/invite.route.js @@ -1,11 +1,25 @@ import express from "express"; import { protectRoute } from "../middleware/protectRoute.js"; -import { sendInvite, getMyInvites, acceptInvite } from "../controllers/invite.controller.js"; +import { + sendInvite, + getMyInvites, + getInviteByToken, + acceptInvite, + acceptInviteByToken, + searchGithubUsers +} from "../controllers/invite.controller.js"; const router = express.Router(); router.post("/send", protectRoute, sendInvite); +router.get("/search-users", protectRoute, searchGithubUsers); router.get("/mine", protectRoute, getMyInvites); + +// Invite link flow +router.get("/token/:token", protectRoute, getInviteByToken); +router.get("/accept/:token", protectRoute, acceptInviteByToken); + +// In-app invite accept router.post("/accept", protectRoute, acceptInvite); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/repo.route.js b/backend/src/routes/repo.route.js index 8afb4a9..1f278b2 100644 --- a/backend/src/routes/repo.route.js +++ b/backend/src/routes/repo.route.js @@ -1,13 +1,43 @@ import express from "express"; -import { getRepos, getRepoMessages, getRepoFiles, getFileContent } from "../controllers/repo.controller.js"; +import { + getRepos, + getRepoMessages, + getRepoFiles, + getFileContent, + getCodeAnchor, + pullRepo, + pushRepo, + getLocalFilePath, + getLocalFileContent, + updateLocalFileContent, + createRepo +} from "../controllers/repo.controller.js"; import { protectRoute } from "../middleware/protectRoute.js"; +import { + getAppStructure, + getDependencyGraph, + getDataFlowGraph, + testDataFlowApi +} from "../controllers/visualizer.controller.js"; const router = express.Router(); router.get("/", protectRoute, getRepos); +router.post("/create", protectRoute, createRepo); router.get("/:repoId/messages", protectRoute, getRepoMessages); router.get("/files", protectRoute, getRepoFiles); router.get("/content", protectRoute, getFileContent); +router.get("/local-content", protectRoute, getLocalFileContent); +router.put("/local-content", protectRoute, updateLocalFileContent); +router.post("/code-anchor", protectRoute, getCodeAnchor); +router.post("/pull", protectRoute, pullRepo); +router.post("/push", protectRoute, pushRepo); +router.get("/local-path", protectRoute, getLocalFilePath); -export default router; \ No newline at end of file +router.get("/visualize/structure", protectRoute, getAppStructure); +router.get("/visualize/dependencies", protectRoute, getDependencyGraph); +router.get("/visualize/data-flow", protectRoute, getDataFlowGraph); +router.post("/visualize/data-flow/test", protectRoute, testDataFlowApi); + +export default router; diff --git a/backend/src/routes/user.route.js b/backend/src/routes/user.route.js new file mode 100644 index 0000000..d4bebb9 --- /dev/null +++ b/backend/src/routes/user.route.js @@ -0,0 +1,9 @@ +import express from "express"; +import { protectRoute } from "../middleware/protectRoute.js"; +import { setRepoBasePath } from "../controllers/user.controller.js"; + +const router = express.Router(); + +router.post("/repo-path", protectRoute, setRepoBasePath); + +export default router; diff --git a/backend/src/services/astParser.service.js b/backend/src/services/astParser.service.js new file mode 100644 index 0000000..1790d28 --- /dev/null +++ b/backend/src/services/astParser.service.js @@ -0,0 +1,303 @@ +import fs from "fs"; +import path from "path"; +import { createRequire } from "module"; +import { parse } from "@babel/parser"; +import traverseModule from "@babel/traverse"; + +const traverseFn = traverseModule?.default || traverseModule; +const require = createRequire(import.meta.url); + +let Parser = null; +const grammars = {}; + +try { + Parser = require("tree-sitter"); + try { grammars["python"] = require("tree-sitter-python"); } catch(e){} + try { grammars["java"] = require("tree-sitter-java"); } catch(e){} + try { grammars["go"] = require("tree-sitter-go"); } catch(e){} +} catch(e) { + console.warn("Tree-sitter not fully available:", e.message); +} + +const getSnippet = (lines, lineStart, lineEnd, maxLines) => { + if (!Array.isArray(lines) || lines.length === 0) return ""; + const start = Math.max(1, Number(lineStart) || 1); + const rawEnd = Number(lineEnd) || start; + const end = Math.max(start, rawEnd); + const hardEnd = Math.min(lines.length, start + Math.max(1, maxLines) - 1, end); + return lines.slice(start - 1, hardEnd).join("\n").trim(); +}; + +const mapExtensionToLanguage = (ext) => { + switch (ext) { + case ".py": return "python"; + case ".java": return "java"; + case ".go": return "go"; + default: return null; + } +}; + +const analyzeWithTreeSitter = (content, ext, snippetLines) => { + const langKey = mapExtensionToLanguage(ext); + if (!Parser || !langKey || !grammars[langKey]) return null; + + const imports = []; + const exports = []; + const symbols = []; + const calls = []; + const extendsRels = []; + const lines = content.split(/\r?\n/); + + const addSymbol = (name, symbolType, startLine, endLine) => { + if (!name) return null; + const symbol = { + key: `${name}:${startLine}:${symbols.length}`, + name, + symbolType, + lineStart: startLine + 1, + lineEnd: endLine + 1, + snippetPreview: getSnippet(lines, startLine + 1, endLine + 1, snippetLines) + }; + symbols.push(symbol); + return symbol; + }; + + try { + const parser = new Parser(); + parser.setLanguage(grammars[langKey]); + const tree = parser.parse(content); + + const walk = (node, symbolStack) => { + const currentStack = [...symbolStack]; + + // Python Specifics + if (langKey === "python") { + if (node.type === "import_statement" || node.type === "import_from_statement") { + const text = node.text; + const match = text.match(/(?:from|import)\s+([a-zA-Z0-9_\.]+)/); + if (match && match[1]) imports.push(match[1]); + } + if (node.type === "function_definition") { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + const sym = addSymbol(nameNode.text, "function", node.startPosition.row, node.endPosition.row); + if (sym) currentStack.push(sym.key); + } + } + if (node.type === "class_definition") { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + const sym = addSymbol(nameNode.text, "class", node.startPosition.row, node.endPosition.row); + if (sym) currentStack.push(sym.key); + + // Check superclasses + const superclassesNode = node.childForFieldName("superclasses"); + if (superclassesNode) { + const extendsArgs = superclassesNode.namedChildren; + for (const ext of extendsArgs) { + extendsRels.push({ fromSymbolKey: sym.key, toName: ext.text }); + } + } + } + } + if (node.type === "call") { + const funcNode = node.childForFieldName("function"); + if (funcNode) { + let calleeName = funcNode.text; + if (funcNode.type === "attribute") { + calleeName = funcNode.childForFieldName("attribute")?.text || calleeName; + } + const fromSymbolKey = currentStack.length ? currentStack[currentStack.length - 1] : null; + calls.push({ fromSymbolKey, toName: calleeName }); + } + } + } + + // Java Specifics + if (langKey === "java") { + if (node.type === "import_declaration") { + const text = node.text; + const match = text.match(/import\s+(static\s+)?([\w\.]+);/); + if (match && match[2]) imports.push(match[2]); + } + if (node.type === "method_declaration") { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + const sym = addSymbol(nameNode.text, "method", node.startPosition.row, node.endPosition.row); + if (sym) currentStack.push(sym.key); + } + } + if (node.type === "class_declaration" || node.type === "interface_declaration") { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + const sym = addSymbol(nameNode.text, node.type === "class_declaration" ? "class" : "interface", node.startPosition.row, node.endPosition.row); + if (sym) currentStack.push(sym.key); + + const superclassNode = node.childForFieldName("superclass"); + if (superclassNode) { + const typeNode = superclassNode.childForFieldName("type"); + if (typeNode) extendsRels.push({ fromSymbolKey: sym.key, toName: typeNode.text }); + } + } + } + if (node.type === "method_invocation" || node.type === "object_creation_expression") { + const nameNode = node.childForFieldName("name") || node.childForFieldName("type"); + if (nameNode) { + const fromSymbolKey = currentStack.length ? currentStack[currentStack.length - 1] : null; + calls.push({ fromSymbolKey, toName: nameNode.text }); + } + } + } + + // Go Specifics + if (langKey === "go") { + if (node.type === "import_spec") { + const pathNode = node.childForFieldName("path"); + if (pathNode) imports.push(pathNode.text.replace(/"/g, "")); + } + if (node.type === "function_declaration" || node.type === "method_declaration") { + const nameNode = node.childForFieldName("name"); + if (nameNode) { + const sym = addSymbol(nameNode.text, "function", node.startPosition.row, node.endPosition.row); + if (sym) currentStack.push(sym.key); + } + } + if (node.type === "type_spec") { + const nameNode = node.childForFieldName("name"); + const typeNode = node.childForFieldName("type"); + if (nameNode && typeNode) { + const typeType = typeNode.type === "struct_type" ? "struct" : (typeNode.type === "interface_type" ? "interface" : "type"); + addSymbol(nameNode.text, typeType, node.startPosition.row, node.endPosition.row); + } + } + if (node.type === "call_expression") { + const funcNode = node.childForFieldName("function"); + if (funcNode) { + let calleeName = funcNode.text; + if (funcNode.type === "selector_expression") { + calleeName = funcNode.childForFieldName("field")?.text || calleeName; + } + const fromSymbolKey = currentStack.length ? currentStack[currentStack.length - 1] : null; + calls.push({ fromSymbolKey, toName: calleeName }); + } + } + } + + for (const child of node.namedChildren) { + walk(child, currentStack); + } + }; + + walk(tree.rootNode, []); + + return { + imports: [...new Set(imports)], + exports: [...new Set(exports)], + symbols, + calls, + extendsRels, + parseOk: true + }; + + } catch (e) { + console.error("Tree-sitter parse failed", e); + return null; // fallback to regex + } +}; + +const analyzeWithRegex = (content, ext, snippetLines) => { + const lines = content.split(/\r?\n/); + const imports = []; + const symbols = []; + const calls = []; + const extendsRels = []; + let currentSymbolKey = null; + + const addSymbol = (name, symbolType, lineIdx) => { + const symbol = { + key: `${name}:${lineIdx + 1}:${symbols.length}`, + name, + symbolType, + lineStart: lineIdx + 1, + lineEnd: lineIdx + 1, + snippetPreview: getSnippet(lines, lineIdx + 1, lineIdx + Math.max(2, snippetLines), snippetLines) + }; + symbols.push(symbol); + currentSymbolKey = symbol.key; + return symbol; + }; + + lines.forEach((line, i) => { + const trimmed = line.trim(); + + // Imports + if (ext === ".py") { + let m = trimmed.match(/^(?:from\s+([a-zA-Z0-9_\.]+)\s+import|import\s+([a-zA-Z0-9_\.]+))/); + if (m) imports.push(m[1] || m[2]); + + m = trimmed.match(/^def\s+([a-zA-Z0-9_]+)\s*\(/); + if (m) addSymbol(m[1], "function", i); + + m = trimmed.match(/^class\s+([a-zA-Z0-9_]+)(?:\(([^)]+)\))?\s*:/); + if (m) { + const sym = addSymbol(m[1], "class", i); + if (m[2]) extendsRels.push({ fromSymbolKey: sym.key, toName: m[2].trim() }); + } + } + else if (ext === ".java" || ext === ".cs") { + let m = trimmed.match(/^(?:import|using)\s+([\w\.]+)\s*;/); + if (m) imports.push(m[1]); + + m = trimmed.match(/(?:public|private|protected|static|final|virtual|override|\s)*\s+(class|interface)\s+([a-zA-Z0-9_]+)\s*(?:extends|implements|:)\s*([a-zA-Z0-9_]+)/); + if (m) { + const sym = addSymbol(m[2], m[1], i); + extendsRels.push({ fromSymbolKey: sym.key, toName: m[3].trim() }); + } else { + m = trimmed.match(/(?:public|private|protected|static|final|virtual|override|\s)*\s+(class|interface)\s+([a-zA-Z0-9_]+)/); + if (m) addSymbol(m[2], m[1], i); + } + + m = trimmed.match(/(?:public|private|protected|static|final|virtual|override|\s)+\s+[\w<>\[\]]+\s+([a-zA-Z0-9_]+)\s*\(/); + if (m && !trimmed.includes("=")) addSymbol(m[1], "method", i); + } + else if (ext === ".go") { + let m = trimmed.match(/^import\s+"([^"]+)"/); + if (m) imports.push(m[1]); + + m = trimmed.match(/^func\s+(?:\([^)]+\)\s+)?([a-zA-Z0-9_]+)\s*\(/); + if (m) addSymbol(m[1], "function", i); + + m = trimmed.match(/^type\s+([a-zA-Z0-9_]+)\s+(struct|interface)/); + if (m) addSymbol(m[1], m[2], i); + } + + // Basic call heuristic (only counts if we are inside a symbol) + if (currentSymbolKey) { + const matches = trimmed.matchAll(/([a-zA-Z0-9_]+)\s*\(/g); + for (const match of matches) { + const callee = match[1]; + if (!["if", "for", "while", "switch", "catch", "return", "function", "def", "func"].includes(callee)) { + calls.push({ fromSymbolKey: currentSymbolKey, toName: callee }); + } + } + } + }); + + return { + imports: [...new Set(imports)], + exports: [], + symbols, + calls, + extendsRels, + parseOk: true + }; +}; + +export const analyzeGenericFile = (content, ext, snippetLines) => { + // 1. Try Tree-Sitter + const tsResult = analyzeWithTreeSitter(content, ext, snippetLines); + if (tsResult) return tsResult; + + // 2. Fallback to Regex + return analyzeWithRegex(content, ext, snippetLines); +}; diff --git a/backend/src/services/cache.service.js b/backend/src/services/cache.service.js new file mode 100644 index 0000000..d806d4a --- /dev/null +++ b/backend/src/services/cache.service.js @@ -0,0 +1,72 @@ +import ProjectCache from "../models/projectCache.model.js"; + +/** + * SEMANTIC CACHE SERVICE + * + * WHY THIS IS BETTER FOR 10,000 USERS: + * 1. AI tokens are expensive. + * 2. 90% of hackathon users build similar things (Fitness app, AI Chat, Crypto Tracker). + * 3. By "fingerprinting" the idea, we can serve a high-quality result instantly. + */ + +/** + * Generates a stable key based on core project traits. + * Example: "I want to build a real-time chat app in React and Node" + * might become "node-react-realtime-chat" + */ +export const generateFingerprint = (messages) => { + // Collect all user messages + const userText = messages + .filter(m => m.role === "user") + .map(m => m.content.toLowerCase()) + .join(" "); + + // Extract core keywords (a basic version of semantic extraction) + // In a production app, you'd use a small AI model (Gemini Flash) + // to return just these 3 words. + const keywords = [ + "mern", "react", "node", "python", "ai", "real-time", + "e-commerce", "dashboard", "mobile", "auth", "socket" + ]; + + const found = keywords + .filter(kw => userText.includes(kw)) + .sort() + .join("-"); + + // We combine the sorted keywords with a hash of the raw text for safety + return found || "generic-app"; +}; + +export const checkCache = async (fingerprint) => { + try { + const hit = await ProjectCache.findOne({ ideaFingerprint: fingerprint }); + if (hit) { + // Update stats + hit.usageCount += 1; + hit.lastUsed = Date.now(); + await hit.save(); + return hit; + } + return null; + } catch (e) { + console.error("Cache Lookup Error:", e); + return null; + } +}; + +export const saveToCache = async (fingerprint, data) => { + try { + await ProjectCache.findOneAndUpdate( + { ideaFingerprint: fingerprint }, + { + ...data, + ideaFingerprint: fingerprint, + lastUsed: Date.now() + }, + { upsert: true } + ); + } catch (e) { + console.error("Cache Save Error:", e); + } +}; diff --git a/backend/src/services/dataFlowParser.service.js b/backend/src/services/dataFlowParser.service.js new file mode 100644 index 0000000..d210ceb --- /dev/null +++ b/backend/src/services/dataFlowParser.service.js @@ -0,0 +1,135 @@ +import fs from "fs"; +import path from "path"; + +const safeReadUtf8 = (filePath, maxBytes = 2 * 1024 * 1024) => { + try { + const s = fs.statSync(filePath); + if (!s.isFile() || s.size > maxBytes) return null; + return fs.readFileSync(filePath, "utf-8"); + } catch { + return null; + } +}; + +export const parseGenericDataFlow = (repoDir, ext, filePaths) => { + const routes = []; + + for (const abs of filePaths) { + const content = safeReadUtf8(abs); + if (!content) continue; + + const lines = content.split(/\r?\n/); + + // Python (Flask/FastAPI/Django) + if (ext === ".py") { + let currentRoutePath = null; + let currentMethod = "GET"; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // FastAPI / Flask + let m = line.match(/^@(app|router|bp)\.(get|post|put|delete|patch|route)\(['"]([^'"]+)['"]/i); + if (m) { + currentMethod = m[2].toUpperCase() === "ROUTE" ? "GET" : m[2].toUpperCase(); + currentRoutePath = m[3]; + continue; + } + + // Django + let mDjango = line.match(/^path\(['"]([^'"]+)['"]/i); + if (mDjango) { + currentMethod = "ALL"; + currentRoutePath = mDjango[1]; + // attempt to find view name later on the same line + let viewMatch = line.match(/,\s*([\w\.]+)/); + if (viewMatch) { + routes.push({ + method: currentMethod, + path: currentRoutePath, + routeFile: abs, + handlers: [{ name: viewMatch[1], controllerFile: abs }] + }); + currentRoutePath = null; + continue; + } + } + + if (currentRoutePath && line.startsWith("def ")) { + const funcMatch = line.match(/^def\s+([a-zA-Z0-9_]+)/); + if (funcMatch) { + routes.push({ + method: currentMethod, + path: currentRoutePath, + routeFile: abs, + handlers: [{ name: funcMatch[1], controllerFile: abs }] + }); + currentRoutePath = null; + } + } + } + } + + // Java Spring Boot + if (ext === ".java") { + let classRoutePrefix = ""; + let currentRoutePath = null; + let currentMethod = "GET"; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + let mClass = line.match(/^@RequestMapping\(['"]([^'"]+)['"]/i); + if (mClass && !line.includes("method")) { + classRoutePrefix = mClass[1]; + } + + let mRoute = line.match(/^@(Get|Post|Put|Delete|Patch)Mapping\(['"]([^'"]+)['"]/i); + if (mRoute) { + currentMethod = mRoute[1].toUpperCase(); + currentRoutePath = mRoute[2]; + } + + if (currentRoutePath && line.match(/(?:public|private|protected)\s+.*?([a-zA-Z0-9_]+)\s*\(/)) { + const funcMatch = line.match(/(?:public|private|protected)\s+.*?([a-zA-Z0-9_]+)\s*\(/); + const finalPath = classRoutePrefix + (currentRoutePath.startsWith("/") ? currentRoutePath : "/" + currentRoutePath); + routes.push({ + method: currentMethod, + path: finalPath.replace(/\/\//g, "/"), + routeFile: abs, + handlers: [{ name: funcMatch[1], controllerFile: abs }] + }); + currentRoutePath = null; + } + } + } + + // Go (Gin/Mux/Chi) + if (ext === ".go") { + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + let m = line.match(/(GET|POST|PUT|DELETE|PATCH)\(['"]([^'"]+)['"],\s*([\w\.]+)/i); + if (m) { + routes.push({ + method: m[1].toUpperCase(), + path: m[2], + routeFile: abs, + handlers: [{ name: m[3], controllerFile: abs }] + }); + } else { + let mStd = line.match(/HandleFunc\(['"]([^'"]+)['"],\s*([\w\.]+)/i); + if (mStd) { + routes.push({ + method: "ALL", + path: mStd[1], + routeFile: abs, + handlers: [{ name: m[2], controllerFile: abs }] + }); + } + } + } + } + } + + return routes; +}; diff --git a/backend/src/uploads/1770136511340-notes1'.pdf b/backend/src/uploads/1770136511340-notes1'.pdf new file mode 100644 index 0000000..4440a9e Binary files /dev/null and b/backend/src/uploads/1770136511340-notes1'.pdf differ diff --git a/backend/src/uploads/1772796654089-WIN_20250910_22_51_30_Pro.jpg b/backend/src/uploads/1772796654089-WIN_20250910_22_51_30_Pro.jpg new file mode 100644 index 0000000..8ebf9d7 Binary files /dev/null and b/backend/src/uploads/1772796654089-WIN_20250910_22_51_30_Pro.jpg differ diff --git a/backend/src/uploads/1772796664854-graph delete.png b/backend/src/uploads/1772796664854-graph delete.png new file mode 100644 index 0000000..76fe8f4 Binary files /dev/null and b/backend/src/uploads/1772796664854-graph delete.png differ diff --git a/backend/src/uploads/1772796716137-Sketchfab_2024_04_05_23_03_13.fbx b/backend/src/uploads/1772796716137-Sketchfab_2024_04_05_23_03_13.fbx new file mode 100644 index 0000000..d484b61 Binary files /dev/null and b/backend/src/uploads/1772796716137-Sketchfab_2024_04_05_23_03_13.fbx differ diff --git a/codechat/LICENSE b/codechat/LICENSE new file mode 100644 index 0000000..0ed533c --- /dev/null +++ b/codechat/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2026 M K Subrahmanya +All rights reserved. + +PROPRIETARY LICENSE + +This software and all associated source code, object code, documentation, and other materials (the "Software") are proprietary. + +1. Limited permission to use +Subject to your compliance with this license, you are granted a limited, non-exclusive, non-transferable, revocable permission to use the Software for your own personal or internal business purposes. + +2. Restrictions +You may NOT, and you may not permit others to: +- copy, reproduce, modify, translate, adapt, or create derivative works of the Software; +- distribute, publish, sell, sublicense, rent, lease, lend, or otherwise make the Software available to any third party; +- reverse engineer, decompile, or disassemble the Software, except to the extent such restriction is prohibited by applicable law; +- remove, alter, or obscure any proprietary notices. + +3. No trademark rights +This license does not grant any rights to use the names, logos, or trademarks of the copyright holder. + +4. Termination +This license is effective until terminated. It terminates automatically if you breach any term of this license. Upon termination, you must stop using the Software and destroy all copies in your possession. + +5. Disclaimer +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + +6. Limitation of liability +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/codechat/README.md b/codechat/README.md index 5412652..d896c99 100644 --- a/codechat/README.md +++ b/codechat/README.md @@ -1,71 +1,3 @@ # codechat README -This is the README for your extension "codechat". After writing up a brief description, we recommend including the following sections. - -## Features - -Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - -For example if there is an image subfolder under your extension project workspace: - -\!\[feature X\]\(images/feature-x.png\) - -> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. - -## Requirements - -If you have any requirements or dependencies, add a section describing those and how to install and configure them. - -## Extension Settings - -Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. - -For example: - -This extension contributes the following settings: - -* `myExtension.enable`: Enable/disable this extension. -* `myExtension.thing`: Set to `blah` to do something. - -## Known Issues - -Calling out known issues can help limit users opening duplicate issues against your extension. - -## Release Notes - -Users appreciate release notes as you update your extension. - -### 1.0.0 - -Initial release of ... - -### 1.0.1 - -Fixed issue #. - -### 1.1.0 - -Added features X, Y, and Z. - ---- - -## Following extension guidelines - -Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. - -* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) - -## Working with Markdown - -You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: - -* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). -* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). -* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. - -## For more information - -* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) -* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) - -**Enjoy!** +CodeChat is a VSCode Extension that brings together Github and productivity with a chat based interface. diff --git a/codechat/package-lock.json b/codechat/package-lock.json index babc0df..8a1ea6f 100644 --- a/codechat/package-lock.json +++ b/codechat/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "codechat", "version": "0.0.1", + "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", diff --git a/codechat/package.json b/codechat/package.json index b69747b..227f59b 100644 --- a/codechat/package.json +++ b/codechat/package.json @@ -4,6 +4,7 @@ "description": "", "version": "0.0.1", "publisher": "MKSubrahmanya", + "license": "SEE LICENSE IN LICENSE", "engines": { "vscode": "^1.108.1" }, diff --git a/codechat/src/extension.ts b/codechat/src/extension.ts index 627784f..0a3bb72 100644 --- a/codechat/src/extension.ts +++ b/codechat/src/extension.ts @@ -24,6 +24,28 @@ export function activate(context: vscode.ExtensionContext) { } }) ); + + context.subscriptions.push( + vscode.window.onDidChangeTextEditorSelection((event) => { + const editor = event.textEditor; + if (!editor || !editor.document) return; + const selection = editor.selection; + if (selection.isEmpty) return; + + const text = editor.document.getText(selection); + const filePath = editor.document.uri.fsPath; + const lineStart = selection.start.line + 1; + const lineEnd = selection.end.line + 1; + + provider.sendMessageToWebview({ + command: "selectionChanged", + text, + filePath, + lineStart, + lineEnd + }); + }) + ); } class ChatViewProvider implements vscode.WebviewViewProvider { @@ -45,11 +67,28 @@ class ChatViewProvider implements vscode.WebviewViewProvider { webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); - webviewView.webview.onDidReceiveMessage(message => { + webviewView.webview.onDidReceiveMessage(async message => { switch (message.command) { case 'loginGithub': vscode.env.openExternal(vscode.Uri.parse(message.url)); return; + case 'openFile': { + if (!message.path) return; + const doc = await vscode.workspace.openTextDocument(message.path); + const editor = await vscode.window.showTextDocument(doc, { + preview: false, + viewColumn: vscode.ViewColumn.Beside + }); + if (message.lineStart) { + const resolvedEnd = message.lineEnd || message.lineStart; + const start = new vscode.Position(message.lineStart - 1, 0); + const end = new vscode.Position(resolvedEnd - 1, 0); + const range = new vscode.Range(start, end); + editor.selection = new vscode.Selection(start, end); + editor.revealRange(range, vscode.TextEditorRevealType.InCenter); + } + return; + } } }); } @@ -61,6 +100,12 @@ class ChatViewProvider implements vscode.WebviewViewProvider { } } + public sendMessageToWebview(message: any) { + if (this._view) { + this._view.webview.postMessage(message); + } + } + private _getHtmlForWebview(webview: vscode.Webview) { // ... (Keep your HTML exactly as it was in the previous step) ... // Ensure the + + + + + ${uiPreview} + + + `} + className="w-full h-full border-none" + /> + ) : ( +
+
+ +
+
+

See your app before it exists.

+

Click the button above to generate a full Tailwind CSS mockup of your dashboard.

+
+
+ )} + + + ); + } + + // --- HOSTING (CLOUD LAUNCHPAD) --- + if (activeTab === "hostingInstructions") { + const repoUrl = compileSuccess?.repoUrl; + + return ( +
+
+

Cloud Launchpad

+

Ship your project to production in one click.

+
+ + {/* Stage 1: Github Requirement */} + {!repoUrl && ( +
+ +

+ Action Required: You must push your repository to GitHub before you can deploy. +

+
+ )} + +
+ {/* Vercel Card */} +
+
+
+
+ +
+ Vercel +
+
Fastest
+
+

Best for Frontend & Static projects. Instant global Edge deployment.

+ + {repoUrl ? "Deploy to Vercel" : "Push to GitHub First"} + {repoUrl && } + +
+ + {/* Render Card */} +
+
+
+
+
+
+
+ Render +
+
Fullstack
+
+

Best for Node.js Backends & Databases. Auto-scaling included.

+ + {repoUrl ? "Deploy to Render" : "Push to GitHub First"} + {repoUrl && } + +
+
+ + {/* Custom Plan (Original AI Instructions) */} + {blueprint.hostingInstructions && ( +
+

AI Recommended Strategy

+
+ {blueprint.hostingInstructions.split(". ").filter(Boolean).map((line, i) => ( +
+ + {line.trim()}. +
+ ))} +
+
+ )} +
+ ); + } + + // --- CODEME PREVIEW (EDITABLE) --- + if (activeTab === "codeMePreview") { + return ( +
+
+

CodeME.md

+
+ {/* ??$$$ — Generate Full CodeME from backend God Prompt */} + + +
+
+ + {/* ??$$$ — Fully editable textarea */} +