mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-20 15:44:25 +01:00
feat(desktop): Add auto-update functionality to Goose desktop app (#2852)
Co-authored-by: jack <jack@deck.local> Co-authored-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
@@ -44,6 +44,19 @@ if (process.env['APPLE_ID'] === undefined) {
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
packagerConfig: cfg,
|
packagerConfig: cfg,
|
||||||
rebuildConfig: {},
|
rebuildConfig: {},
|
||||||
|
publishers: [
|
||||||
|
{
|
||||||
|
name: '@electron-forge/publisher-github',
|
||||||
|
config: {
|
||||||
|
repository: {
|
||||||
|
owner: 'block',
|
||||||
|
name: 'goose'
|
||||||
|
},
|
||||||
|
prerelease: false,
|
||||||
|
draft: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
makers: [
|
makers: [
|
||||||
{
|
{
|
||||||
name: '@electron-forge/maker-zip',
|
name: '@electron-forge/maker-zip',
|
||||||
|
|||||||
79
ui/desktop/package-lock.json
generated
79
ui/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "goose-app",
|
"name": "goose-app",
|
||||||
"version": "1.0.27",
|
"version": "1.0.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "goose-app",
|
"name": "goose-app",
|
||||||
"version": "1.0.27",
|
"version": "1.0.20",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.72",
|
"@ai-sdk/openai": "^0.0.72",
|
||||||
@@ -30,11 +30,13 @@
|
|||||||
"ai": "^3.4.33",
|
"ai": "^3.4.33",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"compare-versions": "^6.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cronstrue": "^2.48.0",
|
"cronstrue": "^2.48.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"electron-log": "^5.2.2",
|
"electron-log": "^5.2.2",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
@@ -5048,7 +5050,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/aria-hidden": {
|
"node_modules/aria-hidden": {
|
||||||
@@ -5563,6 +5564,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/builder-util-runtime": {
|
||||||
|
"version": "9.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz",
|
||||||
|
"integrity": "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"sax": "^1.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -6140,6 +6154,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compare-versions": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -7139,6 +7159,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.6.2.tgz",
|
||||||
|
"integrity": "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"builder-util-runtime": "9.3.1",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lazy-val": "^1.0.5",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"semver": "^7.6.3",
|
||||||
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-window-state": {
|
"node_modules/electron-window-state": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/electron-window-state/-/electron-window-state-5.0.3.tgz",
|
||||||
@@ -8512,7 +8548,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -8966,7 +9001,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
@@ -10139,7 +10173,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -10254,7 +10287,6 @@
|
|||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
@@ -10299,6 +10331,12 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lazy-val": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -10703,6 +10741,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.get": {
|
"node_modules/lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
@@ -10711,6 +10755,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
@@ -14562,6 +14613,12 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sax": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
@@ -14581,7 +14638,6 @@
|
|||||||
"version": "7.7.2",
|
"version": "7.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
@@ -15825,6 +15881,12 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-typed-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
|
||||||
@@ -16290,7 +16352,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
|
|||||||
@@ -104,11 +104,13 @@
|
|||||||
"ai": "^3.4.33",
|
"ai": "^3.4.33",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"compare-versions": "^6.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cronstrue": "^2.48.0",
|
"cronstrue": "^2.48.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"electron-log": "^5.2.2",
|
"electron-log": "^5.2.2",
|
||||||
"electron-squirrel-startup": "^1.0.1",
|
"electron-squirrel-startup": "^1.0.1",
|
||||||
|
"electron-updater": "^6.6.2",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"framer-motion": "^11.11.11",
|
"framer-motion": "^11.11.11",
|
||||||
|
|||||||
53
ui/desktop/scripts/generate-update-icon.js
Normal file
53
ui/desktop/scripts/generate-update-icon.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const { createCanvas, loadImage } = require('canvas');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
async function generateUpdateIcon() {
|
||||||
|
// Load the original icon
|
||||||
|
const iconPath = path.join(__dirname, '../src/images/iconTemplate.png');
|
||||||
|
const icon = await loadImage(iconPath);
|
||||||
|
|
||||||
|
// Create canvas
|
||||||
|
const canvas = createCanvas(22, 22);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Draw the original icon
|
||||||
|
ctx.drawImage(icon, 0, 0);
|
||||||
|
|
||||||
|
// Add red dot in top-right corner
|
||||||
|
ctx.fillStyle = '#FF0000';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(18, 4, 3, 0, 2 * Math.PI);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// Save the new icon
|
||||||
|
const outputPath = path.join(__dirname, '../src/images/iconTemplateUpdate.png');
|
||||||
|
const buffer = canvas.toBuffer('image/png');
|
||||||
|
fs.writeFileSync(outputPath, buffer);
|
||||||
|
|
||||||
|
console.log('Generated update icon at:', outputPath);
|
||||||
|
|
||||||
|
// Also generate @2x version
|
||||||
|
const canvas2x = createCanvas(44, 44);
|
||||||
|
const ctx2x = canvas2x.getContext('2d');
|
||||||
|
|
||||||
|
// Load and draw @2x version
|
||||||
|
const icon2xPath = path.join(__dirname, '../src/images/iconTemplate@2x.png');
|
||||||
|
const icon2x = await loadImage(icon2xPath);
|
||||||
|
ctx2x.drawImage(icon2x, 0, 0);
|
||||||
|
|
||||||
|
// Add red dot in top-right corner (scaled)
|
||||||
|
ctx2x.fillStyle = '#FF0000';
|
||||||
|
ctx2x.beginPath();
|
||||||
|
ctx2x.arc(36, 8, 6, 0, 2 * Math.PI);
|
||||||
|
ctx2x.fill();
|
||||||
|
|
||||||
|
// Save the @2x version
|
||||||
|
const output2xPath = path.join(__dirname, '../src/images/iconTemplateUpdate@2x.png');
|
||||||
|
const buffer2x = canvas2x.toBuffer('image/png');
|
||||||
|
fs.writeFileSync(output2xPath, buffer2x);
|
||||||
|
|
||||||
|
console.log('Generated @2x update icon at:', output2xPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateUpdateIcon().catch(console.error);
|
||||||
@@ -302,8 +302,16 @@ export default function App() {
|
|||||||
console.log('Setting up view change handler');
|
console.log('Setting up view change handler');
|
||||||
const handleSetView = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
const handleSetView = (_event: IpcRendererEvent, ...args: unknown[]) => {
|
||||||
const newView = args[0] as View;
|
const newView = args[0] as View;
|
||||||
console.log(`Received view change request to: ${newView}`);
|
const section = args[1] as string | undefined;
|
||||||
setView(newView);
|
console.log(
|
||||||
|
`Received view change request to: ${newView}${section ? `, section: ${section}` : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (section && newView === 'settings') {
|
||||||
|
setView(newView, { section });
|
||||||
|
} else {
|
||||||
|
setView(newView);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const viewFromUrl = urlParams.get('view');
|
const viewFromUrl = urlParams.get('view');
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import MoreMenuLayout from '../more_menu/MoreMenuLayout';
|
|||||||
export type SettingsViewOptions = {
|
export type SettingsViewOptions = {
|
||||||
deepLinkConfig?: ExtensionConfig;
|
deepLinkConfig?: ExtensionConfig;
|
||||||
showEnvVars?: boolean;
|
showEnvVars?: boolean;
|
||||||
|
section?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingsView({
|
export default function SettingsView({
|
||||||
@@ -55,7 +56,7 @@ export default function SettingsView({
|
|||||||
{/* Tool Selection Strategy */}
|
{/* Tool Selection Strategy */}
|
||||||
<ToolSelectionStrategySection setView={setView} />
|
<ToolSelectionStrategySection setView={setView} />
|
||||||
{/* App Settings */}
|
{/* App Settings */}
|
||||||
<AppSettingsSection />
|
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Switch } from '../../ui/switch';
|
import { Switch } from '../../ui/switch';
|
||||||
|
import UpdateSection from './UpdateSection';
|
||||||
|
|
||||||
export default function AppSettingsSection() {
|
interface AppSettingsSectionProps {
|
||||||
|
scrollToSection?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppSettingsSection({ scrollToSection }: AppSettingsSectionProps) {
|
||||||
const [menuBarIconEnabled, setMenuBarIconEnabled] = useState(true);
|
const [menuBarIconEnabled, setMenuBarIconEnabled] = useState(true);
|
||||||
const [dockIconEnabled, setDockIconEnabled] = useState(true);
|
const [dockIconEnabled, setDockIconEnabled] = useState(true);
|
||||||
const [isMacOS, setIsMacOS] = useState(false);
|
const [isMacOS, setIsMacOS] = useState(false);
|
||||||
const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false);
|
const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false);
|
||||||
|
const updateSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Check if running on macOS
|
// Check if running on macOS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMacOS(window.electron.platform === 'darwin');
|
setIsMacOS(window.electron.platform === 'darwin');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Handle scrolling to update section
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollToSection === 'update' && updateSectionRef.current) {
|
||||||
|
// Use a timeout to ensure the DOM is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
updateSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [scrollToSection]);
|
||||||
|
|
||||||
// Load menu bar and dock icon states
|
// Load menu bar and dock icon states
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.getMenuBarIconState().then((enabled) => {
|
window.electron.getMenuBarIconState().then((enabled) => {
|
||||||
@@ -106,6 +122,11 @@ export default function AppSettingsSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Update Section */}
|
||||||
|
<div ref={updateSectionRef} className="mt-8 pt-8 border-t border-gray-200">
|
||||||
|
<UpdateSection />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
270
ui/desktop/src/components/settings/app/UpdateSection.tsx
Normal file
270
ui/desktop/src/components/settings/app/UpdateSection.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Loader2, Download, CheckCircle, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
type UpdateStatus =
|
||||||
|
| 'idle'
|
||||||
|
| 'checking'
|
||||||
|
| 'downloading'
|
||||||
|
| 'installing'
|
||||||
|
| 'success'
|
||||||
|
| 'error'
|
||||||
|
| 'ready';
|
||||||
|
|
||||||
|
interface UpdateInfo {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion?: string;
|
||||||
|
isUpdateAvailable?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateEventData {
|
||||||
|
version?: string;
|
||||||
|
percent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UpdateSection() {
|
||||||
|
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
|
||||||
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo>({
|
||||||
|
currentVersion: '',
|
||||||
|
});
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get current version on mount
|
||||||
|
const currentVersion = window.electron.getVersion();
|
||||||
|
setUpdateInfo((prev) => ({ ...prev, currentVersion }));
|
||||||
|
|
||||||
|
// Check if there's already an update state from the auto-check
|
||||||
|
window.electron.getUpdateState().then((state) => {
|
||||||
|
if (state) {
|
||||||
|
console.log('Found existing update state:', state);
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isUpdateAvailable: state.updateAvailable,
|
||||||
|
latestVersion: state.latestVersion,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for updater events
|
||||||
|
window.electron.onUpdaterEvent((event) => {
|
||||||
|
console.log('Updater event:', event);
|
||||||
|
|
||||||
|
switch (event.event) {
|
||||||
|
case 'checking-for-update':
|
||||||
|
setUpdateStatus('checking');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update-available':
|
||||||
|
setUpdateStatus('idle');
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
latestVersion: (event.data as UpdateEventData)?.version,
|
||||||
|
isUpdateAvailable: true,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update-not-available':
|
||||||
|
setUpdateStatus('idle');
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isUpdateAvailable: false,
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'download-progress':
|
||||||
|
setUpdateStatus('downloading');
|
||||||
|
setProgress((event.data as UpdateEventData)?.percent || 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update-downloaded':
|
||||||
|
setUpdateStatus('ready');
|
||||||
|
setProgress(100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
setUpdateStatus('error');
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: String(event.data || 'An error occurred'),
|
||||||
|
}));
|
||||||
|
setTimeout(() => setUpdateStatus('idle'), 5000);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkForUpdates = async () => {
|
||||||
|
setUpdateStatus('checking');
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.checkForUpdates();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we successfully checked and no update is available, show success
|
||||||
|
if (!result.error && updateInfo.isUpdateAvailable === false) {
|
||||||
|
setUpdateStatus('success');
|
||||||
|
setTimeout(() => setUpdateStatus('idle'), 3000);
|
||||||
|
}
|
||||||
|
// The actual status will be handled by the updater events
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for updates:', error);
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to check for updates',
|
||||||
|
}));
|
||||||
|
setUpdateStatus('error');
|
||||||
|
setTimeout(() => setUpdateStatus('idle'), 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadAndInstallUpdate = async () => {
|
||||||
|
setUpdateStatus('downloading');
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electron.downloadUpdate();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to download update');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The download progress and completion will be handled by updater events
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error downloading update:', error);
|
||||||
|
setUpdateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to download update',
|
||||||
|
}));
|
||||||
|
setUpdateStatus('error');
|
||||||
|
setTimeout(() => setUpdateStatus('idle'), 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const installUpdate = () => {
|
||||||
|
setUpdateStatus('installing');
|
||||||
|
// This will quit the app and install the update
|
||||||
|
window.electron.installUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusMessage = () => {
|
||||||
|
switch (updateStatus) {
|
||||||
|
case 'checking':
|
||||||
|
return 'Checking for updates...';
|
||||||
|
case 'downloading':
|
||||||
|
return `Downloading update... ${Math.round(progress)}%`;
|
||||||
|
case 'installing':
|
||||||
|
return 'Installing update...';
|
||||||
|
case 'ready':
|
||||||
|
return 'Update downloaded and ready to install!';
|
||||||
|
case 'success':
|
||||||
|
return updateInfo.isUpdateAvailable === false
|
||||||
|
? 'You are running the latest version!'
|
||||||
|
: 'Update available!';
|
||||||
|
case 'error':
|
||||||
|
return updateInfo.error || 'An error occurred';
|
||||||
|
default:
|
||||||
|
if (updateInfo.isUpdateAvailable) {
|
||||||
|
return `Version ${updateInfo.latestVersion} is available`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = () => {
|
||||||
|
switch (updateStatus) {
|
||||||
|
case 'checking':
|
||||||
|
case 'downloading':
|
||||||
|
case 'installing':
|
||||||
|
return <Loader2 className="w-4 h-4 animate-spin" />;
|
||||||
|
case 'success':
|
||||||
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
||||||
|
case 'error':
|
||||||
|
return <AlertCircle className="w-4 h-4 text-red-500" />;
|
||||||
|
case 'ready':
|
||||||
|
return <CheckCircle className="w-4 h-4 text-blue-500" />;
|
||||||
|
default:
|
||||||
|
return updateInfo.isUpdateAvailable ? <Download className="w-4 h-4" /> : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<h2 className="text-xl font-medium text-textStandard">Updates</h2>
|
||||||
|
</div>
|
||||||
|
<div className="pb-8">
|
||||||
|
<p className="text-sm text-textStandard mb-6">
|
||||||
|
Current version: {updateInfo.currentVersion || 'Loading...'}
|
||||||
|
{updateInfo.latestVersion && updateInfo.isUpdateAvailable && (
|
||||||
|
<span className="text-textSubtle"> → {updateInfo.latestVersion} available</span>
|
||||||
|
)}
|
||||||
|
{updateInfo.currentVersion && updateInfo.isUpdateAvailable === false && ' (up to date)'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={checkForUpdates}
|
||||||
|
disabled={updateStatus !== 'idle' && updateStatus !== 'error'}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
Check for Updates
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{updateInfo.isUpdateAvailable && updateStatus === 'idle' && (
|
||||||
|
<Button
|
||||||
|
onClick={downloadAndInstallUpdate}
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Download className="w-3 h-3 mr-1" />
|
||||||
|
Download Update
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateStatus === 'ready' && (
|
||||||
|
<Button onClick={installUpdate} variant="default" size="sm" className="text-xs">
|
||||||
|
Install & Restart
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{getStatusMessage() && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-textSubtle">
|
||||||
|
{getStatusIcon()}
|
||||||
|
<span>{getStatusMessage()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{updateStatus === 'downloading' && (
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Update information */}
|
||||||
|
{updateInfo.isUpdateAvailable && (
|
||||||
|
<div className="text-xs text-textSubtle mt-4 space-y-1">
|
||||||
|
<p>Update will be downloaded and automatically extracted to your Downloads folder.</p>
|
||||||
|
<p className="text-xs text-amber-600">
|
||||||
|
After download, move the Goose app to /Applications to complete the update.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ const buttonVariants = cva(
|
|||||||
default: 'bg-gray-800 text-white rounded-full px-6 py-2 hover:bg-gray-700',
|
default: 'bg-gray-800 text-white rounded-full px-6 py-2 hover:bg-gray-700',
|
||||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
'border border-gray-300 bg-background hover:bg-accent hover:text-accent-foreground',
|
'border border-gray-300 dark:border-gray-600 bg-background text-textStandard hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
|
|||||||
BIN
ui/desktop/src/images/iconTemplateUpdate.png
Normal file
BIN
ui/desktop/src/images/iconTemplateUpdate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 630 B |
BIN
ui/desktop/src/images/iconTemplateUpdate@2x.png
Normal file
BIN
ui/desktop/src/images/iconTemplateUpdate@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -35,6 +35,12 @@ import * as crypto from 'crypto';
|
|||||||
import * as electron from 'electron';
|
import * as electron from 'electron';
|
||||||
import * as yaml from 'yaml';
|
import * as yaml from 'yaml';
|
||||||
import windowStateKeeper from 'electron-window-state';
|
import windowStateKeeper from 'electron-window-state';
|
||||||
|
import {
|
||||||
|
setupAutoUpdater,
|
||||||
|
setTrayRef,
|
||||||
|
updateTrayMenu,
|
||||||
|
getUpdateAvailable,
|
||||||
|
} from './utils/autoUpdater';
|
||||||
|
|
||||||
// Define temp directory for pasted images
|
// Define temp directory for pasted images
|
||||||
const gooseTempDir = path.join(app.getPath('temp'), 'goose-pasted-images');
|
const gooseTempDir = path.join(app.getPath('temp'), 'goose-pasted-images');
|
||||||
@@ -339,7 +345,6 @@ const getVersion = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let [provider, model] = getGooseProvider();
|
let [provider, model] = getGooseProvider();
|
||||||
console.log('[main] Got provider and model:', { provider, model });
|
|
||||||
|
|
||||||
let sharingUrl = getSharingUrl();
|
let sharingUrl = getSharingUrl();
|
||||||
|
|
||||||
@@ -356,8 +361,6 @@ let appConfig = {
|
|||||||
secretKey: generateSecretKey(),
|
secretKey: generateSecretKey(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[main] Created appConfig:', appConfig);
|
|
||||||
|
|
||||||
// Track windows by ID
|
// Track windows by ID
|
||||||
let windowCounter = 0;
|
let windowCounter = 0;
|
||||||
const windowMap = new Map<number, BrowserWindow>();
|
const windowMap = new Map<number, BrowserWindow>();
|
||||||
@@ -512,8 +515,6 @@ const createChat = async (
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[main] Creating window with config:', windowConfig);
|
|
||||||
|
|
||||||
// Handle new window creation for links
|
// Handle new window creation for links
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
// Open all links in external browser
|
// Open all links in external browser
|
||||||
@@ -552,7 +553,6 @@ const createChat = async (
|
|||||||
} else {
|
} else {
|
||||||
// In production, we need to use a proper file protocol URL with correct base path
|
// In production, we need to use a proper file protocol URL with correct base path
|
||||||
const indexPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`);
|
const indexPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`);
|
||||||
console.log('Loading production path:', indexPath);
|
|
||||||
mainWindow.loadFile(indexPath, {
|
mainWindow.loadFile(indexPath, {
|
||||||
search: queryParams ? queryParams.slice(1) : undefined,
|
search: queryParams ? queryParams.slice(1) : undefined,
|
||||||
});
|
});
|
||||||
@@ -607,14 +607,11 @@ const createTray = () => {
|
|||||||
|
|
||||||
tray = new Tray(iconPath);
|
tray = new Tray(iconPath);
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
// Set tray reference for auto-updater
|
||||||
{ label: 'Show Window', click: showWindow },
|
setTrayRef(tray);
|
||||||
{ type: 'separator' },
|
|
||||||
{ label: 'Quit', click: () => app.quit() },
|
|
||||||
]);
|
|
||||||
|
|
||||||
tray.setToolTip('Goose');
|
// Initially build menu based on update status
|
||||||
tray.setContextMenu(contextMenu);
|
updateTrayMenu(getUpdateAvailable());
|
||||||
|
|
||||||
// On Windows, clicking the tray icon should show the window
|
// On Windows, clicking the tray icon should show the window
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@@ -1121,7 +1118,7 @@ ipcMain.handle('get-allowed-extensions', async () => {
|
|||||||
const createNewWindow = async (app: App, dir?: string | null) => {
|
const createNewWindow = async (app: App, dir?: string | null) => {
|
||||||
const recentDirs = loadRecentDirs();
|
const recentDirs = loadRecentDirs();
|
||||||
const openDir = dir || (recentDirs.length > 0 ? recentDirs[0] : undefined);
|
const openDir = dir || (recentDirs.length > 0 ? recentDirs[0] : undefined);
|
||||||
createChat(app, undefined, openDir);
|
return await createChat(app, undefined, openDir);
|
||||||
};
|
};
|
||||||
|
|
||||||
const focusWindow = () => {
|
const focusWindow = () => {
|
||||||
@@ -1159,6 +1156,9 @@ const registerGlobalHotkey = (accelerator: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
// Setup auto-updater
|
||||||
|
setupAutoUpdater();
|
||||||
|
|
||||||
// Add CSP headers to all sessions
|
// Add CSP headers to all sessions
|
||||||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
callback({
|
callback({
|
||||||
@@ -1173,7 +1173,7 @@ app.whenReady().then(async () => {
|
|||||||
// Images from our app and data: URLs (for base64 images)
|
// Images from our app and data: URLs (for base64 images)
|
||||||
"img-src 'self' data: https:;" +
|
"img-src 'self' data: https:;" +
|
||||||
// Connect to our local API and specific external services
|
// Connect to our local API and specific external services
|
||||||
"connect-src 'self' http://127.0.0.1:*" +
|
"connect-src 'self' http://127.0.0.1:* https://api.github.com https://github.com https://objects.githubusercontent.com" +
|
||||||
// Don't allow any plugins
|
// Don't allow any plugins
|
||||||
"object-src 'none';" +
|
"object-src 'none';" +
|
||||||
// Don't allow any frames
|
// Don't allow any frames
|
||||||
@@ -1227,7 +1227,7 @@ app.whenReady().then(async () => {
|
|||||||
// Parse command line arguments
|
// Parse command line arguments
|
||||||
const { dirPath } = parseArgs();
|
const { dirPath } = parseArgs();
|
||||||
|
|
||||||
createNewWindow(app, dirPath);
|
await createNewWindow(app, dirPath);
|
||||||
|
|
||||||
// Get the existing menu
|
// Get the existing menu
|
||||||
const menu = Menu.getApplicationMenu();
|
const menu = Menu.getApplicationMenu();
|
||||||
@@ -1423,7 +1423,7 @@ app.whenReady().then(async () => {
|
|||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createChat(app);
|
createNewWindow(app);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1598,6 +1598,17 @@ app.whenReady().then(async () => {
|
|||||||
console.error('Error opening URL in Chrome:', error);
|
console.error('Error opening URL in Chrome:', error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle app restart
|
||||||
|
ipcMain.on('restart-app', () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handler for getting app version
|
||||||
|
ipcMain.on('get-app-version', (event) => {
|
||||||
|
event.returnValue = app.getVersion();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ interface SaveDataUrlResponse {
|
|||||||
|
|
||||||
const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}');
|
const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}');
|
||||||
|
|
||||||
|
interface UpdaterEvent {
|
||||||
|
event: string;
|
||||||
|
data?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
// Define the API types in a single place
|
// Define the API types in a single place
|
||||||
type ElectronAPI = {
|
type ElectronAPI = {
|
||||||
platform: string;
|
platform: string;
|
||||||
@@ -76,6 +81,14 @@ type ElectronAPI = {
|
|||||||
deleteTempFile: (filePath: string) => void;
|
deleteTempFile: (filePath: string) => void;
|
||||||
// Function to serve temp images
|
// Function to serve temp images
|
||||||
getTempImage: (filePath: string) => Promise<string | null>;
|
getTempImage: (filePath: string) => Promise<string | null>;
|
||||||
|
// Update-related functions
|
||||||
|
getVersion: () => string;
|
||||||
|
checkForUpdates: () => Promise<{ updateInfo: unknown; error: string | null }>;
|
||||||
|
downloadUpdate: () => Promise<{ success: boolean; error: string | null }>;
|
||||||
|
installUpdate: () => void;
|
||||||
|
restartApp: () => void;
|
||||||
|
onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void;
|
||||||
|
getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppConfigAPI = {
|
type AppConfigAPI = {
|
||||||
@@ -149,6 +162,27 @@ const electronAPI: ElectronAPI = {
|
|||||||
getTempImage: (filePath: string): Promise<string | null> => {
|
getTempImage: (filePath: string): Promise<string | null> => {
|
||||||
return ipcRenderer.invoke('get-temp-image', filePath);
|
return ipcRenderer.invoke('get-temp-image', filePath);
|
||||||
},
|
},
|
||||||
|
getVersion: (): string => {
|
||||||
|
return config.GOOSE_VERSION || ipcRenderer.sendSync('get-app-version') || '';
|
||||||
|
},
|
||||||
|
checkForUpdates: (): Promise<{ updateInfo: unknown; error: string | null }> => {
|
||||||
|
return ipcRenderer.invoke('check-for-updates');
|
||||||
|
},
|
||||||
|
downloadUpdate: (): Promise<{ success: boolean; error: string | null }> => {
|
||||||
|
return ipcRenderer.invoke('download-update');
|
||||||
|
},
|
||||||
|
installUpdate: (): void => {
|
||||||
|
ipcRenderer.invoke('install-update');
|
||||||
|
},
|
||||||
|
restartApp: (): void => {
|
||||||
|
ipcRenderer.send('restart-app');
|
||||||
|
},
|
||||||
|
onUpdaterEvent: (callback: (event: UpdaterEvent) => void): void => {
|
||||||
|
ipcRenderer.on('updater-event', (_event, data) => callback(data));
|
||||||
|
},
|
||||||
|
getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => {
|
||||||
|
return ipcRenderer.invoke('get-update-state');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const appConfigAPI: AppConfigAPI = {
|
const appConfigAPI: AppConfigAPI = {
|
||||||
|
|||||||
498
ui/desktop/src/utils/autoUpdater.ts
Normal file
498
ui/desktop/src/utils/autoUpdater.ts
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
import { autoUpdater, UpdateInfo } from 'electron-updater';
|
||||||
|
import {
|
||||||
|
BrowserWindow,
|
||||||
|
ipcMain,
|
||||||
|
nativeImage,
|
||||||
|
Tray,
|
||||||
|
shell,
|
||||||
|
app,
|
||||||
|
dialog,
|
||||||
|
Menu,
|
||||||
|
MenuItemConstructorOptions,
|
||||||
|
} from 'electron';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import log from './logger';
|
||||||
|
import { githubUpdater } from './githubUpdater';
|
||||||
|
import { loadRecentDirs } from './recentDirs';
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
let trayRef: Tray | null = null;
|
||||||
|
let isUsingGitHubFallback = false;
|
||||||
|
let githubUpdateInfo: {
|
||||||
|
latestVersion?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
releaseUrl?: string;
|
||||||
|
downloadPath?: string;
|
||||||
|
extractedPath?: string;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Store update state
|
||||||
|
let lastUpdateState: { updateAvailable: boolean; latestVersion?: string } | null = null;
|
||||||
|
|
||||||
|
// Configure auto-updater
|
||||||
|
export function setupAutoUpdater(tray?: Tray) {
|
||||||
|
if (tray) {
|
||||||
|
trayRef = tray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the feed URL for GitHub releases
|
||||||
|
autoUpdater.setFeedURL({
|
||||||
|
provider: 'github',
|
||||||
|
owner: 'block',
|
||||||
|
repo: 'goose',
|
||||||
|
releaseType: 'release',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure auto-updater settings
|
||||||
|
autoUpdater.autoDownload = false; // We'll trigger downloads manually
|
||||||
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
|
||||||
|
// Enable updates in development mode for testing
|
||||||
|
if (process.env.ENABLE_DEV_UPDATES === 'true') {
|
||||||
|
autoUpdater.forceDevUpdateConfig = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set logger
|
||||||
|
autoUpdater.logger = log;
|
||||||
|
|
||||||
|
// Check for updates on startup
|
||||||
|
setTimeout(() => {
|
||||||
|
log.info('Checking for updates on startup...');
|
||||||
|
autoUpdater.checkForUpdates().catch((err) => {
|
||||||
|
log.error('Error checking for updates on startup:', err);
|
||||||
|
// If electron-updater fails, try GitHub API as fallback
|
||||||
|
if (
|
||||||
|
err.message.includes('HttpError: 404') ||
|
||||||
|
err.message.includes('ERR_CONNECTION_REFUSED') ||
|
||||||
|
err.message.includes('ENOTFOUND')
|
||||||
|
) {
|
||||||
|
log.info('Using GitHub API fallback for startup update check...');
|
||||||
|
isUsingGitHubFallback = true;
|
||||||
|
|
||||||
|
githubUpdater
|
||||||
|
.checkForUpdates()
|
||||||
|
.then((result) => {
|
||||||
|
if (result.error) {
|
||||||
|
sendStatusToWindow('error', result.error);
|
||||||
|
} else if (result.updateAvailable) {
|
||||||
|
// Store GitHub update info
|
||||||
|
githubUpdateInfo = {
|
||||||
|
latestVersion: result.latestVersion,
|
||||||
|
downloadUrl: result.downloadUrl,
|
||||||
|
releaseUrl: result.releaseUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateAvailable = true;
|
||||||
|
lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion };
|
||||||
|
updateTrayIcon(true);
|
||||||
|
sendStatusToWindow('update-available', { version: result.latestVersion });
|
||||||
|
} else {
|
||||||
|
updateAvailable = false;
|
||||||
|
lastUpdateState = { updateAvailable: false };
|
||||||
|
updateTrayIcon(false);
|
||||||
|
sendStatusToWindow('update-not-available', {
|
||||||
|
version: autoUpdater.currentVersion.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((fallbackError) => {
|
||||||
|
log.error('GitHub fallback also failed on startup:', fallbackError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000); // Wait 5 seconds after app starts
|
||||||
|
|
||||||
|
// Handle update events
|
||||||
|
autoUpdater.on('checking-for-update', () => {
|
||||||
|
log.info('Checking for update...');
|
||||||
|
sendStatusToWindow('checking-for-update');
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
||||||
|
log.info('Update available:', info);
|
||||||
|
updateAvailable = true;
|
||||||
|
lastUpdateState = { updateAvailable: true, latestVersion: info.version };
|
||||||
|
updateTrayIcon(true);
|
||||||
|
sendStatusToWindow('update-available', info);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-not-available', (info: UpdateInfo) => {
|
||||||
|
log.info('Update not available:', info);
|
||||||
|
updateAvailable = false;
|
||||||
|
lastUpdateState = { updateAvailable: false };
|
||||||
|
updateTrayIcon(false);
|
||||||
|
sendStatusToWindow('update-not-available', info);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('error', async (err) => {
|
||||||
|
log.error('Error in auto-updater:', err);
|
||||||
|
|
||||||
|
// Check if this is a 404 error (missing update files) or connection error
|
||||||
|
if (
|
||||||
|
err.message.includes('HttpError: 404') ||
|
||||||
|
err.message.includes('ERR_CONNECTION_REFUSED') ||
|
||||||
|
err.message.includes('ENOTFOUND')
|
||||||
|
) {
|
||||||
|
log.info('Falling back to GitHub API for update check...');
|
||||||
|
isUsingGitHubFallback = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await githubUpdater.checkForUpdates();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
sendStatusToWindow('error', result.error);
|
||||||
|
} else if (result.updateAvailable) {
|
||||||
|
// Store GitHub update info
|
||||||
|
githubUpdateInfo = {
|
||||||
|
latestVersion: result.latestVersion,
|
||||||
|
downloadUrl: result.downloadUrl,
|
||||||
|
releaseUrl: result.releaseUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateAvailable = true;
|
||||||
|
updateTrayIcon(true);
|
||||||
|
sendStatusToWindow('update-available', { version: result.latestVersion });
|
||||||
|
} else {
|
||||||
|
updateAvailable = false;
|
||||||
|
updateTrayIcon(false);
|
||||||
|
sendStatusToWindow('update-not-available', {
|
||||||
|
version: autoUpdater.currentVersion.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (fallbackError) {
|
||||||
|
log.error('GitHub fallback also failed:', fallbackError);
|
||||||
|
sendStatusToWindow(
|
||||||
|
'error',
|
||||||
|
'Unable to check for updates. Please check your internet connection.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendStatusToWindow('error', err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('download-progress', (progressObj) => {
|
||||||
|
let log_message = 'Download speed: ' + progressObj.bytesPerSecond;
|
||||||
|
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%';
|
||||||
|
log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')';
|
||||||
|
log.info(log_message);
|
||||||
|
sendStatusToWindow('download-progress', progressObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', (info: UpdateInfo) => {
|
||||||
|
log.info('Update downloaded:', info);
|
||||||
|
sendStatusToWindow('update-downloaded', info);
|
||||||
|
});
|
||||||
|
|
||||||
|
// IPC handlers for renderer process
|
||||||
|
ipcMain.handle('check-for-updates', async () => {
|
||||||
|
try {
|
||||||
|
// Reset fallback flag
|
||||||
|
isUsingGitHubFallback = false;
|
||||||
|
githubUpdateInfo = {};
|
||||||
|
|
||||||
|
// Ensure auto-updater is properly initialized
|
||||||
|
if (!autoUpdater.currentVersion) {
|
||||||
|
throw new Error('Auto-updater not initialized. Please restart the application.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await autoUpdater.checkForUpdates();
|
||||||
|
return {
|
||||||
|
updateInfo: result?.updateInfo,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error checking for updates:', error);
|
||||||
|
|
||||||
|
// If electron-updater fails, try GitHub API fallback
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
(error.message.includes('HttpError: 404') ||
|
||||||
|
error.message.includes('ERR_CONNECTION_REFUSED') ||
|
||||||
|
error.message.includes('ENOTFOUND'))
|
||||||
|
) {
|
||||||
|
log.info('Using GitHub API fallback in check-for-updates...');
|
||||||
|
isUsingGitHubFallback = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await githubUpdater.checkForUpdates();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return {
|
||||||
|
updateInfo: null,
|
||||||
|
error: result.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store GitHub update info
|
||||||
|
if (result.updateAvailable) {
|
||||||
|
githubUpdateInfo = {
|
||||||
|
latestVersion: result.latestVersion,
|
||||||
|
downloadUrl: result.downloadUrl,
|
||||||
|
releaseUrl: result.releaseUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
updateAvailable = true;
|
||||||
|
lastUpdateState = { updateAvailable: true, latestVersion: result.latestVersion };
|
||||||
|
updateTrayIcon(true);
|
||||||
|
sendStatusToWindow('update-available', { version: result.latestVersion });
|
||||||
|
} else {
|
||||||
|
updateAvailable = false;
|
||||||
|
lastUpdateState = { updateAvailable: false };
|
||||||
|
updateTrayIcon(false);
|
||||||
|
sendStatusToWindow('update-not-available', {
|
||||||
|
version: autoUpdater.currentVersion.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInfo: null,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (fallbackError) {
|
||||||
|
log.error('GitHub fallback also failed:', fallbackError);
|
||||||
|
return {
|
||||||
|
updateInfo: null,
|
||||||
|
error: 'Unable to check for updates. Please check your internet connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateInfo: null,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('download-update', async () => {
|
||||||
|
try {
|
||||||
|
if (isUsingGitHubFallback && githubUpdateInfo.downloadUrl && githubUpdateInfo.latestVersion) {
|
||||||
|
log.info('Using GitHub fallback for download...');
|
||||||
|
|
||||||
|
const result = await githubUpdater.downloadUpdate(
|
||||||
|
githubUpdateInfo.downloadUrl,
|
||||||
|
githubUpdateInfo.latestVersion,
|
||||||
|
(percent) => {
|
||||||
|
sendStatusToWindow('download-progress', { percent });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success && result.downloadPath) {
|
||||||
|
githubUpdateInfo.downloadPath = result.downloadPath;
|
||||||
|
githubUpdateInfo.extractedPath = result.extractedPath;
|
||||||
|
sendStatusToWindow('update-downloaded', { version: githubUpdateInfo.latestVersion });
|
||||||
|
return { success: true, error: null };
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Download failed');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use electron-updater
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
return { success: true, error: null };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error downloading update:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('install-update', async () => {
|
||||||
|
if (isUsingGitHubFallback) {
|
||||||
|
// For GitHub fallback, we need to handle the installation differently
|
||||||
|
log.info('Installing update from GitHub fallback...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the stored extracted path if available, otherwise download path
|
||||||
|
const updatePath = githubUpdateInfo.extractedPath || githubUpdateInfo.downloadPath;
|
||||||
|
|
||||||
|
if (!updatePath) {
|
||||||
|
throw new Error('Update file path not found. Please download the update first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the update path exists
|
||||||
|
try {
|
||||||
|
await fs.access(updatePath);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Update file not found. Please download the update first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dialog to inform user about manual installation
|
||||||
|
const isExtracted = !!githubUpdateInfo.extractedPath;
|
||||||
|
const dialogResult = (await dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
title: 'Update Ready',
|
||||||
|
message: isExtracted
|
||||||
|
? 'The update has been downloaded and extracted to your Downloads folder.'
|
||||||
|
: 'The update has been downloaded to your Downloads folder.',
|
||||||
|
detail: isExtracted
|
||||||
|
? `Please move the Goose app from ${path.basename(updatePath)} to your Applications folder to complete the update.`
|
||||||
|
: `Please extract ${path.basename(updatePath)} and move the Goose app to your Applications folder to complete the update.`,
|
||||||
|
buttons: ['Open Downloads', 'Cancel'],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
})) as unknown as { response: number };
|
||||||
|
|
||||||
|
if (dialogResult.response === 0) {
|
||||||
|
// Open the extracted folder or show the zip file
|
||||||
|
shell.showItemInFolder(updatePath);
|
||||||
|
|
||||||
|
// Optionally quit the app so user can replace it
|
||||||
|
setTimeout(() => {
|
||||||
|
app.quit();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error installing GitHub update:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use electron-updater's built-in install
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-current-version', () => {
|
||||||
|
return autoUpdater.currentVersion.version;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-update-state', () => {
|
||||||
|
return lastUpdateState;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdaterEvent {
|
||||||
|
event: string;
|
||||||
|
data?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendStatusToWindow(event: string, data?: unknown) {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
windows.forEach((win) => {
|
||||||
|
win.webContents.send('updater-event', { event, data } as UpdaterEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrayIcon(hasUpdate: boolean) {
|
||||||
|
if (!trayRef) return;
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
let iconPath: string;
|
||||||
|
|
||||||
|
if (hasUpdate) {
|
||||||
|
// Use icon with update indicator
|
||||||
|
if (isDev) {
|
||||||
|
iconPath = path.join(process.cwd(), 'src', 'images', 'iconTemplateUpdate.png');
|
||||||
|
} else {
|
||||||
|
iconPath = path.join(process.resourcesPath, 'images', 'iconTemplateUpdate.png');
|
||||||
|
}
|
||||||
|
trayRef.setToolTip('Goose - Update Available');
|
||||||
|
} else {
|
||||||
|
// Use normal icon
|
||||||
|
if (isDev) {
|
||||||
|
iconPath = path.join(process.cwd(), 'src', 'images', 'iconTemplate.png');
|
||||||
|
} else {
|
||||||
|
iconPath = path.join(process.resourcesPath, 'images', 'iconTemplate.png');
|
||||||
|
}
|
||||||
|
trayRef.setToolTip('Goose');
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = nativeImage.createFromPath(iconPath);
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
// Mark as template for macOS to handle dark/light mode
|
||||||
|
icon.setTemplateImage(true);
|
||||||
|
}
|
||||||
|
trayRef.setImage(icon);
|
||||||
|
|
||||||
|
// Update tray menu when icon changes
|
||||||
|
updateTrayMenu(hasUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to open settings and scroll to update section
|
||||||
|
function openUpdateSettings() {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
if (windows.length > 0) {
|
||||||
|
const mainWindow = windows[0];
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
// Send message to open settings and scroll to update section
|
||||||
|
mainWindow.webContents.send('set-view', 'settings', 'update');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export function to update tray menu
|
||||||
|
export function updateTrayMenu(hasUpdate: boolean) {
|
||||||
|
if (!trayRef) return;
|
||||||
|
|
||||||
|
const menuItems: MenuItemConstructorOptions[] = [];
|
||||||
|
|
||||||
|
// Add update menu item if update is available
|
||||||
|
if (hasUpdate) {
|
||||||
|
menuItems.push({
|
||||||
|
label: 'Update Available...',
|
||||||
|
click: openUpdateSettings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems.push(
|
||||||
|
{
|
||||||
|
label: 'Show Window',
|
||||||
|
click: async () => {
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
if (windows.length === 0) {
|
||||||
|
log.info('No windows are open, creating a new one...');
|
||||||
|
// Get recent directories for the new window
|
||||||
|
const recentDirs = loadRecentDirs();
|
||||||
|
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||||
|
|
||||||
|
// Emit event to create new window (handled in main.ts)
|
||||||
|
ipcMain.emit('create-chat-window', {}, undefined, openDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show all windows with offset
|
||||||
|
const initialOffsetX = 30;
|
||||||
|
const initialOffsetY = 30;
|
||||||
|
|
||||||
|
windows.forEach((win: BrowserWindow, index: number) => {
|
||||||
|
const currentBounds = win.getBounds();
|
||||||
|
const newX = currentBounds.x + initialOffsetX * index;
|
||||||
|
const newY = currentBounds.y + initialOffsetY * index;
|
||||||
|
|
||||||
|
win.setBounds({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
width: currentBounds.width,
|
||||||
|
height: currentBounds.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!win.isVisible()) {
|
||||||
|
win.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
win.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Quit', click: () => app.quit() }
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate(menuItems);
|
||||||
|
trayRef.setContextMenu(contextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions to manage tray reference
|
||||||
|
export function setTrayRef(tray: Tray) {
|
||||||
|
trayRef = tray;
|
||||||
|
// Update icon based on current update status
|
||||||
|
updateTrayIcon(updateAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUpdateAvailable(): boolean {
|
||||||
|
return updateAvailable;
|
||||||
|
}
|
||||||
260
ui/desktop/src/utils/githubUpdater.ts
Normal file
260
ui/desktop/src/utils/githubUpdater.ts
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import { compareVersions } from 'compare-versions';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import log from './logger';
|
||||||
|
|
||||||
|
interface GitHubRelease {
|
||||||
|
tag_name: string;
|
||||||
|
name: string;
|
||||||
|
published_at: string;
|
||||||
|
html_url: string;
|
||||||
|
assets: Array<{
|
||||||
|
name: string;
|
||||||
|
browser_download_url: string;
|
||||||
|
size: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateCheckResult {
|
||||||
|
updateAvailable: boolean;
|
||||||
|
latestVersion?: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
releaseUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GitHubUpdater {
|
||||||
|
private readonly owner = 'block';
|
||||||
|
private readonly repo = 'goose';
|
||||||
|
private readonly apiUrl = `https://api.github.com/repos/${this.owner}/${this.repo}/releases/latest`;
|
||||||
|
|
||||||
|
async checkForUpdates(): Promise<UpdateCheckResult> {
|
||||||
|
try {
|
||||||
|
log.info('GitHubUpdater: Checking for updates via GitHub API...');
|
||||||
|
|
||||||
|
const response = await fetch(this.apiUrl, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent': `Goose-Desktop/${app.getVersion()}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`GitHub API returned ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const release: GitHubRelease = await response.json();
|
||||||
|
const latestVersion = release.tag_name.replace(/^v/, ''); // Remove 'v' prefix if present
|
||||||
|
const currentVersion = app.getVersion();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`GitHubUpdater: Current version: ${currentVersion}, Latest version: ${latestVersion}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compare versions
|
||||||
|
const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
|
||||||
|
if (!updateAvailable) {
|
||||||
|
return {
|
||||||
|
updateAvailable: false,
|
||||||
|
latestVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the appropriate download URL based on platform
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
let downloadUrl: string | undefined;
|
||||||
|
let assetName: string;
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
if (arch === 'arm64') {
|
||||||
|
assetName = 'Goose.zip';
|
||||||
|
} else {
|
||||||
|
assetName = 'Goose_intel_mac.zip';
|
||||||
|
}
|
||||||
|
} else if (platform === 'win32') {
|
||||||
|
// Windows - for future support
|
||||||
|
assetName = 'Goose-win32-x64.zip';
|
||||||
|
} else {
|
||||||
|
// Linux - for future support
|
||||||
|
assetName = `Goose-linux-${arch}.zip`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = release.assets.find((a) => a.name === assetName);
|
||||||
|
if (asset) {
|
||||||
|
downloadUrl = asset.browser_download_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAvailable: true,
|
||||||
|
latestVersion,
|
||||||
|
downloadUrl,
|
||||||
|
releaseUrl: release.html_url,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error('GitHubUpdater: Error checking for updates:', error);
|
||||||
|
return {
|
||||||
|
updateAvailable: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadUpdate(
|
||||||
|
downloadUrl: string,
|
||||||
|
latestVersion: string,
|
||||||
|
onProgress?: (percent: number) => void
|
||||||
|
): Promise<{ success: boolean; downloadPath?: string; extractedPath?: string; error?: string }> {
|
||||||
|
try {
|
||||||
|
log.info(`GitHubUpdater: Downloading update from ${downloadUrl}`);
|
||||||
|
|
||||||
|
const response = await fetch(downloadUrl);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total size from headers
|
||||||
|
const contentLength = response.headers.get('content-length');
|
||||||
|
const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('Response body is null');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the response stream
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let downloadedSize = 0;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
chunks.push(value);
|
||||||
|
downloadedSize += value.length;
|
||||||
|
|
||||||
|
// Report progress
|
||||||
|
if (totalSize > 0 && onProgress) {
|
||||||
|
const percent = Math.round((downloadedSize / totalSize) * 100);
|
||||||
|
onProgress(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine chunks into a single buffer
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
||||||
|
|
||||||
|
// Save to Downloads directory
|
||||||
|
const downloadsDir = path.join(os.homedir(), 'Downloads');
|
||||||
|
const fileName = `Goose-${latestVersion}.zip`;
|
||||||
|
const downloadPath = path.join(downloadsDir, fileName);
|
||||||
|
|
||||||
|
await fs.writeFile(downloadPath, buffer);
|
||||||
|
|
||||||
|
log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`);
|
||||||
|
|
||||||
|
// Auto-unzip the downloaded file
|
||||||
|
try {
|
||||||
|
const tempExtractDir = path.join(downloadsDir, `temp-extract-${Date.now()}`);
|
||||||
|
|
||||||
|
// Create temp extraction directory
|
||||||
|
await fs.mkdir(tempExtractDir, { recursive: true });
|
||||||
|
|
||||||
|
// Use unzip command to extract
|
||||||
|
log.info(`GitHubUpdater: Extracting ${fileName} to temp directory`);
|
||||||
|
|
||||||
|
const unzipProcess = spawn('unzip', ['-o', downloadPath, '-d', tempExtractDir]);
|
||||||
|
|
||||||
|
let stderr = '';
|
||||||
|
unzipProcess.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
unzipProcess.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Unzip process exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
unzipProcess.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stderr && !stderr.includes('warning')) {
|
||||||
|
log.warn(`GitHubUpdater: Unzip stderr: ${stderr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Goose.app exists in the extracted content
|
||||||
|
const appPath = path.join(tempExtractDir, 'Goose.app');
|
||||||
|
try {
|
||||||
|
await fs.access(appPath);
|
||||||
|
log.info(`GitHubUpdater: Found Goose.app at ${appPath}`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('GitHubUpdater: Goose.app not found in extracted content');
|
||||||
|
throw new Error('Goose.app not found in extracted content');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move Goose.app to Downloads folder
|
||||||
|
const finalAppPath = path.join(downloadsDir, 'Goose.app');
|
||||||
|
|
||||||
|
// Remove existing Goose.app if it exists
|
||||||
|
try {
|
||||||
|
await fs.rm(finalAppPath, { recursive: true, force: true });
|
||||||
|
} catch (e) {
|
||||||
|
// File might not exist, that's fine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the app to Downloads
|
||||||
|
log.info(`GitHubUpdater: Moving Goose.app to Downloads folder`);
|
||||||
|
await fs.rename(appPath, finalAppPath);
|
||||||
|
|
||||||
|
// Verify the move was successful
|
||||||
|
try {
|
||||||
|
await fs.access(finalAppPath);
|
||||||
|
log.info(`GitHubUpdater: Successfully moved Goose.app to Downloads`);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('GitHubUpdater: Failed to move Goose.app');
|
||||||
|
throw new Error('Failed to move Goose.app to Downloads');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up temp directory and zip file
|
||||||
|
try {
|
||||||
|
await fs.rm(tempExtractDir, { recursive: true, force: true });
|
||||||
|
await fs.unlink(downloadPath);
|
||||||
|
log.info(`GitHubUpdater: Cleaned up temporary files`);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
log.warn(`GitHubUpdater: Failed to clean up temporary files: ${cleanupError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, downloadPath: finalAppPath, extractedPath: downloadsDir };
|
||||||
|
} catch (unzipError) {
|
||||||
|
log.error('GitHubUpdater: Error extracting update:', unzipError);
|
||||||
|
// Still return success for download, but note the extraction error
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
downloadPath,
|
||||||
|
error: `Downloaded successfully but extraction failed: ${unzipError instanceof Error ? unzipError.message : 'Unknown error'}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error('GitHubUpdater: Error downloading update:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create singleton instance
|
||||||
|
export const githubUpdater = new GitHubUpdater();
|
||||||
Reference in New Issue
Block a user