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
This commit is contained in:
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -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
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
out
|
||||||
|
node_modules
|
||||||
572
package-lock.json
generated
Normal file
572
package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
package.json
Normal file
70
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
readme.md
Normal file
33
readme.md
Normal file
@@ -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
|
||||||
116
src/extension.ts
Normal file
116
src/extension.ts
Normal file
@@ -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<string>;
|
||||||
|
diffWithHEAD(path?: string): Promise<string>;
|
||||||
|
status(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RepositoryState {
|
||||||
|
indexChanges: any[];
|
||||||
|
workingTreeChanges: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function activate(
|
||||||
|
context: vscode.ExtensionContext,
|
||||||
|
): Promise<void> {
|
||||||
|
const commandHandler = vscode.commands.registerCommand(
|
||||||
|
"aiCommitExt.generate",
|
||||||
|
async () => {
|
||||||
|
await handleGenerateCommitMessage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
context.subscriptions.push(commandHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGenerateCommitMessage(): Promise<void> {
|
||||||
|
const config = vscode.workspace.getConfiguration("aiCommitExt");
|
||||||
|
const showNotification = config.get<boolean>("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<GitExtension>("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 {}
|
||||||
170
src/gitService.ts
Normal file
170
src/gitService.ts
Normal file
@@ -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<string>;
|
||||||
|
diffIndexWithHEAD(path?: string): Promise<string>;
|
||||||
|
status(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<GitChange[]> {
|
||||||
|
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<boolean>('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<string> {
|
||||||
|
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<boolean>('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<Repository | null> {
|
||||||
|
const gitExtension = vscode.extensions.getExtension<GitExtension>('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<string | null> {
|
||||||
|
const repository = await getActiveGitRepository();
|
||||||
|
if (!repository) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof repository.root === 'string') {
|
||||||
|
return repository.root;
|
||||||
|
}
|
||||||
|
return repository.rootUri.fsPath;
|
||||||
|
}
|
||||||
128
src/opencodeService.ts
Normal file
128
src/opencodeService.ts
Normal file
@@ -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: <type>(<scope>): <description>
|
||||||
|
|
||||||
|
Types: feat, fix, refactor, docs, style, test, chore, perf, ci, build, revert
|
||||||
|
|
||||||
|
Only output the commit message, nothing else.`;
|
||||||
|
|
||||||
|
export async function isOpenCodeAvailable(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
exec("which opencode", (error: ExecException | null) => {
|
||||||
|
resolve(!error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateCommitMessage(
|
||||||
|
options: GenerateOptions = {},
|
||||||
|
): Promise<string> {
|
||||||
|
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<string>("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;
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user