commit 0835211a711b8d9ac6027fa4d1fc14290dd41410 Author: Zoƫ Date: Mon Apr 6 00:44:36 2026 +0200 feat: initial implementation of AI Commit Ext VS Code extension - Add VS Code extension with OpenCode AI integration - Generate Conventional Commit messages via SCM header button - Include gitService for staged changes detection - Include opencodeService for CLI invocation - Add configuration options for model, includeUnstaged, notifications - Add button disable during generation with progress indicator - Add sparkle icon for the action button - Include package.json, tsconfig, and basic project setup diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c1322dc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e5962e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out +node_modules \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a11e8b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,572 @@ +{ + "name": "ai-commit-ext", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-commit-ext", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^25.5.2", + "@types/vscode": "^1.110.0", + "typescript": "^6.0.2", + "vscode": "^1.1.37" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "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/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.x" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha512-z/GDPjlRMNOa2XJiB4em8wJpuuBfrFOlYKTZxtpkdr1uPdibHI8rYA3MY0KDObpVyaes0e/aunid/t88ZI2EKA==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode": { + "version": "1.1.37", + "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.37.tgz", + "integrity": "sha512-vJNj6IlN7IJPdMavlQa1KoFB3Ihn06q1AiN3ZFI/HfzPNzbKZWPPuiU+XkpNOfGU5k15m4r80nxNPlM7wcc0wg==", + "deprecated": "This package is deprecated in favor of @types/vscode and vscode-test. For more information please read: https://code.visualstudio.com/updates/v1_36#_splitting-vscode-package-into-typesvscode-and-vscodetest", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^7.1.2", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "mocha": "^5.2.0", + "semver": "^5.4.1", + "source-map-support": "^0.5.0", + "vscode-test": "^0.4.1" + }, + "bin": { + "vscode-install": "bin/install" + }, + "engines": { + "node": ">=8.9.3" + } + }, + "node_modules/vscode-test": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz", + "integrity": "sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w==", + "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.1" + }, + "engines": { + "node": ">=8.9.3" + } + }, + "node_modules/vscode-test/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/vscode-test/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/vscode-test/node_modules/http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "4", + "debug": "3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/vscode-test/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/vscode-test/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b0edd75 --- /dev/null +++ b/package.json @@ -0,0 +1,70 @@ +{ + "name": "ai-commit-ext", + "displayName": "AI Commit Ext", + "description": "Generate commit messages using OpenCode AI", + "version": "0.0.1", + "publisher": "local", + "engines": { + "vscode": "^1.71.0" + }, + "categories": [ + "Other" + ], + "main": "./out/extension", + "activationEvents": [ + "onCommand:aiCommitExt.generate" + ], + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "test": "echo \"No tests yet\"" + }, + "devDependencies": { + "@types/node": "^25.5.2", + "@types/vscode": "^1.110.0", + "typescript": "^6.0.2", + "vscode": "^1.1.37" + }, + "contributes": { + "configuration": { + "title": "AI Commit Ext", + "properties": { + "aiCommitExt.model": { + "type": "string", + "default": "", + "description": "OpenCode model to use (e.g. anthropic/claude-3-5-sonnet-20241022). Empty = default model." + }, + "aiCommitExt.includeUnstaged": { + "type": "boolean", + "default": false, + "description": "Use unstaged changes if no staged changes exist" + }, + "aiCommitExt.showNotification": { + "type": "boolean", + "default": true, + "description": "Show notification when commit message is generated" + } + } + }, + "commands": [ + { + "command": "aiCommitExt.generate", + "title": "Generate Commit Message", + "icon": "$(sparkle)", + "category": "AI Commit" + } + ], + "menus": { + "scm/title": [ + { + "when": "scmProvider == git", + "command": "aiCommitExt.generate", + "group": "navigation", + "icon": "$(sparkle)", + "alt": "aiCommitExt.generate" + } + ] + } + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..76fbc1c --- /dev/null +++ b/readme.md @@ -0,0 +1,33 @@ +# AI Commit Ext + +Generate commit messages using OpenCode AI. + +## Features + +- Generate Conventional Commit messages via OpenCode +- Button in SCM header +- Progress indicator while generating + +## Requirements + +- VS Code 1.71+ +- OpenCode CLI installed + +## Installation + +1. Clone this repository +2. npm install +3. Press F5 to test + +## Configuration + +- `aiCommitExt.model` - Model to use +- `aiCommitExt.includeUnstaged` - Use unstaged changes if nothing staged +- `aiCommitExt.showNotification` - Show notifications + +## Usage + +1. Open a Git repository in VS Code +2. Stage some changes +3. Click the sparkle icon in the SCM header +4. Commit message is generated and filled in diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..b254b70 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,116 @@ +import * as vscode from "vscode"; +import { generateCommitMessage, isOpenCodeAvailable } from "./opencodeService"; + +interface GitExtension { + getAPI(version: number): GitAPI; +} + +interface GitAPI { + repositories: Repository[]; +} + +interface Repository { + root: string; + rootUri: vscode.Uri; + state: RepositoryState; + inputBox: { value: string }; + diffIndexWithHEAD(path?: string): Promise; + diffWithHEAD(path?: string): Promise; + status(): Promise; +} + +interface RepositoryState { + indexChanges: any[]; + workingTreeChanges: any[]; +} + +export async function activate( + context: vscode.ExtensionContext, +): Promise { + const commandHandler = vscode.commands.registerCommand( + "aiCommitExt.generate", + async () => { + await handleGenerateCommitMessage(); + }, + ); + context.subscriptions.push(commandHandler); +} + +async function handleGenerateCommitMessage(): Promise { + const config = vscode.workspace.getConfiguration("aiCommitExt"); + const showNotification = config.get("showNotification", true); + + try { + const opencodeAvailable = await isOpenCodeAvailable(); + if (!opencodeAvailable) { + if (showNotification) { + vscode.window.showErrorMessage( + "OpenCode is not installed. Please install from https://opencode.ai", + ); + } + return; + } + + const gitExtension = + vscode.extensions.getExtension("vscode.git"); + if (!gitExtension) { + if (showNotification) { + vscode.window.showErrorMessage("Git extension not found"); + } + return; + } + + const gitApi = gitExtension.exports.getAPI(1); + const repositories = gitApi.repositories; + + if (repositories.length === 0) { + if (showNotification) { + vscode.window.showErrorMessage("No Git repository found"); + } + return; + } + + const repository = repositories[0]; + + await repository.status(); + + const state = repository.state; + + const stagedChanges = state.indexChanges || []; + const unstagedChanges = state.workingTreeChanges || []; + + if (stagedChanges.length === 0 && unstagedChanges.length === 0) { + if (showNotification) { + vscode.window.showErrorMessage("No changes to commit"); + } + return; + } + + const commitMessage = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + title: "Generating commit message...", + }, + async () => { + return await generateCommitMessage(); + }, + ); + + repository.inputBox.value = commitMessage; + + if (showNotification) { + vscode.window.showInformationMessage("Commit message generated"); + } + } catch (error) { + const message = + error instanceof Error ? error.message : "Unknown error"; + if (showNotification) { + vscode.window.showErrorMessage( + `Failed to generate commit message: ${message}`, + ); + } + } +} + +export function deactivate(): void {} diff --git a/src/gitService.ts b/src/gitService.ts new file mode 100644 index 0000000..25d0232 --- /dev/null +++ b/src/gitService.ts @@ -0,0 +1,170 @@ +import * as vscode from 'vscode'; + +interface GitExtension { + getAPI(version: number): GitAPI; +} + +interface GitAPI { + repositories: Repository[]; +} + +interface Repository { + root: string; + rootUri: vscode.Uri; + state: RepositoryState; + inputBox: { value: string }; + diffWithHEAD(path?: string): Promise; + diffIndexWithHEAD(path?: string): Promise; + status(): Promise; +} + +interface RepositoryState { + indexChanges: Change[]; + workingTreeChanges: Change[]; +} + +interface Change { + uri: vscode.Uri; + status: number; +} + +export interface GitChange { + path: string; + status: 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked'; + diff?: string; +} + +export async function getGitChanges(): Promise { + const repository = await getActiveGitRepository(); + if (!repository) { + throw new Error('No Git repository found'); + } + + const state = repository.state; + const changes: GitChange[] = []; + + const stagedChanges = state.indexChanges || []; + const unstagedChanges = state.workingTreeChanges || []; + + if (stagedChanges.length > 0) { + for (const change of stagedChanges) { + changes.push({ + path: change.uri.fsPath, + status: getChangeStatus(change.status), + }); + } + } else if (unstagedChanges.length > 0) { + const config = vscode.workspace.getConfiguration('aiCommitExt'); + const includeUnstaged = config.get('includeUnstaged', false); + + if (includeUnstaged) { + for (const change of unstagedChanges) { + changes.push({ + path: change.uri.fsPath, + status: getChangeStatus(change.status), + }); + } + } + } + + return changes; +} + +export async function getGitDiff(): Promise { + const repository = await getActiveGitRepository(); + if (!repository) { + throw new Error('No Git repository found'); + } + + const state = repository.state; + let diffOutput = ''; + + const stagedChanges = state.indexChanges || []; + const unstagedChanges = state.workingTreeChanges || []; + const config = vscode.workspace.getConfiguration('aiCommitExt'); + const includeUnstaged = config.get('includeUnstaged', false); + + if (stagedChanges.length > 0) { + for (const change of stagedChanges) { + const fileName = change.uri.fsPath.split('/').pop() || ''; + diffOutput += `## ${fileName} (staged)\n`; + + try { + const diff = await repository.diffIndexWithHEAD(change.uri.fsPath); + if (diff) { + diffOutput += diff + '\n'; + } + } catch { + diffOutput += '(Unable to get diff)\n'; + } + } + } else if (unstagedChanges.length > 0 && includeUnstaged) { + for (const change of unstagedChanges) { + const fileName = change.uri.fsPath.split('/').pop() || ''; + diffOutput += `## ${fileName} (unstaged)\n`; + + try { + const diff = await repository.diffWithHEAD(change.uri.fsPath); + if (diff) { + diffOutput += diff + '\n'; + } + } catch { + diffOutput += '(Unable to get diff)\n'; + } + } + } + + if (!diffOutput) { + throw new Error('No changes to commit'); + } + + return diffOutput; +} + +async function getActiveGitRepository(): Promise { + const gitExtension = vscode.extensions.getExtension('vscode.git'); + if (!gitExtension) { + throw new Error('Git extension not found'); + } + + const api = gitExtension.exports.getAPI(1); + if (api.repositories.length === 0) { + return null; + } + return api.repositories[0]; +} + +function getChangeStatus(status: number): GitChange['status'] { + const Status = { + INDEX_MODIFIED: 0, + INDEX_ADDED: 1, + INDEX_DELETED: 2, + INDEX_RENAMED: 3, + MODIFIED: 4, + DELETED: 5, + UNTRACKED: 6, + }; + + if (status === Status.INDEX_ADDED || status === Status.UNTRACKED) { + return 'added'; + } + if (status === Status.INDEX_DELETED || status === Status.DELETED) { + return 'deleted'; + } + if (status === Status.INDEX_RENAMED) { + return 'renamed'; + } + + return 'modified'; +} + +export async function getRepositoryRoot(): Promise { + const repository = await getActiveGitRepository(); + if (!repository) { + return null; + } + if (typeof repository.root === 'string') { + return repository.root; + } + return repository.rootUri.fsPath; +} \ No newline at end of file diff --git a/src/opencodeService.ts b/src/opencodeService.ts new file mode 100644 index 0000000..3a7820b --- /dev/null +++ b/src/opencodeService.ts @@ -0,0 +1,128 @@ +import { exec, spawn, ExecException } from "child_process"; +import * as vscode from "vscode"; +import { getGitDiff, getRepositoryRoot } from "./gitService"; + +export interface GenerateOptions { + model?: string; +} + +const DEFAULT_PROMPT = `You are a helpful assistant that generates git commit messages. +Generate a concise Conventional Commit message (max 72 characters for the subject line). +Format: (): + +Types: feat, fix, refactor, docs, style, test, chore, perf, ci, build, revert + +Only output the commit message, nothing else.`; + +export async function isOpenCodeAvailable(): Promise { + return new Promise((resolve) => { + exec("which opencode", (error: ExecException | null) => { + resolve(!error); + }); + }); +} + +export async function generateCommitMessage( + options: GenerateOptions = {}, +): Promise { + const opencodeAvailable = await isOpenCodeAvailable(); + if (!opencodeAvailable) { + throw new Error( + "OpenCode is not installed. Please install it from https://opencode.ai", + ); + } + + const diff = await getGitDiff(); + const repoRoot = await getRepositoryRoot(); + + const config = vscode.workspace.getConfiguration("aiCommitExt"); + const model = options.model || config.get("model", ""); + + const prompt = `${DEFAULT_PROMPT} + +Here are the git changes: +${diff} + +Generate a concise Conventional Commit message for these changes:`; + + return new Promise((resolve, reject) => { + const args: string[] = ["run", "--format", "default"]; + + if (model) { + args.push("--model", model); + } + + const proc = spawn("opencode", args, { + stdio: ["pipe", "pipe", "pipe"], + shell: false, + cwd: repoRoot || undefined, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + proc.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + proc.stdin?.write(prompt + "\n", (err) => { + if (err) { + new Error(`OpenCode write error with ${err}`); + } + proc.stdin?.end(); + }); + + proc.on("close", (code: number | null) => { + if (code !== 0) { + reject( + new Error(`OpenCode exited with code ${code}: ${stderr}`), + ); + return; + } + + const message = parseCommitMessage(stdout); + resolve(message); + }); + + proc.on("error", (err: Error) => { + reject(new Error(`Failed to run opencode: ${err.message}`)); + }); + + setTimeout(() => { + proc.kill(); + reject(new Error("OpenCode timed out")); + }, 120000); + }); +} + +function parseCommitMessage(output: string): string { + const lines = output.split("\n").filter((line) => line.trim()); + + for (const line of lines) { + const trimmed = line.trim(); + + if ( + trimmed.includes(":") && + (trimmed.startsWith("feat") || + trimmed.startsWith("fix") || + trimmed.startsWith("refactor") || + trimmed.startsWith("docs") || + trimmed.startsWith("style") || + trimmed.startsWith("test") || + trimmed.startsWith("chore") || + trimmed.startsWith("perf") || + trimmed.startsWith("ci") || + trimmed.startsWith("build") || + trimmed.startsWith("revert")) + ) { + return trimmed.substring(0, 200); + } + } + + const cleaned = + output.trim().split("\n")[0]?.substring(0, 200) || + "chore: generated commit message"; + return cleaned; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..007320f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "outDir": "./out", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node"], + "ignoreDeprecations": "6.0" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "out"] +} \ No newline at end of file