From c8467513133e9f95694263cf4a68abae6e61ecb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:11:39 +0000 Subject: [PATCH 1/2] Initial plan From 173466cc7c479d2171e0b4f0865b153e2a0d3b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:16:54 +0000 Subject: [PATCH 2/2] fix: add missing deps, fix security/quality issues from review Co-authored-by: jaseel0 <225665919+jaseel0@users.noreply.github.com> --- git-ai/package-lock.json | 340 +++++++++++++++++++++++- git-ai/package.json | 6 +- git-ai/src/cli/pr-command.ts | 31 +-- git-ai/src/index.ts | 2 +- git-ai/src/services/AIService.ts | 2 + git-ai/src/services/ConfigService.ts | 2 +- git-ai/src/services/ConflictResolver.ts | 91 +++++-- git-ai/src/services/GitHubService.ts | 24 +- 8 files changed, 460 insertions(+), 38 deletions(-) diff --git a/git-ai/package-lock.json b/git-ai/package-lock.json index 8740ad7..33421df 100644 --- a/git-ai/package-lock.json +++ b/git-ai/package-lock.json @@ -9,11 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^20.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^9.5.0", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.24.2" }, "bin": { "ai-git": "dist/cli.js" @@ -483,6 +487,15 @@ "node": ">=18" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -498,6 +511,167 @@ "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", "license": "MIT" }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "11.4.4-cjs.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.4-cjs.2.tgz", + "integrity": "sha512-2dK6z8fhs8lla5PaOTgqfCGBxgAv/le+EhPs27KklPhm1bKObpu6lXzwfUEQ16ajXzqNrKMujsFyo9K2eaoISw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.7.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz", + "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==", + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "13.3.2-cjs.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.2-cjs.1.tgz", + "integrity": "sha512-VUjIjOOvF2oELQmiFpWA1aOPdawpyaCUqcEBc/UOUnj3Xp6DJGrJ1+bjUIIDzdHjnFNO6q57ODMfdEZnoBkCwQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.8.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/rest": { + "version": "20.1.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.2.tgz", + "integrity": "sha512-GmYiltypkHHtihFwPRxlaorG5R9VAHuk/vbszVoRTGXnAsY60wYLkh/E2XiFmdZmqrisw+9FaazS1i5SbdWYgA==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.2", + "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.35", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", @@ -565,6 +739,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/auto-bind": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", @@ -577,6 +760,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -702,6 +891,12 @@ } } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -941,6 +1136,24 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -965,6 +1178,65 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -993,6 +1265,15 @@ "react": "^18.3.1" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1019,6 +1300,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1080,6 +1370,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -1124,6 +1432,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -1177,6 +1494,12 @@ "dev": true, "license": "MIT" }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, "node_modules/widest-line": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", @@ -1209,6 +1532,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "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==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", @@ -1235,6 +1564,15 @@ "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/git-ai/package.json b/git-ai/package.json index adaeb73..743a55e 100644 --- a/git-ai/package.json +++ b/git-ai/package.json @@ -31,11 +31,15 @@ "author": "", "license": "ISC", "dependencies": { + "@google/generative-ai": "^0.24.1", + "@octokit/rest": "^20.1.2", "chalk": "^5.3.0", "commander": "^12.1.0", "ink": "^5.0.0", + "pino": "^9.5.0", "react": "^18.2.0", - "simple-git": "^3.27.0" + "simple-git": "^3.27.0", + "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^20.11.0", diff --git a/git-ai/src/cli/pr-command.ts b/git-ai/src/cli/pr-command.ts index 0ed4148..fa161b2 100644 --- a/git-ai/src/cli/pr-command.ts +++ b/git-ai/src/cli/pr-command.ts @@ -21,32 +21,29 @@ export async function runPRCommand(): Promise { if (!origin || !origin.refs.fetch) { console.error('❌ Error: No remote "origin" found. Ensure your repo is hosted on GitHub.'); - return; + process.exit(1); } const githubService = new GitHubService(configService, origin.refs.fetch); - // 2. Define the selection handler - const handleSelect = (pr: PullRequestMetadata) => { - console.log('\n-----------------------------------'); - console.log(`🚀 Selected PR: #${pr.number}`); - console.log(`🔗 URL: ${pr.url}`); - console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); - console.log('-----------------------------------\n'); - - // In a future update, we can trigger gitService.checkout(pr.branch) - process.exit(0); - }; - - // 3. Launch the Ink TUI - const { waitUntilExit } = render( + // 2. Launch the Ink TUI + const renderInstance = render( React.createElement(PRList, { githubService, - onSelect: handleSelect + onSelect: (pr: PullRequestMetadata) => { + console.log('\n-----------------------------------'); + console.log(`🚀 Selected PR: #${pr.number}`); + console.log(`🔗 URL: ${pr.url}`); + console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); + console.log('-----------------------------------\n'); + // In a future update, we can trigger gitService.checkout(pr.branch) + renderInstance.unmount(); + } }) ); - await waitUntilExit(); + // 3. Await clean exit + await renderInstance.waitUntilExit(); } catch (error) { logger.error(error instanceof Error ? error : new Error(String(error)), 'Failed to initialize PR command'); console.error('❌ Critical Error: Could not launch PR interface.'); diff --git a/git-ai/src/index.ts b/git-ai/src/index.ts index 7570efc..8618465 100644 --- a/git-ai/src/index.ts +++ b/git-ai/src/index.ts @@ -7,7 +7,6 @@ import { logger } from './utils/logger.js'; const program = new Command(); const configService = new ConfigService(); const gitService = new GitService(); -const aiService = new AIService(configService); program .name('ai-git') @@ -18,6 +17,7 @@ program .description('Generate a commit message using AI and commit staged changes') .action(async () => { try { + const aiService = new AIService(configService); const diff = await gitService.getDiff(); if (!diff) { console.log('No staged changes found. Please stage files first.'); diff --git a/git-ai/src/services/AIService.ts b/git-ai/src/services/AIService.ts index 2b3e814..511e5d7 100644 --- a/git-ai/src/services/AIService.ts +++ b/git-ai/src/services/AIService.ts @@ -36,6 +36,8 @@ export class AIService implements AIProvider { maxOutputTokens: 200, }, }); + } else { + throw new Error(`Unsupported AI provider: ${config.ai.provider}`); } } diff --git a/git-ai/src/services/ConfigService.ts b/git-ai/src/services/ConfigService.ts index f4f33d0..16d7453 100644 --- a/git-ai/src/services/ConfigService.ts +++ b/git-ai/src/services/ConfigService.ts @@ -55,7 +55,7 @@ export class ConfigService { public saveConfig(newConfig: Config): void { const validated = ConfigSchema.parse(newConfig); - fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2)); + fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 }); this.config = validated; } } \ No newline at end of file diff --git a/git-ai/src/services/ConflictResolver.ts b/git-ai/src/services/ConflictResolver.ts index 6c4334b..85782c2 100644 --- a/git-ai/src/services/ConflictResolver.ts +++ b/git-ai/src/services/ConflictResolver.ts @@ -1,5 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; +import { randomBytes } from 'crypto'; import { AIService } from './AIService.js'; import { GitService } from '../core/GitService.js'; import { logger } from '../utils/logger.js'; @@ -10,6 +11,54 @@ export interface ConflictDetail { suggestion?: string; } +/** + * Patterns for detecting secrets and sensitive data in conflict content. + */ +const SECRET_PATTERNS: RegExp[] = [ + /-----BEGIN\s+(?:RSA\s+)?PRIVATE KEY-----[\s\S]*?-----END\s+(?:RSA\s+)?PRIVATE KEY-----/gi, + /(?:api[_\s-]?key|apikey|secret|token|password|passwd|pwd|auth)['":\s=]+['"]?[A-Za-z0-9/+_\-]{16,}['"]?/gi, + // Long base64-like strings that resemble encoded tokens (standalone, not part of identifiers) + /(?>>>>>>) from file content. + */ +function extractConflictHunks(content: string): string { + const lines = content.split('\n'); + const hunks: string[] = []; + let inConflict = false; + let hunk: string[] = []; + + for (const line of lines) { + if (line.startsWith('<<<<<<<')) { + inConflict = true; + hunk = [line]; + } else if (inConflict) { + hunk.push(line); + if (line.startsWith('>>>>>>>')) { + hunks.push(hunk.join('\n')); + hunk = []; + inConflict = false; + } + } + } + + return hunks.length > 0 ? hunks.join('\n\n') : content; +} + +/** + * Redacts common secret patterns from content before sending to an external model. + */ +function sanitizeContent(content: string): string { + let sanitized = extractConflictHunks(content); + for (const pattern of SECRET_PATTERNS) { + sanitized = sanitized.replace(pattern, '[REDACTED]'); + } + return sanitized; +} + export class ConflictResolver { constructor( private aiService: AIService, @@ -50,15 +99,18 @@ export class ConflictResolver { } /** - * Uses Gemini to analyze the conflict markers and suggest a fix + * Uses Gemini to analyze the conflict markers and suggest a fix. + * Sanitizes the content before sending to the external model. */ public async suggestResolution(conflict: ConflictDetail): Promise { + const sanitizedContent = sanitizeContent(conflict.content); + const prompt = ` You are a senior software architect. I have a merge conflict in the file: ${conflict.file}. - Below is the file content containing git conflict markers (<<<<<<<, =======, >>>>>>>). + Below are the conflict hunks containing git conflict markers (<<<<<<<, =======, >>>>>>>). - FILE CONTENT: - ${conflict.content} + CONFLICT HUNKS: + ${sanitizedContent} INSTRUCTIONS: 1. Analyze the changes from both branches. @@ -81,8 +133,8 @@ export class ConflictResolver { } /** - * Applies the AI's suggested resolution to the physical file. - * Uses atomic write with O_NOFOLLOW to prevent symlink attacks and TOCTOU races. + * Applies the AI's suggested resolution to the physical file using a truly atomic + * write: write to a temp file in the same directory, fsync, then rename to target. */ public async applyResolution(file: string, resolvedContent: string): Promise { const realRepoRoot = await fs.realpath(process.cwd()); @@ -95,11 +147,9 @@ export class ConflictResolver { throw new Error(`Refusing to write to symlink: ${file}`); } } catch (error: unknown) { - // Handle ENOENT gracefully - file doesn't exist yet, which is fine if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') { // File does not exist, proceeding with creation is safe } else { - // Re-throw any other error throw error; } } @@ -120,15 +170,26 @@ export class ConflictResolver { throw new Error(`Refusing to write outside repository root: ${file}`); } - // Use atomic write with O_NOFOLLOW to prevent TOCTOU races and symlink escape - const flags = fs.constants.O_NOFOLLOW | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY; + // Atomic write: write to a temp file, fsync, then rename into place. + const tempPath = path.join(targetParent, `.tmp-${process.pid}-${randomBytes(8).toString('hex')}`); const buffer = Buffer.from(resolvedContent, 'utf-8'); - const fileHandle = await fs.open(targetPath, flags, 0o644); + const tempHandle = await fs.open(tempPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o644); try { - await fileHandle.write(buffer, 0, buffer.length); - await fileHandle.sync(); - } finally { - await fileHandle.close(); + await tempHandle.write(buffer, 0, buffer.length); + await tempHandle.sync(); + await tempHandle.close(); + } catch (error) { + await tempHandle.close().catch(() => {}); + await fs.unlink(tempPath).catch(() => {}); + throw error; + } + + // Atomically replace target with temp file + try { + await fs.rename(tempPath, targetPath); + } catch (error) { + await fs.unlink(tempPath).catch(() => {}); + throw error; } // Note: User should still 'git add' the file manually or via CLI flow } diff --git a/git-ai/src/services/GitHubService.ts b/git-ai/src/services/GitHubService.ts index 250e3c5..062fd85 100644 --- a/git-ai/src/services/GitHubService.ts +++ b/git-ai/src/services/GitHubService.ts @@ -33,6 +33,26 @@ export class GitHubService { this.parseRepoInfo(repoUrl); } + /** + * Strips credentials (userinfo) from a URL for safe logging. + */ + private sanitizeUrl(url: string): string { + try { + // Handle SSH-style git URLs like git@github.com:owner/repo.git + const sshMatch = url.match(/^[^@]+@([^:]+):(.+)$/); + if (sshMatch) { + return `${sshMatch[1]}:${sshMatch[2]}`; + } + const parsed = new URL(url); + parsed.username = ''; + parsed.password = ''; + return parsed.toString(); + } catch { + // If URL parsing fails, return a placeholder + return '[unparseable URL]'; + } + } + /** * Extracts owner and repo name from a git remote URL */ @@ -42,12 +62,12 @@ export class GitHubService { this.owner = match[1]; this.repo = match[2]; } else { - logger.error(`Could not parse GitHub owner/repo from remote URL: ${url}`); + logger.error(`Could not parse GitHub owner/repo from remote URL: ${this.sanitizeUrl(url)}`); throw new Error("Invalid GitHub remote URL. Expected github.com//."); } if (!this.owner || !this.repo) { - logger.error(`Parsed empty GitHub owner/repo from remote URL: ${url}`); + logger.error(`Parsed empty GitHub owner/repo from remote URL: ${this.sanitizeUrl(url)}`); throw new Error("Could not determine GitHub owner and repository from remote URL."); } }