diff --git a/package.json b/package.json index 4c296b6..0a44fe9 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "db:generate": "drizzle-kit generate", "db:push": "drizzle-kit push", "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:studio": "drizzle-kit studio", + "test": "vitest" }, "dependencies": { "better-sqlite3": "^12.2.0", @@ -29,7 +30,8 @@ "drizzle-orm": "^0.44.2", "ink": "5.2.1", "ink-text-input": "^6.0.0", - "react": "^18.3.1" + "react": "^18.3.1", + "simple-git": "^3.28.0" }, "devDependencies": { "@biomejs/biome": "2.0.6", @@ -40,7 +42,8 @@ "drizzle-kit": "^0.31.4", "tsup": "^8.5.0", "tsx": "^4.20.3", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.2.4" }, "packageManager": "pnpm@9.15.5" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a51e8a9..9af9e01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + simple-git: + specifier: ^3.28.0 + version: 3.28.0 devDependencies: '@biomejs/biome': specifier: 2.0.6 @@ -47,13 +50,16 @@ importers: version: 0.31.4 tsup: specifier: ^8.5.0 - version: 8.5.0(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) + version: 8.5.0(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0) tsx: specifier: ^4.20.3 version: 4.20.3 typescript: specifier: ^5 version: 5.8.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0) packages: @@ -429,6 +435,12 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -536,6 +548,12 @@ packages: '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -545,6 +563,35 @@ packages: '@types/react@19.1.8': resolution: {integrity: sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -573,6 +620,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + auto-bind@5.0.1: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -612,10 +663,18 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + chalk@5.4.1: resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -685,6 +744,10 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -812,6 +875,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-toolkit@1.39.5: resolution: {integrity: sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==} @@ -834,10 +900,17 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + fdir@6.4.6: resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: @@ -941,6 +1014,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -959,6 +1035,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -996,6 +1075,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -1032,6 +1116,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1064,6 +1152,10 @@ packages: yaml: optional: true + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -1133,6 +1225,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -1146,6 +1241,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-git@3.28.0: + resolution: {integrity: sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -1154,6 +1252,10 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -1169,6 +1271,12 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1196,6 +1304,9 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -1215,6 +1326,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1222,6 +1336,18 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -1277,6 +1403,79 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.0.3: + resolution: {integrity: sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -1288,6 +1487,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} @@ -1548,6 +1752,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -1615,6 +1827,12 @@ snapshots: dependencies: '@types/node': 24.0.7 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/node@24.0.7': @@ -1625,6 +1843,48 @@ snapshots: dependencies: csstype: 3.1.3 + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + acorn@8.15.0: {} ansi-escapes@7.0.0: @@ -1643,6 +1903,8 @@ snapshots: any-promise@1.3.0: {} + assertion-error@2.0.1: {} + auto-bind@5.0.1: {} balanced-match@1.0.2: {} @@ -1682,8 +1944,18 @@ snapshots: cac@6.7.14: {} + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + chalk@5.4.1: {} + check-error@2.1.1: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -1737,6 +2009,8 @@ snapshots: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} detect-libc@2.0.4: {} @@ -1771,6 +2045,8 @@ snapshots: environment@1.1.0: {} + es-module-lexer@1.7.0: {} + es-toolkit@1.39.5: {} esbuild-register@3.6.0(esbuild@0.25.5): @@ -1835,8 +2111,14 @@ snapshots: escape-string-regexp@2.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + expand-template@2.0.3: {} + expect-type@1.2.2: {} + fdir@6.4.6(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -1946,6 +2228,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -1958,6 +2242,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.4: {} + lru-cache@10.4.3: {} magic-string@0.30.17: @@ -1993,6 +2279,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.11: {} + napi-build-utils@2.0.0: {} node-abi@3.75.0: @@ -2022,6 +2310,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@4.0.2: {} @@ -2034,13 +2324,20 @@ snapshots: mlly: 1.7.4 pathe: 2.0.3 - postcss-load-config@6.0.1(tsx@4.20.3)(yaml@2.8.0): + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0): dependencies: lilconfig: 3.1.3 optionalDependencies: + postcss: 8.5.6 tsx: 4.20.3 yaml: 2.8.0 + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prebuild-install@7.1.3: dependencies: detect-libc: 2.0.4 @@ -2137,6 +2434,8 @@ snapshots: shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -2149,6 +2448,14 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-git@3.28.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -2159,6 +2466,8 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + source-map-js@1.2.1: {} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -2174,6 +2483,10 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + + std-env@3.9.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -2206,6 +2519,10 @@ snapshots: strip-json-comments@2.0.1: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.8 @@ -2239,6 +2556,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.14: @@ -2246,6 +2565,12 @@ snapshots: fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -2254,7 +2579,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.0(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): + tsup@8.5.0(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0): dependencies: bundle-require: 5.1.0(esbuild@0.25.5) cac: 6.7.14 @@ -2265,7 +2590,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(tsx@4.20.3)(yaml@2.8.0) + postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.20.3)(yaml@2.8.0) resolve-from: 5.0.0 rollup: 4.44.1 source-map: 0.8.0-beta.0 @@ -2274,6 +2599,7 @@ snapshots: tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: + postcss: 8.5.6 typescript: 5.8.3 transitivePeerDependencies: - jiti @@ -2302,6 +2628,82 @@ snapshots: util-deprecate@1.0.2: {} + vite-node@3.2.4(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.7 + fsevents: 2.3.3 + tsx: 4.20.3 + yaml: 2.8.0 + + vitest@3.2.4(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.0.3(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@24.0.7)(tsx@4.20.3)(yaml@2.8.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.0.7 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + webidl-conversions@4.0.2: {} whatwg-url@7.1.0: @@ -2314,6 +2716,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@5.0.0: dependencies: string-width: 7.2.0 diff --git a/src/cli.ts b/src/cli.ts index 90b9a5d..7dd4d0d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,10 +16,10 @@ program .command("start") .description("Start a new intent") .argument("", "Intent message") - .action((message: string) => { + .action(async (message: string) => { try { - const projectId = ensureProject(); - const branchId = ensureBranch(projectId); + const projectId = await ensureProject(); + const branchId = await ensureBranch(projectId); const rowid = commands.start({ message, branchId }); console.log(`Started intent #${rowid}: ${message}`); diff --git a/src/core/git/errors.ts b/src/core/git/errors.ts new file mode 100644 index 0000000..9723f6a --- /dev/null +++ b/src/core/git/errors.ts @@ -0,0 +1,18 @@ +export class GitError extends Error { + constructor(message: string) { + super(message); + this.name = "GitError"; + } +} + +export class EmptyCommitMessageError extends GitError { + constructor(message: string) { + super(message); + } +} + +export class IsNotGitRepositoryError extends GitError { + constructor(message: string) { + super(message); + } +} diff --git a/src/core/git/gitManager.test.ts b/src/core/git/gitManager.test.ts new file mode 100644 index 0000000..abca7ef --- /dev/null +++ b/src/core/git/gitManager.test.ts @@ -0,0 +1,147 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + EmptyCommitMessageError, + GitError, + IsNotGitRepositoryError, +} from "./errors"; +import { __testing__, createGitService } from "./gitManager"; + +function createMockGit(overrides = {}) { + return { + checkIsRepo: vi.fn().mockResolvedValue(true), + commit: vi.fn().mockResolvedValue({ commit: "abc123" }), + revparse: vi.fn().mockResolvedValue("abc123"), + branchLocal: vi.fn().mockResolvedValue({ + detached: false, + current: "main", + }), + ...overrides, + }; +} + +describe("GitService", () => { + beforeEach(() => { + __testing__.clearGlobalCache(); + }); + + it("should cache git instances efficiently", () => { + const mockGit = createMockGit(); + const mockFactory = vi.fn().mockReturnValue(mockGit); + + expect(__testing__.getGlobalCacheSize()).toBe(0); + + const service1 = createGitService({ gitProvider: mockFactory }); + const service2 = createGitService({ gitProvider: mockFactory }); + + service1.commit("abc123"); + service2.commit("abc123"); + + expect(__testing__.getGlobalCacheSize()).toBe(1); + }); + + describe("getProjectMetadata", () => { + it("should handle git command failures", async () => { + const mockGitFactory = vi.fn().mockReturnValue({ + revparse: vi.fn().mockRejectedValue(new Error("Git command failed")), + }); + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.getProjectMetadata()).rejects.toThrow(GitError); + }); + + it("should only work inside a git repo", async () => { + const mockGit = { + checkIsRepo: vi.fn().mockReturnValue(false), + }; + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.getProjectMetadata()).rejects.instanceOf( + IsNotGitRepositoryError, + ); + }); + }); + + describe("getBranchMetadata", () => { + it("should handle detached HEAD state", async () => { + const mockGit = createMockGit({ + branchLocal: vi.fn().mockResolvedValue({ + detached: true, + }), + }); + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.getBranchMetadata("project-id")).rejects.toThrow( + "Cannot determine branch name in detached HEAD state", + ); + }); + + it("should only work inside a git repo", async () => { + const mockGit = createMockGit({ + checkIsRepo: vi.fn().mockReturnValue(false), + }); + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.getBranchMetadata("project-id")).rejects.toThrow( + IsNotGitRepositoryError, + ); + }); + + it("should handle git command failures", async () => { + const mockGit = createMockGit({ + revparse: vi.fn().mockRejectedValue(new Error("Git command failed")), + }); + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.getBranchMetadata("project-id")).rejects.toThrow( + GitError, + ); + }); + }); + + describe("commit", () => { + it("should handle commit success", async () => { + const mockGit = createMockGit(); + + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + + const service = createGitService({ gitProvider: mockGitFactory }); + const result = await service.commit("test message"); + + expect(result).toBe("abc123"); + expect(mockGit.commit).toHaveBeenCalledWith("test message"); + }); + + it("should not allow commit outside of git repo", async () => { + const mockGit = createMockGit({ + checkIsRepo: vi.fn().mockReturnValue(false), + }); + const mockGitFactory = vi.fn().mockReturnValue(mockGit); + + const service = createGitService({ gitProvider: mockGitFactory }); + + await expect(service.commit("test message")).rejects.instanceOf( + IsNotGitRepositoryError, + ); + }); + + it("should not allow commit without message", async () => { + const service = createGitService(); + + await expect(service.commit("")).rejects.instanceOf( + EmptyCommitMessageError, + ); + await expect(service.commit(" ")).rejects.instanceOf( + EmptyCommitMessageError, + ); + await expect(service.commit("\n")).rejects.instanceOf( + EmptyCommitMessageError, + ); + }); + }); +}); diff --git a/src/core/git/gitManager.ts b/src/core/git/gitManager.ts new file mode 100644 index 0000000..1c4b65b --- /dev/null +++ b/src/core/git/gitManager.ts @@ -0,0 +1,147 @@ +import path from "node:path"; +import { type SimpleGit, type SimpleGitOptions, simpleGit } from "simple-git"; +import type { NewBranch, NewProject } from "../db/schema"; +import { getErrorMessage } from "../utils/error"; +import { + EmptyCommitMessageError, + GitError, + IsNotGitRepositoryError, +} from "./errors"; + +interface GitServiceConfig { + gitProvider?: typeof simpleGit; + repoPath?: string; + gitOptions?: Partial; +} + +const gitInstances = new Map(); + +export function createGitService(config: GitServiceConfig = {}) { + const { + gitProvider: gitFactory = simpleGit, + repoPath = process.cwd(), + gitOptions = { trimmed: true }, + } = config; + + function getGitInstance(): SimpleGit { + const normalizedPath = path.resolve(repoPath); + + if (!gitInstances.has(normalizedPath)) { + const newGitInstance = gitFactory(normalizedPath, gitOptions); + gitInstances.set(normalizedPath, newGitInstance); + } + + return gitInstances.get(normalizedPath)!; + } + + async function checkIsRepo(): Promise { + try { + const git = getGitInstance(); + const isRepo = await git.checkIsRepo(); + return isRepo; + } catch (error) { + throw new GitError( + `Failed to check if directory is a Git repository: ${getErrorMessage(error)}`, + ); + } + } + + async function getProjectMetadata(): Promise { + const git = getGitInstance(); + + const isGitRepo = await checkIsRepo(); + if (!isGitRepo) { + throw new IsNotGitRepositoryError( + "Current directory is not a git repository", + ); + } + + try { + const repoPath = await git.revparse(["--show-toplevel"]); + const repoName = path.basename(repoPath); + + return { + id: repoPath, // TODO: make it unique + repoPath, + repoName, + }; + } catch (error) { + throw new GitError( + `Failed to get project metadata: ${getErrorMessage(error)}`, + ); + } + } + + async function getBranchMetadata(projectId: string): Promise { + const git = getGitInstance(); + + const isGitRepo = await checkIsRepo(); + if (!isGitRepo) { + throw new IsNotGitRepositoryError( + "Current directory is not a git repository", + ); + } + + try { + const branchSummary = await git.branchLocal(); + + if (branchSummary.detached) { + throw new GitError( + "Cannot determine branch name in detached HEAD state", + ); + } + + const currentBranchFull = await git.raw("symbolic-ref", "HEAD"); + const currentBranchShort = await git.raw( + "symbolic-ref", + "HEAD", + "--short", + ); + + return { + id: currentBranchFull, + name: currentBranchShort, + projectId, + }; + } catch (error) { + throw new GitError( + `Failed to get branch metadata: ${getErrorMessage(error)}`, + ); + } + } + + async function commit(message: string): Promise { + const git = getGitInstance(); + + const isGitRepo = await checkIsRepo(); + if (!isGitRepo) { + throw new IsNotGitRepositoryError( + "Current directory is not a git repository", + ); + } + + const trimmedMessage = message.trim(); + if (!trimmedMessage) { + throw new EmptyCommitMessageError("Commit message is required"); + } + + try { + const commitResult = await git.commit(trimmedMessage); + + return commitResult.commit; + } catch (error) { + throw new GitError(`Failed to commit changes: ${getErrorMessage(error)}`); + } + } + + return { + commit, + getBranchMetadata, + getProjectMetadata, + }; +} + +export const __testing__ = { + clearGlobalCache: () => gitInstances.clear(), + getGlobalCacheSize: () => gitInstances.size, +}; diff --git a/src/core/utils/branch.ts b/src/core/utils/branch.ts index dd32460..2830ec3 100644 --- a/src/core/utils/branch.ts +++ b/src/core/utils/branch.ts @@ -1,9 +1,10 @@ import { branches } from "../db/schema"; +import { createGitService } from "../git/gitManager"; import { ensureEntity } from "./db-helpers"; -import { getBranchMetadata } from "./git"; -export function ensureBranch(projectId: string) { - const branchMeta = getBranchMetadata(projectId); +export async function ensureBranch(projectId: string) { + const { getBranchMetadata } = createGitService(); + const branchMeta = await getBranchMetadata(projectId); return ensureEntity({ table: branches, diff --git a/src/core/utils/git.ts b/src/core/utils/git.ts deleted file mode 100644 index c0fb7fe..0000000 --- a/src/core/utils/git.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { execSync } from "node:child_process"; -import path from "node:path"; -import type { Branch, Project } from "../db/schema"; -import { getErrorMessage } from "./error"; - -export class GitError extends Error { - constructor(message: string) { - super(message); - this.name = "GitError"; - } -} - -export function execGit(command: string): string { - try { - return execSync(`git ${command}`, { encoding: "utf-8" }).trim(); - } catch (error) { - throw new GitError(`Git command failed: ${getErrorMessage(error)}`); - } -} - -export function getProjectMetadata(): Project { - try { - const repoPath = execGit("rev-parse --show-toplevel"); - const repoName = path.basename(repoPath); - - return { - id: repoPath, // TODO: make it unique - repoPath, - repoName, - }; - } catch (error) { - throw new GitError( - `Failed to get project metadata: ${getErrorMessage(error)}`, - ); - } -} - -export function getBranchMetadata(projectId: string): Branch { - try { - const branchName = execGit("rev-parse --abbrev-ref HEAD"); - - if (branchName === "HEAD") { - throw new GitError("Cannot determine branch name in detached HEAD state"); - } - - const branchRefHashId = execGit( - `rev-parse --symbolic-full-name ${branchName}`, - ); - - return { - id: branchRefHashId, - name: branchName, - projectId, - }; - } catch (error) { - throw new GitError( - `Failed to get branch metadata: ${getErrorMessage(error)}`, - ); - } -} - -export function commit(message: string): string { - if (!message) { - throw new Error("Commit message is required"); - } - - try { - execGit(`commit -m "${message}"`); - const commitHash = execGit("rev-parse HEAD"); - - return commitHash; - } catch (error) { - throw new GitError(`Failed to commit changes: ${getErrorMessage(error)}`); - } -} diff --git a/src/core/utils/project.ts b/src/core/utils/project.ts index 6c0bc6a..0976aa5 100644 --- a/src/core/utils/project.ts +++ b/src/core/utils/project.ts @@ -1,9 +1,10 @@ import { projects } from "../db/schema"; +import { createGitService } from "../git/gitManager"; import { ensureEntity } from "./db-helpers"; -import { getProjectMetadata } from "./git"; -export function ensureProject() { - const projectMeta = getProjectMetadata(); +export async function ensureProject() { + const { getProjectMetadata } = createGitService(); + const projectMeta = await getProjectMetadata(); return ensureEntity({ table: projects, diff --git a/src/ui/contexts/QueryContext.tsx b/src/ui/contexts/QueryContext.tsx index 8f194f5..8061d35 100644 --- a/src/ui/contexts/QueryContext.tsx +++ b/src/ui/contexts/QueryContext.tsx @@ -31,13 +31,13 @@ export const QueryProvider = ({ children }: { children: ReactNode }) => { const [activeIntentList, setActiveIntentList] = useState(getActiveIntent); - const submitQuery = () => { + const submitQuery = async () => { if (query.trim() === "") { return; } - const projectId = ensureProject(); - const branchId = ensureBranch(projectId); + const projectId = await ensureProject(); + const branchId = await ensureBranch(projectId); commands.start({ message: query, branchId }); setActiveIntentList(getActiveIntent()); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c024f05 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { configDefaults, defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, "./drizzle/*"], + }, +});