Big upgrade to windows and linux compability, also introduced a auto upgrading functionality to work cross platform with also the posiblity for Git init on app startup and checking when adding new projects.

This commit is contained in:
AndyMik90
2025-12-18 13:41:57 +01:00
parent 96dd04d411
commit d7fd1a24da
34 changed files with 784 additions and 89 deletions
+9 -1
View File
@@ -5,13 +5,21 @@ import { resolve } from 'path';
export default defineConfig({ export default defineConfig({
main: { main: {
plugins: [externalizeDepsPlugin({ plugins: [externalizeDepsPlugin({
exclude: [] // Bundle these packages into the main process (they won't be in node_modules in packaged app)
exclude: [
'uuid',
'chokidar',
'ioredis',
'electron-updater',
'@electron-toolkit/utils'
]
})], })],
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
index: resolve(__dirname, 'src/main/index.ts') index: resolve(__dirname, 'src/main/index.ts')
}, },
// Only node-pty needs to be external (native module rebuilt by electron-builder)
external: ['node-pty'] external: ['node-pty']
} }
} }
+40 -38
View File
@@ -42,7 +42,7 @@
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"lucide-react": "^0.560.0", "lucide-react": "^0.560.0",
"motion": "^12.23.26", "motion": "^12.23.26",
"node-pty": "^1.0.0", "node-pty": "^1.1.0-beta42",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -150,6 +150,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -535,6 +536,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -558,6 +560,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
@@ -597,6 +600,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -991,7 +995,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1013,7 +1016,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -3928,8 +3930,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
@@ -4126,6 +4127,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -4136,6 +4138,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -4227,6 +4230,7 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0", "@typescript-eslint/types": "8.49.0",
@@ -4626,7 +4630,8 @@
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/7zip-bin": { "node_modules/7zip-bin": {
"version": "5.2.0", "version": "5.2.0",
@@ -4648,6 +4653,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -4708,6 +4714,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -4880,7 +4887,6 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
@@ -5265,6 +5271,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -5944,8 +5951,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
@@ -6289,6 +6295,7 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==", "integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.0.12", "app-builder-lib": "26.0.12",
"builder-util": "26.0.11", "builder-util": "26.0.11",
@@ -6346,8 +6353,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
@@ -6423,6 +6429,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^22.7.7", "@types/node": "^22.7.7",
@@ -6551,7 +6558,6 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -6572,7 +6578,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -6588,7 +6593,6 @@
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"optionalDependencies": { "optionalDependencies": {
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
@@ -6599,7 +6603,6 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
@@ -6969,6 +6972,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -8988,6 +8992,7 @@
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"cssstyle": "^4.2.1", "cssstyle": "^4.2.1",
"data-urls": "^5.0.0", "data-urls": "^5.0.0",
@@ -9931,7 +9936,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"lz-string": "bin/bin.js" "lz-string": "bin/bin.js"
} }
@@ -11198,12 +11202,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/nan": {
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz",
"integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==",
"license": "MIT"
},
"node_modules/nano-spawn": { "node_modules/nano-spawn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@@ -11285,15 +11283,21 @@
} }
}, },
"node_modules/node-pty": { "node_modules/node-pty": {
"version": "1.0.0", "version": "1.1.0-beta42",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta42.tgz",
"integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", "integrity": "sha512-59KoV6xxhJciRVpo4lQ9wnP38SPaBlXgwszYS8nlHAHrt02d14peg+kHtJ4AOtyLWiCf8WPCeJNbxBkiA7Oy7Q==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nan": "^2.17.0" "node-addon-api": "^7.1.0"
} }
}, },
"node_modules/node-pty/node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -11771,6 +11775,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -11868,6 +11873,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -11904,7 +11910,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -11922,7 +11927,6 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -11943,7 +11947,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1", "ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
@@ -11959,7 +11962,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@@ -11972,8 +11974,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/proc-log": { "node_modules/proc-log": {
"version": "2.0.1", "version": "2.0.1",
@@ -12077,6 +12078,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -12086,6 +12088,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -13402,7 +13405,8 @@
"version": "4.1.18", "version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@@ -13459,7 +13463,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -13486,7 +13489,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@@ -13508,7 +13510,6 @@
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -13522,7 +13523,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -13537,7 +13537,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -13854,6 +13853,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -14194,6 +14194,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -15227,6 +15228,7 @@
"integrity": "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==", "integrity": "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+14 -3
View File
@@ -59,7 +59,7 @@
"ioredis": "^5.8.2", "ioredis": "^5.8.2",
"lucide-react": "^0.560.0", "lucide-react": "^0.560.0",
"motion": "^12.23.26", "motion": "^12.23.26",
"node-pty": "^1.0.0", "node-pty": "^1.1.0-beta42",
"react": "^19.2.3", "react": "^19.2.3",
"react-dom": "^19.2.3", "react-dom": "^19.2.3",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -127,7 +127,18 @@
"buildResources": "resources" "buildResources": "resources"
}, },
"files": [ "files": [
"out/**/*" "out/**/*",
"package.json"
],
"extraResources": [
{
"from": "node_modules/node-pty",
"to": "node_modules/node-pty"
},
{
"from": "resources/icon.ico",
"to": "icon.ico"
}
], ],
"mac": { "mac": {
"category": "public.app-category.developer-tools", "category": "public.app-category.developer-tools",
@@ -138,7 +149,7 @@
] ]
}, },
"win": { "win": {
"icon": "resources/icon-256.png", "icon": "resources/icon.ico",
"target": [ "target": [
"nsis", "nsis",
"zip" "zip"
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

@@ -176,7 +176,8 @@ export class AgentProcessManager {
...extraEnv, ...extraEnv,
...profileEnv, // Include active Claude profile config ...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1', // Ensure real-time output PYTHONUNBUFFERED: '1', // Ensure real-time output
PYTHONIOENCODING: 'utf-8' // Ensure UTF-8 encoding on Windows PYTHONIOENCODING: 'utf-8', // Ensure UTF-8 encoding on Windows
PYTHONUTF8: '1' // Force Python UTF-8 mode on Windows (Python 3.7+)
} }
}); });
@@ -241,17 +242,17 @@ export class AgentProcessManager {
} }
}; };
// Handle stdout // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stdout?.on('data', (data: Buffer) => { childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
console.log('[spawnProcess] stdout:', log.substring(0, 200)); console.log('[spawnProcess] stdout:', log.substring(0, 200));
this.emitter.emit('log', taskId, log); this.emitter.emit('log', taskId, log);
processLog(log); processLog(log);
}); });
// Handle stderr // Handle stderr - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stderr?.on('data', (data: Buffer) => { childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
console.log('[spawnProcess] stderr:', log.substring(0, 200)); console.log('[spawnProcess] stderr:', log.substring(0, 200));
// Some Python output goes to stderr (like progress bars) // Some Python output goes to stderr (like progress bars)
// so we treat it as log, not error // so we treat it as log, not error
+14 -10
View File
@@ -156,7 +156,9 @@ export class AgentQueueManager {
...process.env, ...process.env,
...combinedEnv, ...combinedEnv,
...profileEnv, ...profileEnv,
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
} }
}); });
@@ -193,9 +195,9 @@ export class AgentQueueManager {
const completedTypes = new Set<string>(); const completedTypes = new Set<string>();
const totalTypes = 7; // Default all types const totalTypes = 7; // Default all types
// Handle stdout // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stdout?.on('data', (data: Buffer) => { childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
// Collect output for rate limit detection (keep last 10KB) // Collect output for rate limit detection (keep last 10KB)
allOutput = (allOutput + log).slice(-10000); allOutput = (allOutput + log).slice(-10000);
@@ -242,9 +244,9 @@ export class AgentQueueManager {
}); });
}); });
// Handle stderr - also emit as logs // Handle stderr - also emit as logs, explicitly decode as UTF-8
childProcess.stderr?.on('data', (data: Buffer) => { childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
// Collect stderr for rate limit detection too // Collect stderr for rate limit detection too
allOutput = (allOutput + log).slice(-10000); allOutput = (allOutput + log).slice(-10000);
console.error('[Ideation STDERR]', log); console.error('[Ideation STDERR]', log);
@@ -354,7 +356,9 @@ export class AgentQueueManager {
...process.env, ...process.env,
...combinedEnv, ...combinedEnv,
...profileEnv, ...profileEnv,
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
} }
}); });
@@ -384,9 +388,9 @@ export class AgentQueueManager {
} }
}; };
// Handle stdout // Handle stdout - explicitly decode as UTF-8 for cross-platform Unicode support
childProcess.stdout?.on('data', (data: Buffer) => { childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
// Collect output for rate limit detection (keep last 10KB) // Collect output for rate limit detection (keep last 10KB)
allRoadmapOutput = (allRoadmapOutput + log).slice(-10000); allRoadmapOutput = (allRoadmapOutput + log).slice(-10000);
@@ -406,9 +410,9 @@ export class AgentQueueManager {
}); });
}); });
// Handle stderr // Handle stderr - explicitly decode as UTF-8
childProcess.stderr?.on('data', (data: Buffer) => { childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString(); const log = data.toString('utf8');
// Collect stderr for rate limit detection too // Collect stderr for rate limit detection too
allRoadmapOutput = (allRoadmapOutput + log).slice(-10000); allRoadmapOutput = (allRoadmapOutput + log).slice(-10000);
console.error('[Roadmap STDERR]', log); console.error('[Roadmap STDERR]', log);
@@ -277,7 +277,9 @@ export class ChangelogGenerator extends EventEmitter {
USER: process.env.USER || process.env.USERNAME || 'user', USER: process.env.USER || process.env.USERNAME || 'user',
// Add common binary locations to PATH for claude CLI // Add common binary locations to PATH for claude CLI
PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter), PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter),
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
}; };
this.debug('Spawn environment', { this.debug('Spawn environment', {
@@ -237,7 +237,9 @@ except Exception as e:
...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }), ...(isWindows ? { USERPROFILE: homeDir } : { HOME: homeDir }),
USER: process.env.USER || process.env.USERNAME || 'user', USER: process.env.USER || process.env.USERNAME || 'user',
PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter), PATH: [process.env.PATH || '', ...pathAdditions].filter(Boolean).join(path.delimiter),
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
}; };
return spawnEnv; return spawnEnv;
+1 -1
View File
@@ -22,7 +22,7 @@ function getIconPath(): string {
// Use PNG in dev mode (works better), ICNS in production // Use PNG in dev mode (works better), ICNS in production
iconName = is.dev ? 'icon-256.png' : 'icon.icns'; iconName = is.dev ? 'icon-256.png' : 'icon.icns';
} else if (process.platform === 'win32') { } else if (process.platform === 'win32') {
iconName = 'icon-256.png'; iconName = 'icon.ico';
} else { } else {
iconName = 'icon.png'; iconName = 'icon.png';
} }
+2 -1
View File
@@ -105,7 +105,8 @@ export class InsightsConfig {
...autoBuildEnv, ...autoBuildEnv,
...profileEnv, ...profileEnv,
PYTHONUNBUFFERED: '1', PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8' PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
}; };
} }
} }
@@ -9,13 +9,16 @@ import type {
ProjectSettings, ProjectSettings,
IPCResult, IPCResult,
InitializationResult, InitializationResult,
AutoBuildVersionInfo AutoBuildVersionInfo,
GitStatus
} from '../../shared/types'; } from '../../shared/types';
import { projectStore } from '../project-store'; import { projectStore } from '../project-store';
import { import {
initializeProject, initializeProject,
isInitialized, isInitialized,
hasLocalSource hasLocalSource,
checkGitStatus,
initializeGit
} from '../project-initializer'; } from '../project-initializer';
import { PythonEnvManager, type PythonEnvStatus } from '../python-env-manager'; import { PythonEnvManager, type PythonEnvStatus } from '../python-env-manager';
import { AgentManager } from '../agent'; import { AgentManager } from '../agent';
@@ -505,4 +508,42 @@ export function registerProjectHandlers(
} }
} }
); );
// Check git status for a project (is it a repo? has commits?)
ipcMain.handle(
IPC_CHANNELS.GIT_CHECK_STATUS,
async (_, projectPath: string): Promise<IPCResult<GitStatus>> => {
try {
if (!existsSync(projectPath)) {
return { success: false, error: 'Directory does not exist' };
}
const gitStatus = checkGitStatus(projectPath);
return { success: true, data: gitStatus };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
);
// Initialize git in a project (run git init and create initial commit)
ipcMain.handle(
IPC_CHANNELS.GIT_INITIALIZE,
async (_, projectPath: string): Promise<IPCResult<InitializationResult>> => {
try {
if (!existsSync(projectPath)) {
return { success: false, error: 'Directory does not exist' };
}
const result = initializeGit(projectPath);
return { success: result.success, data: result, error: result.error };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
);
} }
@@ -6,6 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { AgentManager } from '../../agent'; import { AgentManager } from '../../agent';
import { fileWatcher } from '../../file-watcher'; import { fileWatcher } from '../../file-watcher';
import { findTaskAndProject } from './shared'; import { findTaskAndProject } from './shared';
import { checkGitStatus } from '../../project-initializer';
/** /**
* Register task execution handlers (start, stop, review, status management, recovery) * Register task execution handlers (start, stop, review, status management, recovery)
@@ -40,6 +41,27 @@ export function registerTaskExecutionHandlers(
return; return;
} }
// Check git status - Auto Claude requires git for worktree-based builds
const gitStatus = checkGitStatus(project.path);
if (!gitStatus.isGitRepo) {
console.log('[TASK_START] Project is not a git repository:', project.path);
mainWindow.webContents.send(
IPC_CHANNELS.TASK_ERROR,
taskId,
'Git repository required. Please run "git init" in your project directory. Auto Claude uses git worktrees for isolated builds.'
);
return;
}
if (!gitStatus.hasCommits) {
console.log('[TASK_START] Git repository has no commits:', project.path);
mainWindow.webContents.send(
IPC_CHANNELS.TASK_ERROR,
taskId,
'Git repository has no commits. Please make an initial commit first (git add . && git commit -m "Initial commit").'
);
return;
}
console.log('[TASK_START] Found task:', task.specId, 'status:', task.status, 'subtasks:', task.subtasks.length); console.log('[TASK_START] Found task:', task.specId, 'status:', task.status, 'subtasks:', task.subtasks.length);
// Start file watcher for this task // Start file watcher for this task
@@ -287,6 +309,21 @@ export function registerTaskExecutionHandlers(
// Auto-start task when status changes to 'in_progress' and no process is running // Auto-start task when status changes to 'in_progress' and no process is running
if (status === 'in_progress' && !agentManager.isRunning(taskId)) { if (status === 'in_progress' && !agentManager.isRunning(taskId)) {
const mainWindow = getMainWindow(); const mainWindow = getMainWindow();
// Check git status before auto-starting
const gitStatusCheck = checkGitStatus(project.path);
if (!gitStatusCheck.isGitRepo || !gitStatusCheck.hasCommits) {
console.log('[TASK_UPDATE_STATUS] Git check failed, cannot auto-start task');
if (mainWindow) {
mainWindow.webContents.send(
IPC_CHANNELS.TASK_ERROR,
taskId,
gitStatusCheck.error || 'Git repository with commits required to run tasks.'
);
}
return { success: false, error: gitStatusCheck.error || 'Git repository required' };
}
console.log('[TASK_UPDATE_STATUS] Auto-starting task:', taskId); console.log('[TASK_UPDATE_STATUS] Auto-starting task:', taskId);
// Start file watcher for this task // Start file watcher for this task
@@ -498,6 +535,23 @@ export function registerTaskExecutionHandlers(
// Auto-restart the task if requested // Auto-restart the task if requested
let autoRestarted = false; let autoRestarted = false;
if (autoRestart && project) { if (autoRestart && project) {
// Check git status before auto-restarting
const gitStatusForRestart = checkGitStatus(project.path);
if (!gitStatusForRestart.isGitRepo || !gitStatusForRestart.hasCommits) {
console.log('[Recovery] Git check failed, cannot auto-restart task');
// Recovery succeeded but we can't restart without git
return {
success: true,
data: {
taskId,
recovered: true,
newStatus,
message: `Task recovered but cannot restart: ${gitStatusForRestart.error || 'Git repository with commits required.'}`,
autoRestarted: false
}
};
}
try { try {
// Set status to in_progress for the restart // Set status to in_progress for the restart
newStatus = 'in_progress'; newStatus = 'in_progress';
@@ -315,7 +315,9 @@ export function registerWorktreeHandlers(
env: { env: {
...process.env, ...process.env,
...profileEnv, // Include active Claude profile OAuth token ...profileEnv, // Include active Claude profile OAuth token
PYTHONUNBUFFERED: '1' PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
}, },
stdio: ['ignore', 'pipe', 'pipe'] // Don't connect stdin to avoid blocking stdio: ['ignore', 'pipe', 'pipe'] // Don't connect stdin to avoid blocking
}); });
@@ -568,7 +570,7 @@ export function registerWorktreeHandlers(
return new Promise((resolve) => { return new Promise((resolve) => {
const previewProcess = spawn(pythonPath, args, { const previewProcess = spawn(pythonPath, args, {
cwd: sourcePath, cwd: sourcePath,
env: { ...process.env, ...previewProfileEnv, PYTHONUNBUFFERED: '1', DEBUG: 'true' } env: { ...process.env, ...previewProfileEnv, PYTHONUNBUFFERED: '1', PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', DEBUG: 'true' }
}); });
let stdout = ''; let stdout = '';
@@ -1,5 +1,6 @@
import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'fs'; import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from 'fs';
import path from 'path'; import path from 'path';
import { execSync } from 'child_process';
/** /**
* Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set * Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set
@@ -16,6 +17,137 @@ function debug(message: string, data?: Record<string, unknown>): void {
} }
} }
/**
* Git status information for a project
*/
export interface GitStatus {
isGitRepo: boolean;
hasCommits: boolean;
currentBranch: string | null;
error?: string;
}
/**
* Check if a directory is a git repository and has at least one commit
*/
export function checkGitStatus(projectPath: string): GitStatus {
try {
// Check if it's a git repository
execSync('git rev-parse --git-dir', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
} catch {
return {
isGitRepo: false,
hasCommits: false,
currentBranch: null,
error: 'Not a git repository. Please run "git init" to initialize git.'
};
}
// Check if there are any commits
let hasCommits = false;
try {
execSync('git rev-parse HEAD', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
hasCommits = true;
} catch {
// No commits yet
hasCommits = false;
}
// Get current branch
let currentBranch: string | null = null;
try {
currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
} catch {
// Branch detection failed
}
if (!hasCommits) {
return {
isGitRepo: true,
hasCommits: false,
currentBranch,
error: 'Git repository has no commits. Please make an initial commit first.'
};
}
return {
isGitRepo: true,
hasCommits: true,
currentBranch
};
}
/**
* Initialize git in a project directory and create an initial commit.
* This is a user-friendly way to set up git for non-technical users.
*/
export function initializeGit(projectPath: string): InitializationResult {
debug('initializeGit called', { projectPath });
// Check current git status
const status = checkGitStatus(projectPath);
try {
// Step 1: Initialize git if needed
if (!status.isGitRepo) {
debug('Initializing git repository');
execSync('git init', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
}
// Step 2: Check if there are files to commit
const statusOutput = execSync('git status --porcelain', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim();
// Step 3: If there are untracked/modified files, add and commit them
if (statusOutput || !status.hasCommits) {
debug('Adding files and creating initial commit');
// Add all files
execSync('git add -A', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
// Create initial commit
execSync('git commit -m "Initial commit" --allow-empty', {
cwd: projectPath,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
});
}
debug('Git initialization complete');
return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error during git initialization';
debug('Git initialization failed', { error: errorMessage });
return {
success: false,
error: errorMessage
};
}
}
/** /**
* Entries to add to .gitignore when initializing a project * Entries to add to .gitignore when initializing a project
*/ */
@@ -130,6 +262,10 @@ export function isInitialized(projectPath: string): boolean {
* *
* Creates .auto-claude/ with data directories (specs, ideation, insights, roadmap). * Creates .auto-claude/ with data directories (specs, ideation, insights, roadmap).
* The framework code runs from the source repo - only data is stored here. * The framework code runs from the source repo - only data is stored here.
*
* Requires:
* - Project directory must exist
* - Project must be a git repository with at least one commit
*/ */
export function initializeProject(projectPath: string): InitializationResult { export function initializeProject(projectPath: string): InitializationResult {
debug('initializeProject called', { projectPath }); debug('initializeProject called', { projectPath });
@@ -143,6 +279,16 @@ export function initializeProject(projectPath: string): InitializationResult {
}; };
} }
// Check git status - Auto Claude requires git for worktree-based builds
const gitStatus = checkGitStatus(projectPath);
if (!gitStatus.isGitRepo || !gitStatus.hasCommits) {
debug('Git check failed', { gitStatus });
return {
success: false,
error: gitStatus.error || 'Git repository required. Auto Claude uses git worktrees for isolated builds.'
};
}
// Check if already initialized // Check if already initialized
const dotAutoBuildPath = path.join(projectPath, '.auto-claude'); const dotAutoBuildPath = path.join(projectPath, '.auto-claude');
@@ -137,7 +137,8 @@ export class TerminalNameGenerator extends EventEmitter {
...autoBuildEnv, ...autoBuildEnv,
...profileEnv, // Include active Claude profile config ...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1', PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8' PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
} }
}); });
+2 -1
View File
@@ -136,7 +136,8 @@ export class TitleGenerator extends EventEmitter {
...autoBuildEnv, ...autoBuildEnv,
...profileEnv, // Include active Claude profile config ...profileEnv, // Include active Claude profile config
PYTHONUNBUFFERED: '1', PYTHONUNBUFFERED: '1',
PYTHONIOENCODING: 'utf-8' PYTHONIOENCODING: 'utf-8',
PYTHONUTF8: '1'
} }
}); });
+11 -2
View File
@@ -15,7 +15,8 @@ import type {
ClaudeAuthResult, ClaudeAuthResult,
InfrastructureStatus, InfrastructureStatus,
GraphitiValidationResult, GraphitiValidationResult,
GraphitiConnectionTestResult GraphitiConnectionTestResult,
GitStatus
} from '../../shared/types'; } from '../../shared/types';
export interface ProjectAPI { export interface ProjectAPI {
@@ -72,6 +73,8 @@ export interface ProjectAPI {
getGitBranches: (projectPath: string) => Promise<IPCResult<string[]>>; getGitBranches: (projectPath: string) => Promise<IPCResult<string[]>>;
getCurrentGitBranch: (projectPath: string) => Promise<IPCResult<string | null>>; getCurrentGitBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
detectMainBranch: (projectPath: string) => Promise<IPCResult<string | null>>; detectMainBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
checkGitStatus: (projectPath: string) => Promise<IPCResult<GitStatus>>;
initializeGit: (projectPath: string) => Promise<IPCResult<InitializationResult>>;
} }
export const createProjectAPI = (): ProjectAPI => ({ export const createProjectAPI = (): ProjectAPI => ({
@@ -180,5 +183,11 @@ export const createProjectAPI = (): ProjectAPI => ({
ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, projectPath), ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, projectPath),
detectMainBranch: (projectPath: string): Promise<IPCResult<string | null>> => detectMainBranch: (projectPath: string): Promise<IPCResult<string | null>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath) ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath),
checkGitStatus: (projectPath: string): Promise<IPCResult<GitStatus>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_CHECK_STATUS, projectPath),
initializeGit: (projectPath: string): Promise<IPCResult<InitializationResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.GIT_INITIALIZE, projectPath)
}); });
@@ -0,0 +1,206 @@
import { useState } from 'react';
import { GitBranch, Terminal, CheckCircle2, AlertCircle, Loader2, FolderGit2 } from 'lucide-react';
import { Button } from './ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from './ui/dialog';
import type { Project, GitStatus } from '../../shared/types';
interface GitSetupModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
project: Project | null;
gitStatus: GitStatus | null;
onGitInitialized: () => void;
onSkip?: () => void;
}
export function GitSetupModal({
open,
onOpenChange,
project,
gitStatus,
onGitInitialized,
onSkip
}: GitSetupModalProps) {
const [isInitializing, setIsInitializing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [step, setStep] = useState<'info' | 'initializing' | 'success'>('info');
const needsGitInit = gitStatus && !gitStatus.isGitRepo;
const needsCommit = gitStatus && gitStatus.isGitRepo && !gitStatus.hasCommits;
const handleInitializeGit = async () => {
if (!project) return;
setIsInitializing(true);
setError(null);
setStep('initializing');
try {
// Call the backend to initialize git
const result = await window.electronAPI.initializeGit(project.path);
if (result.success) {
setStep('success');
// Wait a moment to show success, then trigger callback
setTimeout(() => {
onGitInitialized();
onOpenChange(false);
setStep('info');
}, 1500);
} else {
setError(result.error || 'Failed to initialize git');
setStep('info');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize git');
setStep('info');
} finally {
setIsInitializing(false);
}
};
const handleSkip = () => {
onSkip?.();
onOpenChange(false);
};
const renderInfoStep = () => (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderGit2 className="h-5 w-5 text-primary" />
Git Repository Required
</DialogTitle>
<DialogDescription>
Auto Claude uses git to safely build features in isolated workspaces
</DialogDescription>
</DialogHeader>
<div className="py-4 space-y-4">
{/* Status indicator */}
<div className="rounded-lg bg-muted p-4">
<div className="flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-warning mt-0.5 shrink-0" />
<div className="space-y-1">
<p className="font-medium text-sm">
{needsGitInit
? 'This folder is not a git repository'
: 'Git repository has no commits'}
</p>
<p className="text-sm text-muted-foreground">
{needsGitInit
? 'Git needs to be initialized before Auto Claude can manage your code.'
: 'At least one commit is required for Auto Claude to create worktrees.'}
</p>
</div>
</div>
</div>
{/* What will happen */}
<div className="rounded-lg border border-border p-4">
<p className="font-medium text-sm mb-3">We'll set up git for you:</p>
<ul className="space-y-2">
{needsGitInit && (
<li className="flex items-center gap-2 text-sm text-muted-foreground">
<GitBranch className="h-4 w-4 text-primary" />
Initialize a new git repository
</li>
)}
<li className="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle2 className="h-4 w-4 text-primary" />
Create an initial commit with your current files
</li>
</ul>
</div>
{/* Manual instructions for advanced users */}
<details className="text-sm">
<summary className="cursor-pointer text-muted-foreground hover:text-foreground">
Prefer to do it manually?
</summary>
<div className="mt-3 rounded-lg bg-muted/50 p-3 font-mono text-xs space-y-1">
<p className="text-muted-foreground">Open a terminal in your project folder and run:</p>
{needsGitInit && <p>git init</p>}
<p>git add .</p>
<p>git commit -m "Initial commit"</p>
</div>
</details>
{error && (
<div className="rounded-lg bg-destructive/10 border border-destructive/20 p-3 text-sm text-destructive">
{error}
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleSkip}>
Skip for now
</Button>
<Button onClick={handleInitializeGit} disabled={isInitializing}>
<GitBranch className="mr-2 h-4 w-4" />
Initialize Git
</Button>
</DialogFooter>
</>
);
const renderInitializingStep = () => (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin text-primary" />
Setting up Git
</DialogTitle>
</DialogHeader>
<div className="py-8 flex flex-col items-center justify-center">
<div className="space-y-3 text-center">
<Terminal className="h-12 w-12 text-muted-foreground mx-auto" />
<p className="text-sm text-muted-foreground">
Initializing git repository and creating initial commit...
</p>
</div>
</div>
</>
);
const renderSuccessStep = () => (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-success" />
Git Initialized
</DialogTitle>
</DialogHeader>
<div className="py-8 flex flex-col items-center justify-center">
<div className="space-y-3 text-center">
<div className="h-16 w-16 rounded-full bg-success/10 flex items-center justify-center mx-auto">
<CheckCircle2 className="h-8 w-8 text-success" />
</div>
<p className="text-sm text-muted-foreground">
Your project is now ready to use with Auto Claude!
</p>
</div>
</div>
</>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
{step === 'info' && renderInfoStep()}
{step === 'initializing' && renderInitializingStep()}
{step === 'success' && renderSuccessStep()}
</DialogContent>
</Dialog>
);
}
@@ -55,8 +55,9 @@ import {
} from '../stores/project-store'; } from '../stores/project-store';
import { useSettingsStore, saveSettings } from '../stores/settings-store'; import { useSettingsStore, saveSettings } from '../stores/settings-store';
import { AddProjectModal } from './AddProjectModal'; import { AddProjectModal } from './AddProjectModal';
import { GitSetupModal } from './GitSetupModal';
import { RateLimitIndicator } from './RateLimitIndicator'; import { RateLimitIndicator } from './RateLimitIndicator';
import type { Project, AutoBuildVersionInfo } from '../../shared/types'; import type { Project, AutoBuildVersionInfo, GitStatus } from '../../shared/types';
export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'github-issues' | 'changelog' | 'insights' | 'worktrees' | 'agent-tools' | 'agent-profiles'; export type SidebarView = 'kanban' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'github-issues' | 'changelog' | 'insights' | 'worktrees' | 'agent-tools' | 'agent-profiles';
@@ -104,6 +105,8 @@ export function Sidebar({
const [showAddProjectModal, setShowAddProjectModal] = useState(false); const [showAddProjectModal, setShowAddProjectModal] = useState(false);
const [showInitDialog, setShowInitDialog] = useState(false); const [showInitDialog, setShowInitDialog] = useState(false);
const [showUpdateDialog, setShowUpdateDialog] = useState(false); const [showUpdateDialog, setShowUpdateDialog] = useState(false);
const [showGitSetupModal, setShowGitSetupModal] = useState(false);
const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
const [pendingProject, setPendingProject] = useState<Project | null>(null); const [pendingProject, setPendingProject] = useState<Project | null>(null);
const [versionInfo, setVersionInfo] = useState<AutoBuildVersionInfo | null>(null); const [versionInfo, setVersionInfo] = useState<AutoBuildVersionInfo | null>(null);
const [isInitializing, setIsInitializing] = useState(false); const [isInitializing, setIsInitializing] = useState(false);
@@ -159,6 +162,29 @@ export function Sidebar({
checkUpdates(); checkUpdates();
}, [selectedProjectId, settings.autoUpdateAutoBuild]); }, [selectedProjectId, settings.autoUpdateAutoBuild]);
// Check git status when project changes
useEffect(() => {
const checkGit = async () => {
if (selectedProject) {
try {
const result = await window.electronAPI.checkGitStatus(selectedProject.path);
if (result.success && result.data) {
setGitStatus(result.data);
// Show git setup modal if project is not a git repo or has no commits
if (!result.data.isGitRepo || !result.data.hasCommits) {
setShowGitSetupModal(true);
}
}
} catch (error) {
console.error('Failed to check git status:', error);
}
} else {
setGitStatus(null);
}
};
checkGit();
}, [selectedProject]);
const handleAddProject = () => { const handleAddProject = () => {
setShowAddProjectModal(true); setShowAddProjectModal(true);
}; };
@@ -210,6 +236,20 @@ export function Sidebar({
setVersionInfo(null); setVersionInfo(null);
}; };
const handleGitInitialized = async () => {
// Refresh git status after initialization
if (selectedProject) {
try {
const result = await window.electronAPI.checkGitStatus(selectedProject.path);
if (result.success && result.data) {
setGitStatus(result.data);
}
} catch (error) {
console.error('Failed to refresh git status:', error);
}
}
};
const handleRemoveProject = async (projectId: string, e: React.MouseEvent) => { const handleRemoveProject = async (projectId: string, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@@ -518,6 +558,15 @@ export function Sidebar({
onOpenChange={setShowAddProjectModal} onOpenChange={setShowAddProjectModal}
onProjectAdded={handleProjectAdded} onProjectAdded={handleProjectAdded}
/> />
{/* Git Setup Modal */}
<GitSetupModal
open={showGitSetupModal}
onOpenChange={setShowGitSetupModal}
project={selectedProject || null}
gitStatus={gitStatus}
onGitInitialized={handleGitInitialized}
/>
</TooltipProvider> </TooltipProvider>
); );
} }
@@ -84,5 +84,19 @@ export const projectMock = {
detectMainBranch: async () => ({ detectMainBranch: async () => ({
success: true, success: true,
data: 'main' data: 'main'
}),
checkGitStatus: async () => ({
success: true,
data: {
isGitRepo: true,
hasCommits: true,
currentBranch: 'main'
}
}),
initializeGit: async () => ({
success: true,
data: { success: true }
}) })
}; };
@@ -258,6 +258,8 @@ export const IPC_CHANNELS = {
GIT_GET_BRANCHES: 'git:getBranches', GIT_GET_BRANCHES: 'git:getBranches',
GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch', GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch',
GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch', GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch',
GIT_CHECK_STATUS: 'git:checkStatus',
GIT_INITIALIZE: 'git:initialize',
// App auto-update operations // App auto-update operations
APP_UPDATE_CHECK: 'app-update:check', APP_UPDATE_CHECK: 'app-update:check',
-2
View File
@@ -15,8 +15,6 @@ export * from './changelog';
export * from './insights'; export * from './insights';
export * from './roadmap'; export * from './roadmap';
export * from './integrations'; export * from './integrations';
export * from './glossary';
export * from './status';
export * from './app-update'; export * from './app-update';
// IPC types (must be last to use types from other modules) // IPC types (must be last to use types from other modules)
+4 -1
View File
@@ -18,7 +18,8 @@ import type {
ProjectEnvConfig, ProjectEnvConfig,
InfrastructureStatus, InfrastructureStatus,
GraphitiValidationResult, GraphitiValidationResult,
GraphitiConnectionTestResult GraphitiConnectionTestResult,
GitStatus
} from './project'; } from './project';
import type { import type {
Task, Task,
@@ -500,6 +501,8 @@ export interface ElectronAPI {
getGitBranches: (projectPath: string) => Promise<IPCResult<string[]>>; getGitBranches: (projectPath: string) => Promise<IPCResult<string[]>>;
getCurrentGitBranch: (projectPath: string) => Promise<IPCResult<string | null>>; getCurrentGitBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
detectMainBranch: (projectPath: string) => Promise<IPCResult<string | null>>; detectMainBranch: (projectPath: string) => Promise<IPCResult<string | null>>;
checkGitStatus: (projectPath: string) => Promise<IPCResult<GitStatus>>;
initializeGit: (projectPath: string) => Promise<IPCResult<InitializationResult>>;
} }
declare global { declare global {
@@ -305,6 +305,13 @@ export interface InitializationResult {
error?: string; error?: string;
} }
export interface GitStatus {
isGitRepo: boolean;
hasCommits: boolean;
currentBranch: string | null;
error?: string;
}
export interface CreateProjectFolderResult { export interface CreateProjectFolderResult {
path: string; path: string;
name: string; name: string;
+15 -3
View File
@@ -79,11 +79,15 @@ class WorktreeManager:
cwd=self.project_dir, cwd=self.project_dir,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
) )
if result.returncode == 0: if result.returncode == 0:
return env_branch return env_branch
else: else:
print(f"Warning: DEFAULT_BRANCH '{env_branch}' not found, auto-detecting...") print(
f"Warning: DEFAULT_BRANCH '{env_branch}' not found, auto-detecting..."
)
# 2. Auto-detect main/master # 2. Auto-detect main/master
for branch in ["main", "master"]: for branch in ["main", "master"]:
@@ -92,15 +96,17 @@ class WorktreeManager:
cwd=self.project_dir, cwd=self.project_dir,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
) )
if result.returncode == 0: if result.returncode == 0:
return branch return branch
# 3. Fall back to current branch with warning # 3. Fall back to current branch with warning
current = self._get_current_branch() current = self._get_current_branch()
print(f"Warning: Could not find 'main' or 'master' branch.") print("Warning: Could not find 'main' or 'master' branch.")
print(f"Warning: Using current branch '{current}' as base for worktree.") print(f"Warning: Using current branch '{current}' as base for worktree.")
print(f"Tip: Set DEFAULT_BRANCH=your-branch in .env to avoid this.") print("Tip: Set DEFAULT_BRANCH=your-branch in .env to avoid this.")
return current return current
def _get_current_branch(self) -> str: def _get_current_branch(self) -> str:
@@ -110,6 +116,8 @@ class WorktreeManager:
cwd=self.project_dir, cwd=self.project_dir,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
) )
if result.returncode != 0: if result.returncode != 0:
raise WorktreeError(f"Failed to get current branch: {result.stderr}") raise WorktreeError(f"Failed to get current branch: {result.stderr}")
@@ -124,6 +132,8 @@ class WorktreeManager:
cwd=cwd or self.project_dir, cwd=cwd or self.project_dir,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
) )
def _unstage_gitignored_files(self) -> None: def _unstage_gitignored_files(self) -> None:
@@ -153,6 +163,8 @@ class WorktreeManager:
input="\n".join(staged_files), input="\n".join(staged_files),
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8",
errors="replace",
) )
if result.stdout.strip(): if result.stdout.strip():
+3 -3
View File
@@ -105,8 +105,8 @@ class ImplementationPlan:
self.update_status_from_subtasks() self.update_status_from_subtasks()
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f: with open(path, "w", encoding="utf-8") as f:
json.dump(self.to_dict(), f, indent=2) json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
def update_status_from_subtasks(self): def update_status_from_subtasks(self):
"""Update overall status and planStatus based on subtask completion state. """Update overall status and planStatus based on subtask completion state.
@@ -169,7 +169,7 @@ class ImplementationPlan:
@classmethod @classmethod
def load(cls, path: Path) -> "ImplementationPlan": def load(cls, path: Path) -> "ImplementationPlan":
"""Load plan from JSON file.""" """Load plan from JSON file."""
with open(path) as f: with open(path, encoding="utf-8") as f:
return cls.from_dict(json.load(f)) return cls.from_dict(json.load(f))
def get_available_phases(self) -> list[Phase]: def get_available_phases(self) -> list[Phase]:
+32
View File
@@ -28,6 +28,38 @@ Prerequisites:
- Claude Code CLI installed - Claude Code CLI installed
""" """
import io
import sys
# Configure safe encoding on Windows BEFORE any imports that might print
# This handles both TTY and piped output (e.g., from Electron)
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
from cli import main from cli import main
if __name__ == "__main__": if __name__ == "__main__":
+33 -1
View File
@@ -34,10 +34,40 @@ Usage:
""" """
import asyncio import asyncio
import io
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
# Configure safe encoding on Windows BEFORE any imports that might print
# This handles both TTY and piped output (e.g., from Electron)
if sys.platform == "win32":
for _stream_name in ("stdout", "stderr"):
_stream = getattr(sys, _stream_name)
# Method 1: Try reconfigure (works for TTY)
if hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Method 2: Wrap with TextIOWrapper for piped output
try:
if hasattr(_stream, "buffer"):
_new_stream = io.TextIOWrapper(
_stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, _stream_name, _new_stream)
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Clean up temporary variables
del _stream_name, _stream
if "_new_stream" in dir():
del _new_stream
# Add auto-claude to path (parent of runners/) # Add auto-claude to path (parent of runners/)
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
@@ -230,7 +260,9 @@ Examples:
print( print(
f" {muted('Or re-run spec_runner with --auto-approve to skip review:')}" f" {muted('Or re-run spec_runner with --auto-approve to skip review:')}"
) )
example_cmd = 'python auto-claude/spec_runner.py --task "..." --auto-approve' example_cmd = (
'python auto-claude/spec_runner.py --task "..." --auto-approve'
)
print(f" {highlight(example_cmd)}") print(f" {highlight(example_cmd)}")
sys.exit(1) sys.exit(1)
@@ -7,6 +7,11 @@ Handles the execution of AI agents for the spec creation pipeline.
from pathlib import Path from pathlib import Path
# Configure safe encoding before any output (fixes Windows encoding errors)
from ui.capabilities import configure_safe_encoding
configure_safe_encoding()
from client import create_client from client import create_client
from task_logger import ( from task_logger import (
LogEntryType, LogEntryType,
+4 -2
View File
@@ -457,9 +457,11 @@ class SpecOrchestrator:
print_key_value("Reasoning", assessment.reasoning) print_key_value("Reasoning", assessment.reasoning)
if assessment.needs_research: if assessment.needs_research:
print(f" {muted(' Research phase enabled')}") print(f" {muted(icon(Icons.ARROW_RIGHT) + ' Research phase enabled')}")
if assessment.needs_self_critique: if assessment.needs_self_critique:
print(f" {muted('→ Self-critique phase enabled')}") print(
f" {muted(icon(Icons.ARROW_RIGHT) + ' Self-critique phase enabled')}"
)
def _print_phases_to_run(self) -> None: def _print_phases_to_run(self) -> None:
"""Print the list of phases that will be executed.""" """Print the list of phases that will be executed."""
+3 -3
View File
@@ -30,16 +30,16 @@ class ValidationResult:
if self.errors: if self.errors:
lines.append("\nErrors:") lines.append("\nErrors:")
for err in self.errors: for err in self.errors:
lines.append(f" {err}") lines.append(f" [X] {err}")
if self.warnings: if self.warnings:
lines.append("\nWarnings:") lines.append("\nWarnings:")
for warn in self.warnings: for warn in self.warnings:
lines.append(f" {warn}") lines.append(f" [!] {warn}")
if self.fixes and not self.valid: if self.fixes and not self.valid:
lines.append("\nSuggested Fixes:") lines.append("\nSuggested Fixes:")
for fix in self.fixes: for fix in self.fixes:
lines.append(f" {fix}") lines.append(f" -> {fix}")
return "\n".join(lines) return "\n".join(lines)
+4 -4
View File
@@ -30,7 +30,7 @@ class LogStorage:
"""Load existing logs or create new structure.""" """Load existing logs or create new structure."""
if self.log_file.exists(): if self.log_file.exists():
try: try:
with open(self.log_file) as f: with open(self.log_file, encoding="utf-8") as f:
return json.load(f) return json.load(f)
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError):
pass pass
@@ -69,8 +69,8 @@ class LogStorage:
self._data["updated_at"] = self._timestamp() self._data["updated_at"] = self._timestamp()
try: try:
self.spec_dir.mkdir(parents=True, exist_ok=True) self.spec_dir.mkdir(parents=True, exist_ok=True)
with open(self.log_file, "w") as f: with open(self.log_file, "w", encoding="utf-8") as f:
json.dump(self._data, f, indent=2) json.dump(self._data, f, indent=2, ensure_ascii=False)
except OSError as e: except OSError as e:
print(f"Warning: Failed to save task logs: {e}", file=sys.stderr) print(f"Warning: Failed to save task logs: {e}", file=sys.stderr)
@@ -159,7 +159,7 @@ def load_task_logs(spec_dir: Path) -> dict | None:
return None return None
try: try:
with open(log_file) as f: with open(log_file, encoding="utf-8") as f:
return json.load(f) return json.load(f)
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError):
return None return None
+2
View File
@@ -21,6 +21,7 @@ from .capabilities import (
FANCY_UI, FANCY_UI,
INTERACTIVE, INTERACTIVE,
UNICODE, UNICODE,
configure_safe_encoding,
supports_color, supports_color,
supports_interactive, supports_interactive,
supports_unicode, supports_unicode,
@@ -57,6 +58,7 @@ _INTERACTIVE = INTERACTIVE
__all__ = [ __all__ = [
# Capabilities # Capabilities
"configure_safe_encoding",
"supports_unicode", "supports_unicode",
"supports_color", "supports_color",
"supports_interactive", "supports_interactive",
+46
View File
@@ -8,10 +8,56 @@ Detects terminal capabilities for:
- Interactive input support - Interactive input support
""" """
import io
import os import os
import sys import sys
def configure_safe_encoding() -> None:
"""
Configure stdout/stderr to handle Unicode safely on Windows.
On Windows, the default console encoding (cp1252) can't display many
Unicode characters. This function forces UTF-8 encoding with 'replace'
error handling, so unrenderable characters are replaced with '?' instead
of raising exceptions.
This handles both:
1. Regular console output (reconfigure method)
2. Piped output from subprocess (TextIOWrapper replacement)
"""
if sys.platform != "win32":
return
# Method 1: Try reconfigure (works for TTY)
for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name)
if hasattr(stream, "reconfigure"):
try:
stream.reconfigure(encoding="utf-8", errors="replace")
continue
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Method 2: Wrap with TextIOWrapper for piped output
# This is needed when stdout/stderr are pipes (e.g., from Electron)
try:
if hasattr(stream, "buffer"):
new_stream = io.TextIOWrapper(
stream.buffer,
encoding="utf-8",
errors="replace",
line_buffering=True,
)
setattr(sys, stream_name, new_stream)
except (AttributeError, io.UnsupportedOperation, OSError):
pass
# Configure safe encoding on module import
configure_safe_encoding()
def _is_fancy_ui_enabled() -> bool: def _is_fancy_ui_enabled() -> bool:
"""Check if fancy UI is enabled via environment variable.""" """Check if fancy UI is enabled via environment variable."""
value = os.environ.get("ENABLE_FANCY_UI", "true").lower() value = os.environ.get("ENABLE_FANCY_UI", "true").lower()