Add remote access controls
This commit is contained in:
207
package-lock.json
generated
207
package-lock.json
generated
@@ -2898,6 +2898,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/camelcase-css": {
|
"node_modules/camelcase-css": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||||
@@ -3393,6 +3402,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -3536,6 +3554,12 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dir-compare": {
|
"node_modules/dir-compare": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz",
|
||||||
@@ -4440,6 +4464,19 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -4660,7 +4697,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "6.* || 8.* || >= 10.*"
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
@@ -5626,6 +5662,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
@@ -6254,6 +6302,42 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/p-try": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
@@ -6273,6 +6357,15 @@
|
|||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-exists": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-is-absolute": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
@@ -6701,6 +6794,98 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@@ -6868,7 +7053,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6883,6 +7067,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -7238,6 +7428,12 @@
|
|||||||
"seroval": "^1.0"
|
"seroval": "^1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
@@ -8436,6 +8632,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@@ -8696,6 +8898,7 @@
|
|||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"lucide-solid": "^0.300.0",
|
"lucide-solid": "^0.300.0",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0"
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ export function setupCliIPC(mainWindow: BrowserWindow, cliManager: CliProcessMan
|
|||||||
|
|
||||||
ipcMain.handle("cli:getStatus", async () => cliManager.getStatus())
|
ipcMain.handle("cli:getStatus", async () => cliManager.getStatus())
|
||||||
|
|
||||||
|
ipcMain.handle("cli:restart", async () => {
|
||||||
|
const devMode = process.env.NODE_ENV === "development"
|
||||||
|
await cliManager.stop()
|
||||||
|
return cliManager.start({ dev: devMode })
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle("dialog:open", async (_, request: DialogOpenRequest): Promise<DialogOpenResult> => {
|
ipcMain.handle("dialog:open", async (_, request: DialogOpenRequest): Promise<DialogOpenResult> => {
|
||||||
const properties: OpenDialogOptions["properties"] =
|
const properties: OpenDialogOptions["properties"] =
|
||||||
request.mode === "directory" ? ["openDirectory", "createDirectory"] : ["openFile"]
|
request.mode === "directory" ? ["openDirectory", "createDirectory"] : ["openFile"]
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { spawn, type ChildProcess } from "child_process"
|
|||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import { createRequire } from "module"
|
import { createRequire } from "module"
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
import { existsSync } from "fs"
|
import { existsSync, readFileSync } from "fs"
|
||||||
|
import os from "os"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
|
import { buildUserShellCommand, getUserShellEnv, supportsUserShell } from "./user-shell"
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ const nodeRequire = createRequire(import.meta.url)
|
|||||||
|
|
||||||
|
|
||||||
type CliState = "starting" | "ready" | "error" | "stopped"
|
type CliState = "starting" | "ready" | "error" | "stopped"
|
||||||
|
type ListeningMode = "local" | "all"
|
||||||
|
|
||||||
export interface CliStatus {
|
export interface CliStatus {
|
||||||
state: CliState
|
state: CliState
|
||||||
@@ -34,6 +36,36 @@ interface CliEntryResolution {
|
|||||||
runnerPath?: string
|
runnerPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_PATH = "~/.config/codenomad/config.json"
|
||||||
|
|
||||||
|
function resolveConfigPath(configPath?: string): string {
|
||||||
|
const target = configPath && configPath.trim().length > 0 ? configPath : DEFAULT_CONFIG_PATH
|
||||||
|
if (target.startsWith("~/")) {
|
||||||
|
return path.join(os.homedir(), target.slice(2))
|
||||||
|
}
|
||||||
|
return path.resolve(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHostForMode(mode: ListeningMode): string {
|
||||||
|
return mode === "local" ? "127.0.0.1" : "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
function readListeningModeFromConfig(): ListeningMode {
|
||||||
|
try {
|
||||||
|
const configPath = resolveConfigPath(process.env.CLI_CONFIG)
|
||||||
|
if (!existsSync(configPath)) return "local"
|
||||||
|
const content = readFileSync(configPath, "utf-8")
|
||||||
|
const parsed = JSON.parse(content)
|
||||||
|
const mode = parsed?.preferences?.listeningMode
|
||||||
|
if (mode === "local" || mode === "all") {
|
||||||
|
return mode
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[cli] failed to read listening mode from config", error)
|
||||||
|
}
|
||||||
|
return "local"
|
||||||
|
}
|
||||||
|
|
||||||
export declare interface CliProcessManager {
|
export declare interface CliProcessManager {
|
||||||
on(event: "status", listener: (status: CliStatus) => void): this
|
on(event: "status", listener: (status: CliStatus) => void): this
|
||||||
on(event: "ready", listener: (status: CliStatus) => void): this
|
on(event: "ready", listener: (status: CliStatus) => void): this
|
||||||
@@ -58,10 +90,12 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
||||||
|
|
||||||
const cliEntry = this.resolveCliEntry(options)
|
const cliEntry = this.resolveCliEntry(options)
|
||||||
const args = this.buildCliArgs(options)
|
const listeningMode = this.resolveListeningMode()
|
||||||
|
const host = resolveHostForMode(listeningMode)
|
||||||
|
const args = this.buildCliArgs(options, host)
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) using ${cliEntry.runner} at ${cliEntry.entry}`,
|
`[cli] launching CodeNomad CLI (${options.dev ? "dev" : "prod"}) using ${cliEntry.runner} at ${cliEntry.entry} (host=${host})`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const env = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
const env = supportsUserShell() ? getUserShellEnv() : { ...process.env }
|
||||||
@@ -158,6 +192,10 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
return { ...this.status }
|
return { ...this.status }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveListeningMode(): ListeningMode {
|
||||||
|
return readListeningModeFromConfig()
|
||||||
|
}
|
||||||
|
|
||||||
private handleTimeout() {
|
private handleTimeout() {
|
||||||
if (this.child) {
|
if (this.child) {
|
||||||
this.child.kill("SIGKILL")
|
this.child.kill("SIGKILL")
|
||||||
@@ -232,8 +270,8 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
this.emit("status", this.status)
|
this.emit("status", this.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCliArgs(options: StartOptions): string[] {
|
private buildCliArgs(options: StartOptions, host: string): string[] {
|
||||||
const args = ["serve", "--host", "0.0.0.0", "--port", "0"]
|
const args = ["serve", "--host", host, "--port", "0"]
|
||||||
|
|
||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
args.push("--ui-dev-server", "http://localhost:3000", "--log-level", "debug")
|
args.push("--ui-dev-server", "http://localhost:3000", "--log-level", "debug")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const electronAPI = {
|
|||||||
return () => ipcRenderer.removeAllListeners("cli:error")
|
return () => ipcRenderer.removeAllListeners("cli:error")
|
||||||
},
|
},
|
||||||
getCliStatus: () => ipcRenderer.invoke("cli:getStatus"),
|
getCliStatus: () => ipcRenderer.invoke("cli:getStatus"),
|
||||||
|
restartCli: () => ipcRenderer.invoke("cli:restart"),
|
||||||
openDialog: (options) => ipcRenderer.invoke("dialog:open", options),
|
openDialog: (options) => ipcRenderer.invoke("dialog:open", options),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -180,15 +180,30 @@ export type WorkspaceEventPayload =
|
|||||||
| { type: "instance.event"; instanceId: string; event: InstanceStreamEvent }
|
| { type: "instance.event"; instanceId: string; event: InstanceStreamEvent }
|
||||||
| { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string }
|
| { type: "instance.eventStatus"; instanceId: string; status: InstanceStreamStatus; reason?: string }
|
||||||
|
|
||||||
|
export interface NetworkAddress {
|
||||||
|
ip: string
|
||||||
|
family: "ipv4" | "ipv6"
|
||||||
|
scope: "external" | "internal" | "loopback"
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerMeta {
|
export interface ServerMeta {
|
||||||
/** Base URL clients should target for REST calls (useful for Electron embedding). */
|
/** Base URL clients should target for REST calls (useful for Electron embedding). */
|
||||||
httpBaseUrl: string
|
httpBaseUrl: string
|
||||||
/** SSE endpoint advertised to clients (`/api/events` by default). */
|
/** SSE endpoint advertised to clients (`/api/events` by default). */
|
||||||
eventsUrl: string
|
eventsUrl: string
|
||||||
|
/** Host the server is bound to (e.g., 127.0.0.1 or 0.0.0.0). */
|
||||||
|
host: string
|
||||||
|
/** Listening mode derived from host binding. */
|
||||||
|
listeningMode: "local" | "all"
|
||||||
|
/** Actual port in use after binding. */
|
||||||
|
port: number
|
||||||
/** Display label for the host (e.g., hostname or friendly name). */
|
/** Display label for the host (e.g., hostname or friendly name). */
|
||||||
hostLabel: string
|
hostLabel: string
|
||||||
/** Absolute path of the filesystem root exposed to clients. */
|
/** Absolute path of the filesystem root exposed to clients. */
|
||||||
workspaceRoot: string
|
workspaceRoot: string
|
||||||
|
/** Reachable addresses for this server, external first. */
|
||||||
|
addresses: NetworkAddress[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const PreferencesSchema = z.object({
|
|||||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||||
showUsageMetrics: z.boolean().default(true),
|
showUsageMetrics: z.boolean().default(true),
|
||||||
autoCleanupBlankSessions: z.boolean().default(true),
|
autoCleanupBlankSessions: z.boolean().default(true),
|
||||||
|
listeningMode: z.enum(["local", "all"]).default("local"),
|
||||||
})
|
})
|
||||||
|
|
||||||
const RecentFolderSchema = z.object({
|
const RecentFolderSchema = z.object({
|
||||||
|
|||||||
@@ -141,8 +141,12 @@ async function main() {
|
|||||||
const serverMeta: ServerMeta = {
|
const serverMeta: ServerMeta = {
|
||||||
httpBaseUrl: `http://${options.host}:${options.port}`,
|
httpBaseUrl: `http://${options.host}:${options.port}`,
|
||||||
eventsUrl: `/api/events`,
|
eventsUrl: `/api/events`,
|
||||||
|
host: options.host,
|
||||||
|
listeningMode: options.host === "0.0.0.0" ? "all" : "local",
|
||||||
|
port: options.port,
|
||||||
hostLabel: options.host,
|
hostLabel: options.host,
|
||||||
workspaceRoot: options.rootDir,
|
workspaceRoot: options.rootDir,
|
||||||
|
addresses: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = createHttpServer({
|
const server = createHttpServer({
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
const serverUrl = `http://${displayHost}:${actualPort}`
|
const serverUrl = `http://${displayHost}:${actualPort}`
|
||||||
|
|
||||||
deps.serverMeta.httpBaseUrl = serverUrl
|
deps.serverMeta.httpBaseUrl = serverUrl
|
||||||
|
deps.serverMeta.host = deps.host
|
||||||
|
deps.serverMeta.port = actualPort
|
||||||
|
deps.serverMeta.listeningMode = deps.host === "0.0.0.0" ? "all" : "local"
|
||||||
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
deps.logger.info({ port: actualPort, host: deps.host }, "HTTP server listening")
|
||||||
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
console.log(`CodeNomad Server is ready at ${serverUrl}`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,102 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { ServerMeta } from "../../api-types"
|
import os from "os"
|
||||||
|
import { NetworkAddress, ServerMeta } from "../../api-types"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
serverMeta: ServerMeta
|
serverMeta: ServerMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
app.get("/api/meta", async () => deps.serverMeta)
|
app.get("/api/meta", async () => buildMetaResponse(deps.serverMeta))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
||||||
|
const port = resolvePort(meta)
|
||||||
|
const addresses = port > 0 ? resolveAddresses(port, meta.host) : []
|
||||||
|
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
port,
|
||||||
|
listeningMode: meta.host === "0.0.0.0" ? "all" : "local",
|
||||||
|
addresses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePort(meta: ServerMeta): number {
|
||||||
|
if (Number.isInteger(meta.port) && meta.port > 0) {
|
||||||
|
return meta.port
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = new URL(meta.httpBaseUrl)
|
||||||
|
const port = Number(parsed.port)
|
||||||
|
return Number.isInteger(port) && port > 0 ? port : 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAddresses(port: number, host: string): NetworkAddress[] {
|
||||||
|
const interfaces = os.networkInterfaces()
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const results: NetworkAddress[] = []
|
||||||
|
|
||||||
|
const addAddress = (ip: string, scope: NetworkAddress["scope"]) => {
|
||||||
|
if (!ip || ip === "0.0.0.0") return
|
||||||
|
const key = `ipv4-${ip}`
|
||||||
|
if (seen.has(key)) return
|
||||||
|
seen.add(key)
|
||||||
|
results.push({ ip, family: "ipv4", scope, url: `http://${ip}:${port}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeFamily = (value: string | number) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const lowered = value.toLowerCase()
|
||||||
|
if (lowered === "ipv4") {
|
||||||
|
return "ipv4" as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value === 4) return "ipv4" as const
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enumerate system interfaces (IPv4 only)
|
||||||
|
for (const entries of Object.values(interfaces)) {
|
||||||
|
if (!entries) continue
|
||||||
|
for (const entry of entries) {
|
||||||
|
const family = normalizeFamily(entry.family)
|
||||||
|
if (!family) continue
|
||||||
|
if (!entry.address || entry.address === "0.0.0.0") continue
|
||||||
|
const scope: NetworkAddress["scope"] = entry.internal ? "loopback" : "external"
|
||||||
|
addAddress(entry.address, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always include loopback address
|
||||||
|
addAddress("127.0.0.1", "loopback")
|
||||||
|
|
||||||
|
// Include explicitly configured host if it was IPv4
|
||||||
|
if (isIPv4Address(host) && host !== "0.0.0.0") {
|
||||||
|
const isLoopback = host.startsWith("127.")
|
||||||
|
addAddress(host, isLoopback ? "loopback" : "external")
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeWeight: Record<NetworkAddress["scope"], number> = { external: 0, internal: 1, loopback: 2 }
|
||||||
|
|
||||||
|
return results.sort((a, b) => {
|
||||||
|
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
||||||
|
if (scopeDelta !== 0) return scopeDelta
|
||||||
|
return a.ip.localeCompare(b.ip)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIPv4Address(value: string | undefined): value is string {
|
||||||
|
if (!value) return false
|
||||||
|
const parts = value.split(".")
|
||||||
|
if (parts.length !== 4) return false
|
||||||
|
return parts.every((part) => {
|
||||||
|
if (part.length === 0 || part.length > 3) return false
|
||||||
|
if (!/^[0-9]+$/.test(part)) return false
|
||||||
|
const num = Number(part)
|
||||||
|
return Number.isInteger(num) && num >= 0 && num <= 255
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
111
packages/tauri-app/Cargo.lock
generated
111
packages/tauri-app/Cargo.lock
generated
@@ -372,6 +372,7 @@ name = "codenomad-tauri"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"dirs 5.0.1",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -608,13 +609,34 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys 0.4.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs-sys",
|
"dirs-sys 0.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users 0.4.6",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -625,7 +647,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2804,6 +2826,17 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_users"
|
name = "redox_users"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -3530,7 +3563,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
"cookie",
|
"cookie",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dunce",
|
"dunce",
|
||||||
"embed_plist",
|
"embed_plist",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
@@ -3580,7 +3613,7 @@ checksum = "87d6f8cafe6a75514ce5333f115b7b1866e8e68d9672bf4ca89fc0f35697ea9d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"glob",
|
"glob",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@@ -4106,7 +4139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b"
|
checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"libappindicator",
|
"libappindicator",
|
||||||
"muda",
|
"muda",
|
||||||
"objc2 0.6.3",
|
"objc2 0.6.3",
|
||||||
@@ -4750,6 +4783,15 @@ dependencies = [
|
|||||||
"windows-targets 0.42.2",
|
"windows-targets 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -4792,6 +4834,21 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc 0.42.2",
|
"windows_x86_64_msvc 0.42.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.48.5",
|
||||||
|
"windows_aarch64_msvc 0.48.5",
|
||||||
|
"windows_i686_gnu 0.48.5",
|
||||||
|
"windows_i686_msvc 0.48.5",
|
||||||
|
"windows_x86_64_gnu 0.48.5",
|
||||||
|
"windows_x86_64_gnullvm 0.48.5",
|
||||||
|
"windows_x86_64_msvc 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4849,6 +4906,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4867,6 +4930,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4885,6 +4954,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4915,6 +4990,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4933,6 +5014,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4951,6 +5038,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -4969,6 +5062,12 @@ version = "0.42.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
@@ -5031,7 +5130,7 @@ dependencies = [
|
|||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs 6.0.0",
|
||||||
"dpi",
|
"dpi",
|
||||||
"dunce",
|
"dunce",
|
||||||
"gdkx11",
|
"gdkx11",
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ anyhow = "1"
|
|||||||
which = "4"
|
which = "4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
dirs = "5"
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
use dirs::home_dir;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::env;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::fs;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
@@ -41,6 +44,66 @@ fn navigate_main(app: &AppHandle, url: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct PreferencesConfig {
|
||||||
|
#[serde(rename = "listeningMode")]
|
||||||
|
listening_mode: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AppConfig {
|
||||||
|
preferences: Option<PreferencesConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_config_path() -> PathBuf {
|
||||||
|
let raw = env::var("CLI_CONFIG")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
|
||||||
|
expand_home(&raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expand_home(path: &str) -> PathBuf {
|
||||||
|
if path.starts_with("~/") {
|
||||||
|
if let Some(home) = home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from)) {
|
||||||
|
return home.join(path.trim_start_matches("~/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathBuf::from(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_listening_mode() -> String {
|
||||||
|
let path = resolve_config_path();
|
||||||
|
if let Ok(content) = fs::read_to_string(path) {
|
||||||
|
if let Ok(config) = serde_json::from_str::<AppConfig>(&content) {
|
||||||
|
if let Some(mode) = config
|
||||||
|
.preferences
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|prefs| prefs.listening_mode.as_ref())
|
||||||
|
{
|
||||||
|
if mode == "local" {
|
||||||
|
return "local".to_string();
|
||||||
|
}
|
||||||
|
if mode == "all" {
|
||||||
|
return "all".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"local".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_listening_host() -> String {
|
||||||
|
let mode = resolve_listening_mode();
|
||||||
|
if mode == "local" {
|
||||||
|
"127.0.0.1".to_string()
|
||||||
|
} else {
|
||||||
|
"0.0.0.0".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum CliState {
|
pub enum CliState {
|
||||||
@@ -178,11 +241,12 @@ impl CliProcessManager {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
log_line("resolving CLI entry");
|
log_line("resolving CLI entry");
|
||||||
let resolution = CliEntry::resolve(&app, dev)?;
|
let resolution = CliEntry::resolve(&app, dev)?;
|
||||||
|
let host = resolve_listening_host();
|
||||||
log_line(&format!(
|
log_line(&format!(
|
||||||
"resolved CLI entry runner={:?} entry={}",
|
"resolved CLI entry runner={:?} entry={} host={}",
|
||||||
resolution.runner, resolution.entry
|
resolution.runner, resolution.entry, host
|
||||||
));
|
));
|
||||||
let args = resolution.build_args(dev);
|
let args = resolution.build_args(dev, &host);
|
||||||
log_line(&format!("CLI args: {:?}", args));
|
log_line(&format!("CLI args: {:?}", args));
|
||||||
if dev {
|
if dev {
|
||||||
log_line("development mode: will prefer tsx + source if present");
|
log_line("development mode: will prefer tsx + source if present");
|
||||||
@@ -480,11 +544,11 @@ impl CliEntry {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_args(&self, dev: bool) -> Vec<String> {
|
fn build_args(&self, dev: bool, host: &str) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"serve".to_string(),
|
"serve".to_string(),
|
||||||
"--host".to_string(),
|
"--host".to_string(),
|
||||||
"0.0.0.0".to_string(),
|
host.to_string(),
|
||||||
"--port".to_string(),
|
"--port".to_string(),
|
||||||
"0".to_string(),
|
"0".to_string(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ fn cli_get_status(state: tauri::State<AppState>) -> CliStatus {
|
|||||||
state.manager.status()
|
state.manager.status()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn cli_restart(app: AppHandle, state: tauri::State<AppState>) -> Result<CliStatus, String> {
|
||||||
|
let dev_mode = is_dev_mode();
|
||||||
|
state.manager.stop().map_err(|e| e.to_string())?;
|
||||||
|
state
|
||||||
|
.manager
|
||||||
|
.start(app, dev_mode)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(state.manager.status())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_dev_mode() -> bool {
|
||||||
|
cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@@ -25,7 +40,7 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
build_menu(&app.handle())?;
|
build_menu(&app.handle())?;
|
||||||
let dev_mode = cfg!(debug_assertions) || std::env::var("TAURI_DEV").is_ok();
|
let dev_mode = is_dev_mode();
|
||||||
let app_handle = app.handle().clone();
|
let app_handle = app.handle().clone();
|
||||||
let manager = app.state::<AppState>().manager.clone();
|
let manager = app.state::<AppState>().manager.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@@ -38,7 +53,7 @@ fn main() {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![cli_get_status])
|
.invoke_handler(tauri::generate_handler![cli_get_status, cli_restart])
|
||||||
.on_menu_event(|_app_handle, _event| {
|
.on_menu_event(|_app_handle, _event| {
|
||||||
// No menu items defined currently
|
// No menu items defined currently
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"github-markdown-css": "^5.8.1",
|
"github-markdown-css": "^5.8.1",
|
||||||
"lucide-solid": "^0.300.0",
|
"lucide-solid": "^0.300.0",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
|
"qrcode": "^1.5.3",
|
||||||
"shiki": "^3.13.0",
|
"shiki": "^3.13.0",
|
||||||
"solid-js": "^1.8.0",
|
"solid-js": "^1.8.0",
|
||||||
"solid-toast": "^0.5.0"
|
"solid-toast": "^0.5.0"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { showConfirmDialog } from "./stores/alerts"
|
|||||||
import InstanceTabs from "./components/instance-tabs"
|
import InstanceTabs from "./components/instance-tabs"
|
||||||
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
||||||
import InstanceShell from "./components/instance/instance-shell"
|
import InstanceShell from "./components/instance/instance-shell"
|
||||||
|
import { RemoteAccessOverlay } from "./components/remote-access-overlay"
|
||||||
import { initMarkdown } from "./lib/markdown"
|
import { initMarkdown } from "./lib/markdown"
|
||||||
import { useTheme } from "./lib/theme"
|
import { useTheme } from "./lib/theme"
|
||||||
import { useCommands } from "./lib/hooks/use-commands"
|
import { useCommands } from "./lib/hooks/use-commands"
|
||||||
@@ -57,6 +58,7 @@ const App: Component = () => {
|
|||||||
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
const [escapeInDebounce, setEscapeInDebounce] = createSignal(false)
|
||||||
const [launchErrorBinary, setLaunchErrorBinary] = createSignal<string | null>(null)
|
const [launchErrorBinary, setLaunchErrorBinary] = createSignal<string | null>(null)
|
||||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false)
|
||||||
|
const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false)
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
void initMarkdown(isDark()).catch(console.error)
|
void initMarkdown(isDark()).catch(console.error)
|
||||||
@@ -284,6 +286,7 @@ const App: Component = () => {
|
|||||||
onSelect={setActiveInstanceId}
|
onSelect={setActiveInstanceId}
|
||||||
onClose={handleCloseInstance}
|
onClose={handleCloseInstance}
|
||||||
onNew={handleNewInstanceRequest}
|
onNew={handleNewInstanceRequest}
|
||||||
|
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Show when={activeInstance()} keyed>
|
<Show when={activeInstance()} keyed>
|
||||||
@@ -339,9 +342,12 @@ const App: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
|
||||||
|
|
||||||
<AlertDialog />
|
<AlertDialog />
|
||||||
|
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
||||||
position="top-right"
|
position="top-right"
|
||||||
gutter={16}
|
gutter={16}
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js"
|
|||||||
import type { Instance } from "../types/instance"
|
import type { Instance } from "../types/instance"
|
||||||
import InstanceTab from "./instance-tab"
|
import InstanceTab from "./instance-tab"
|
||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import { Plus } from "lucide-solid"
|
import { Plus, Share2 } from "lucide-solid"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
|
|
||||||
interface InstanceTabsProps {
|
interface InstanceTabsProps {
|
||||||
@@ -11,6 +11,7 @@ interface InstanceTabsProps {
|
|||||||
onSelect: (instanceId: string) => void
|
onSelect: (instanceId: string) => void
|
||||||
onClose: (instanceId: string) => void
|
onClose: (instanceId: string) => void
|
||||||
onNew: () => void
|
onNew: () => void
|
||||||
|
onOpenRemoteAccess?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
||||||
@@ -37,6 +38,16 @@ const InstanceTabs: Component<InstanceTabsProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Plus class="w-4 h-4" />
|
<Plus class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<Show when={Boolean(props.onOpenRemoteAccess)}>
|
||||||
|
<button
|
||||||
|
class="new-tab-button"
|
||||||
|
onClick={() => props.onOpenRemoteAccess?.()}
|
||||||
|
title="Remote access"
|
||||||
|
aria-label="Remote access"
|
||||||
|
>
|
||||||
|
<Share2 class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={Array.from(props.instances.entries()).length > 1}>
|
<Show when={Array.from(props.instances.entries()).length > 1}>
|
||||||
<div class="flex-shrink-0 ml-auto pl-4">
|
<div class="flex-shrink-0 ml-auto pl-4">
|
||||||
|
|||||||
236
packages/ui/src/components/remote-access-overlay.tsx
Normal file
236
packages/ui/src/components/remote-access-overlay.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
|
import { Switch } from "@kobalte/core/switch"
|
||||||
|
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
|
import { toDataURL } from "qrcode"
|
||||||
|
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
||||||
|
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
|
||||||
|
import { serverApi } from "../lib/api-client"
|
||||||
|
import { restartCli } from "../lib/native/cli"
|
||||||
|
import { preferences, setListeningMode } from "../stores/preferences"
|
||||||
|
import { showConfirmDialog } from "../stores/alerts"
|
||||||
|
|
||||||
|
interface RemoteAccessOverlayProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
||||||
|
const [meta, setMeta] = createSignal<ServerMeta | null>(null)
|
||||||
|
const [loading, setLoading] = createSignal(false)
|
||||||
|
const [qrCodes, setQrCodes] = createSignal<Record<string, string>>({})
|
||||||
|
const [expanded, setExpanded] = createSignal<Set<string>>(new Set())
|
||||||
|
const [error, setError] = createSignal<string | null>(null)
|
||||||
|
|
||||||
|
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
||||||
|
const currentMode = createMemo(() => meta()?.listeningMode ?? preferences().listeningMode)
|
||||||
|
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
||||||
|
|
||||||
|
const refreshMeta = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const result = await serverApi.fetchServerMeta()
|
||||||
|
setMeta(result)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.open) {
|
||||||
|
void refreshMeta()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleExpanded = async (url: string) => {
|
||||||
|
const next = new Set(expanded())
|
||||||
|
if (next.has(url)) {
|
||||||
|
next.delete(url)
|
||||||
|
setExpanded(next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.add(url)
|
||||||
|
setExpanded(next)
|
||||||
|
if (!qrCodes()[url]) {
|
||||||
|
try {
|
||||||
|
const dataUrl = await toDataURL(url, { margin: 1, scale: 4 })
|
||||||
|
setQrCodes((prev) => ({ ...prev, [url]: dataUrl }))
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to generate QR code", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAllowConnectionsChange = async (checked: boolean) => {
|
||||||
|
const allow = Boolean(checked)
|
||||||
|
const targetMode: "local" | "all" = allow ? "all" : "local"
|
||||||
|
if (targetMode === currentMode()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await showConfirmDialog("Restart to apply listening mode? This will stop all running instances.", {
|
||||||
|
title: allow ? "Open to other devices" : "Limit to this device",
|
||||||
|
variant: "warning",
|
||||||
|
confirmLabel: "Restart now",
|
||||||
|
cancelLabel: "Cancel",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
// Switch will revert automatically since `checked` is derived from store state
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setListeningMode(targetMode)
|
||||||
|
const restarted = await restartCli()
|
||||||
|
if (!restarted) {
|
||||||
|
setError("Unable to restart automatically. Please restart the app to apply the change.")
|
||||||
|
} else {
|
||||||
|
setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev))
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshMeta()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
window.open(url, "_blank", "noopener,noreferrer")
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to open URL", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={props.open}
|
||||||
|
modal
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
if (!nextOpen) {
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="modal-overlay remote-overlay-backdrop" />
|
||||||
|
<div class="remote-overlay">
|
||||||
|
<Dialog.Content class="modal-surface remote-panel" tabIndex={-1}>
|
||||||
|
<header class="remote-header">
|
||||||
|
<div>
|
||||||
|
<p class="remote-eyebrow">Remote access</p>
|
||||||
|
<h2 class="remote-title">Share this CodeNomad server</h2>
|
||||||
|
<p class="remote-subtitle">Choose who can connect and share ready-to-open links or QR codes.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="remote-close" onClick={props.onClose} aria-label="Close remote access">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="remote-body">
|
||||||
|
<section class="remote-section">
|
||||||
|
<div class="remote-section-heading">
|
||||||
|
<div class="remote-section-title">
|
||||||
|
<Shield class="remote-icon" />
|
||||||
|
<div>
|
||||||
|
<p class="remote-label">Listening mode</p>
|
||||||
|
<p class="remote-help">Toggle whether other devices on your network can reach this server.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="remote-refresh" type="button" onClick={() => void refreshMeta()} disabled={loading()}>
|
||||||
|
<RefreshCw class={`remote-icon ${loading() ? "remote-spin" : ""}`} />
|
||||||
|
Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
class="remote-toggle"
|
||||||
|
checked={allowExternalConnections()}
|
||||||
|
onChange={(nextChecked) => {
|
||||||
|
void handleAllowConnectionsChange(nextChecked)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch.Input />
|
||||||
|
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
||||||
|
<span class="remote-toggle-state">{allowExternalConnections() ? "On" : "Off"}</span>
|
||||||
|
<Switch.Thumb class="remote-toggle-thumb" />
|
||||||
|
</Switch.Control>
|
||||||
|
<div class="remote-toggle-copy">
|
||||||
|
<span class="remote-toggle-title">Allow connections from other IPs</span>
|
||||||
|
<span class="remote-toggle-caption">
|
||||||
|
{allowExternalConnections() ? "Binding to 0.0.0.0" : "Binding to 127.0.0.1"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Switch>
|
||||||
|
<p class="remote-toggle-note">
|
||||||
|
Changing this requires a restart and temporarily stops all active instances. Share the addresses below once the
|
||||||
|
server restarts.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="remote-section">
|
||||||
|
<div class="remote-section-heading">
|
||||||
|
<div class="remote-section-title">
|
||||||
|
<Wifi class="remote-icon" />
|
||||||
|
<div>
|
||||||
|
<p class="remote-label">Reachable addresses</p>
|
||||||
|
<p class="remote-help">Use these URLs to connect from this or other devices.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={!loading()} fallback={<div class="remote-card">Loading addresses…</div>}>
|
||||||
|
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||||
|
<Show when={addresses().length > 0} fallback={<div class="remote-card">No addresses available yet.</div>}>
|
||||||
|
<div class="remote-address-list">
|
||||||
|
<For each={addresses()}>
|
||||||
|
{(address) => {
|
||||||
|
const expandedState = () => expanded().has(address.url)
|
||||||
|
const qr = () => qrCodes()[address.url]
|
||||||
|
return (
|
||||||
|
<div class="remote-address">
|
||||||
|
<div class="remote-address-main">
|
||||||
|
<div>
|
||||||
|
<p class="remote-address-url">{address.url}</p>
|
||||||
|
<p class="remote-address-meta">
|
||||||
|
{address.family.toUpperCase()} • {address.scope === "external" ? "Network" : address.scope === "loopback" ? "Loopback" : "Internal"} • {address.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="remote-actions">
|
||||||
|
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(address.url)}>
|
||||||
|
<ExternalLink class="remote-icon" />
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="remote-pill"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void toggleExpanded(address.url)}
|
||||||
|
aria-expanded={expandedState()}
|
||||||
|
>
|
||||||
|
<Link2 class="remote-icon" />
|
||||||
|
{expandedState() ? "Hide QR" : "Show QR"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={expandedState()}>
|
||||||
|
<div class="remote-qr">
|
||||||
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
|
{(dataUrl) => <img src={dataUrl()} alt={`QR for ${address.url}`} class="remote-qr-img" />}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
packages/ui/src/lib/native/cli.ts
Normal file
28
packages/ui/src/lib/native/cli.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { runtimeEnv } from "../runtime-env"
|
||||||
|
|
||||||
|
export async function restartCli(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (runtimeEnv.host === "electron") {
|
||||||
|
const api = (window as typeof window & { electronAPI?: { restartCli?: () => Promise<unknown> } }).electronAPI
|
||||||
|
if (api?.restartCli) {
|
||||||
|
await api.restartCli()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtimeEnv.host === "tauri") {
|
||||||
|
const tauri = (window as typeof window & { __TAURI__?: { invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T> } }).__TAURI__
|
||||||
|
if (tauri?.invoke) {
|
||||||
|
await tauri.invoke("cli_restart")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to restart CLI", error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@ import { serverApi } from "./api-client"
|
|||||||
let cachedMeta: ServerMeta | null = null
|
let cachedMeta: ServerMeta | null = null
|
||||||
let pendingMeta: Promise<ServerMeta> | null = null
|
let pendingMeta: Promise<ServerMeta> | null = null
|
||||||
|
|
||||||
export async function getServerMeta(): Promise<ServerMeta> {
|
export async function getServerMeta(forceRefresh = false): Promise<ServerMeta> {
|
||||||
if (cachedMeta) {
|
if (cachedMeta && !forceRefresh) {
|
||||||
return cachedMeta
|
return cachedMeta
|
||||||
}
|
}
|
||||||
if (pendingMeta) {
|
if (pendingMeta) {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface AgentModelSelections {
|
|||||||
export type DiffViewMode = "split" | "unified"
|
export type DiffViewMode = "split" | "unified"
|
||||||
export type ExpansionPreference = "expanded" | "collapsed"
|
export type ExpansionPreference = "expanded" | "collapsed"
|
||||||
|
|
||||||
|
export type ListeningMode = "local" | "all"
|
||||||
|
|
||||||
export interface Preferences {
|
export interface Preferences {
|
||||||
showThinkingBlocks: boolean
|
showThinkingBlocks: boolean
|
||||||
thinkingBlocksExpansion: ExpansionPreference
|
thinkingBlocksExpansion: ExpansionPreference
|
||||||
@@ -37,10 +39,13 @@ export interface Preferences {
|
|||||||
toolOutputExpansion: ExpansionPreference
|
toolOutputExpansion: ExpansionPreference
|
||||||
diagnosticsExpansion: ExpansionPreference
|
diagnosticsExpansion: ExpansionPreference
|
||||||
showUsageMetrics: boolean
|
showUsageMetrics: boolean
|
||||||
autoCleanupBlankSessions?: boolean
|
autoCleanupBlankSessions: boolean
|
||||||
|
listeningMode: ListeningMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface OpenCodeBinary {
|
export interface OpenCodeBinary {
|
||||||
|
|
||||||
path: string
|
path: string
|
||||||
version?: string
|
version?: string
|
||||||
lastUsed: number
|
lastUsed: number
|
||||||
@@ -66,8 +71,10 @@ const defaultPreferences: Preferences = {
|
|||||||
diagnosticsExpansion: "expanded",
|
diagnosticsExpansion: "expanded",
|
||||||
showUsageMetrics: true,
|
showUsageMetrics: true,
|
||||||
autoCleanupBlankSessions: true,
|
autoCleanupBlankSessions: true,
|
||||||
|
listeningMode: "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deepEqual(a: unknown, b: unknown): boolean {
|
function deepEqual(a: unknown, b: unknown): boolean {
|
||||||
if (a === b) return true
|
if (a === b) return true
|
||||||
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
||||||
@@ -101,10 +108,12 @@ function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelectio
|
|||||||
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
|
diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
|
||||||
showUsageMetrics: sanitized.showUsageMetrics ?? defaultPreferences.showUsageMetrics,
|
showUsageMetrics: sanitized.showUsageMetrics ?? defaultPreferences.showUsageMetrics,
|
||||||
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultPreferences.autoCleanupBlankSessions,
|
autoCleanupBlankSessions: sanitized.autoCleanupBlankSessions ?? defaultPreferences.autoCleanupBlankSessions,
|
||||||
|
listeningMode: sanitized.listeningMode ?? defaultPreferences.listeningMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [internalConfig, setInternalConfig] = createSignal<ConfigData>(buildFallbackConfig())
|
const [internalConfig, setInternalConfig] = createSignal<ConfigData>(buildFallbackConfig())
|
||||||
|
|
||||||
const config = createMemo<DeepReadonly<ConfigData>>(() => internalConfig())
|
const config = createMemo<DeepReadonly<ConfigData>>(() => internalConfig())
|
||||||
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
|
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
|
||||||
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
|
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
|
||||||
@@ -260,6 +269,11 @@ function updatePreferences(updates: Partial<Preferences>): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setListeningMode(mode: ListeningMode): void {
|
||||||
|
if (preferences().listeningMode === mode) return
|
||||||
|
updatePreferences({ listeningMode: mode })
|
||||||
|
}
|
||||||
|
|
||||||
function setDiffViewMode(mode: DiffViewMode): void {
|
function setDiffViewMode(mode: DiffViewMode): void {
|
||||||
if (preferences().diffViewMode === mode) return
|
if (preferences().diffViewMode === mode) return
|
||||||
updatePreferences({ diffViewMode: mode })
|
updatePreferences({ diffViewMode: mode })
|
||||||
@@ -399,6 +413,7 @@ interface ConfigContextValue {
|
|||||||
setToolOutputExpansion: typeof setToolOutputExpansion
|
setToolOutputExpansion: typeof setToolOutputExpansion
|
||||||
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
setDiagnosticsExpansion: typeof setDiagnosticsExpansion
|
||||||
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
setThinkingBlocksExpansion: typeof setThinkingBlocksExpansion
|
||||||
|
setListeningMode: typeof setListeningMode
|
||||||
addRecentFolder: typeof addRecentFolder
|
addRecentFolder: typeof addRecentFolder
|
||||||
removeRecentFolder: typeof removeRecentFolder
|
removeRecentFolder: typeof removeRecentFolder
|
||||||
addOpenCodeBinary: typeof addOpenCodeBinary
|
addOpenCodeBinary: typeof addOpenCodeBinary
|
||||||
@@ -432,6 +447,7 @@ const configContextValue: ConfigContextValue = {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setListeningMode,
|
||||||
addRecentFolder,
|
addRecentFolder,
|
||||||
removeRecentFolder,
|
removeRecentFolder,
|
||||||
addOpenCodeBinary,
|
addOpenCodeBinary,
|
||||||
@@ -502,8 +518,11 @@ export {
|
|||||||
setToolOutputExpansion,
|
setToolOutputExpansion,
|
||||||
setDiagnosticsExpansion,
|
setDiagnosticsExpansion,
|
||||||
setThinkingBlocksExpansion,
|
setThinkingBlocksExpansion,
|
||||||
|
setListeningMode,
|
||||||
themePreference,
|
themePreference,
|
||||||
setThemePreference,
|
setThemePreference,
|
||||||
recordWorkspaceLaunch,
|
recordWorkspaceLaunch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
289
packages/ui/src/styles/components/remote-access.css
Normal file
289
packages/ui/src/styles/components/remote-access.css
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
.remote-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 41;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.remote-overlay-backdrop {
|
||||||
|
background: var(--overlay-scrim);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-panel {
|
||||||
|
width: min(960px, 100%);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-eyebrow {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-subtle);
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-subtitle {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-close {
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-body {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-section {
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-section-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-label {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-help {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-refresh {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
background: var(--surface-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
background: var(--surface-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-switch {
|
||||||
|
width: 58px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 8px 0 6px;
|
||||||
|
transition: background 0.2s ease, border-color 0.2s ease;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-state {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-primary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-switch[data-checked="true"] {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
color: var(--surface-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-switch[data-checked="true"] .remote-toggle-thumb {
|
||||||
|
transform: translateX(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-caption {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-toggle-note {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address {
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-url {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-meta {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-qr {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px dashed var(--border-base);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-qr-img {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-card {
|
||||||
|
border: 1px dashed var(--border-base);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-error {
|
||||||
|
border: 1px solid var(--border-critical, #e65c5c);
|
||||||
|
background: color-mix(in srgb, var(--border-critical, #e65c5c) 10%, transparent);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-spin {
|
||||||
|
animation: remote-spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes remote-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@
|
|||||||
@import "./components/selector.css";
|
@import "./components/selector.css";
|
||||||
@import "./components/env-vars.css";
|
@import "./components/env-vars.css";
|
||||||
@import "./components/directory-browser.css";
|
@import "./components/directory-browser.css";
|
||||||
|
@import "./components/remote-access.css";
|
||||||
|
|||||||
3
packages/ui/src/types/qrcode.d.ts
vendored
Normal file
3
packages/ui/src/types/qrcode.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
declare module "qrcode" {
|
||||||
|
export function toDataURL(text: string, opts?: Record<string, unknown>): Promise<string>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user