mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-05 11:15:57 +00:00
Add first iteration of file browser
This commit is contained in:
parent
eab7a6f833
commit
db8f4ffbeb
16 changed files with 1651 additions and 61 deletions
64
web/bun.lock
64
web/bun.lock
|
|
@ -5,10 +5,12 @@
|
|||
"name": "vibetunnel-web",
|
||||
"dependencies": {
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@xterm/headless": "^5.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"express": "^4.19.2",
|
||||
"lit": "^3.3.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"signal-exit": "^4.1.0",
|
||||
"ws": "^8.18.2",
|
||||
},
|
||||
|
|
@ -382,6 +384,8 @@
|
|||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/mime-types": ["@types/mime-types@3.0.1", "", {}, "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
|
@ -1100,9 +1104,9 @@
|
|||
|
||||
"mime": ["mime@2.6.0", "", { "bin": "cli.js" }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
|
|
@ -1544,12 +1548,8 @@
|
|||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.15.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
|
@ -1576,7 +1576,7 @@
|
|||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
|
||||
|
||||
|
|
@ -1594,16 +1594,12 @@
|
|||
|
||||
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"cliui/string-width": ["string-width@3.1.0", "", { "dependencies": { "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^5.1.0" } }, "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@5.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "string-width": "^3.0.0", "strip-ansi": "^5.0.0" } }, "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q=="],
|
||||
|
||||
"concurrently/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
|
@ -1616,6 +1612,8 @@
|
|||
|
||||
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"get-uri/data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
|
@ -1624,8 +1622,6 @@
|
|||
|
||||
"istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"jake/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"jest-circus/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="],
|
||||
|
||||
"jest-cli/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
|
@ -1644,14 +1640,10 @@
|
|||
|
||||
"jest-snapshot/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="],
|
||||
|
||||
"jest-util/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"jest-validate/camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
|
||||
|
||||
"jest-validate/pretty-format": ["pretty-format@30.0.2", "", { "dependencies": { "@jest/schemas": "30.0.1", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg=="],
|
||||
|
||||
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"log-update/ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
|
||||
|
|
@ -1660,8 +1652,6 @@
|
|||
|
||||
"log-update/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"log-update/wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
|
@ -1692,8 +1682,6 @@
|
|||
|
||||
"string-length/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
|
@ -1710,11 +1698,7 @@
|
|||
|
||||
"test-exclude/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"vite/fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"vite/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
|
|
@ -1730,10 +1714,6 @@
|
|||
|
||||
"yargs/yargs-parser": ["yargs-parser@13.1.2", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg=="],
|
||||
|
||||
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
|
@ -1760,9 +1740,9 @@
|
|||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"babel-plugin-istanbul/test-exclude/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
"babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
|
|
@ -1774,10 +1754,6 @@
|
|||
|
||||
"cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"cliui/string-width/emoji-regex": ["emoji-regex@7.0.3", "", {}, "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="],
|
||||
|
||||
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="],
|
||||
|
||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||
|
||||
"concurrently/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
|
@ -1792,12 +1768,12 @@
|
|||
|
||||
"finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"jake/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"jest-circus/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"jest-circus/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
|
@ -1846,24 +1822,20 @@
|
|||
|
||||
"log-update/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"log-update/wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
|
||||
|
||||
"prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
||||
|
||||
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
|
@ -1888,8 +1860,6 @@
|
|||
|
||||
"@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"babel-plugin-istanbul/test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"chokidar-cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
|
@ -1916,8 +1886,6 @@
|
|||
|
||||
"jest-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"log-update/wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
|
||||
|
||||
"tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
|
|
|||
87
web/package-lock.json
generated
87
web/package-lock.json
generated
|
|
@ -10,10 +10,12 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@xterm/headless": "^5.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"express": "^4.19.2",
|
||||
"lit": "^3.3.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"signal-exit": "^4.1.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
|
|
@ -2779,6 +2781,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime-types": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz",
|
||||
|
|
@ -3615,6 +3623,27 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
|
|
@ -6356,6 +6385,29 @@
|
|||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
|
|
@ -8846,21 +8898,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
|
||||
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
|
|
@ -11586,6 +11638,27 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.12.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@xterm/headless": "^5.5.0",
|
||||
"chalk": "^4.1.2",
|
||||
"express": "^4.19.2",
|
||||
"lit": "^3.3.0",
|
||||
"mime-types": "^3.0.1",
|
||||
"signal-exit": "^4.1.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import './components/session-create-form.js';
|
|||
import './components/session-list.js';
|
||||
import './components/session-view.js';
|
||||
import './components/session-card.js';
|
||||
import './components/file-browser-enhanced.js';
|
||||
|
||||
import type { Session } from './components/session-list.js';
|
||||
import type { SessionCard } from './components/session-card.js';
|
||||
|
|
@ -27,6 +28,7 @@ export class VibeTunnelApp extends LitElement {
|
|||
@state() private selectedSessionId: string | null = null;
|
||||
@state() private hideExited = this.loadHideExitedState();
|
||||
@state() private showCreateModal = false;
|
||||
@state() private showFileBrowser = false;
|
||||
|
||||
private hotReloadWs: WebSocket | null = null;
|
||||
private errorTimeoutId: number | null = null;
|
||||
|
|
@ -38,6 +40,7 @@ export class VibeTunnelApp extends LitElement {
|
|||
this.loadSessions();
|
||||
this.startAutoRefresh();
|
||||
this.setupRouting();
|
||||
this.setupKeyboardShortcuts();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
|
@ -47,6 +50,20 @@ export class VibeTunnelApp extends LitElement {
|
|||
}
|
||||
// Clean up routing listeners
|
||||
window.removeEventListener('popstate', this.handlePopState);
|
||||
// Clean up keyboard shortcuts
|
||||
window.removeEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Handle Cmd+O / Ctrl+O to open file browser
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'o' && this.currentView === 'list') {
|
||||
e.preventDefault();
|
||||
this.showFileBrowser = true;
|
||||
}
|
||||
};
|
||||
|
||||
private setupKeyboardShortcuts() {
|
||||
window.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
|
|
@ -452,6 +469,7 @@ export class VibeTunnelApp extends LitElement {
|
|||
@hide-exited-change=${this.handleHideExitedChange}
|
||||
@kill-all-sessions=${this.handleKillAll}
|
||||
@clean-exited-sessions=${this.handleCleanExited}
|
||||
@open-file-browser=${() => (this.showFileBrowser = true)}
|
||||
></app-header>
|
||||
<session-list
|
||||
.sessions=${this.sessions}
|
||||
|
|
@ -469,6 +487,13 @@ export class VibeTunnelApp extends LitElement {
|
|||
></session-list>
|
||||
</div>
|
||||
`}
|
||||
|
||||
<!-- File Browser Modal -->
|
||||
<file-browser-enhanced
|
||||
.visible=${this.showFileBrowser}
|
||||
.mode=${'browse'}
|
||||
@browser-cancel=${() => (this.showFileBrowser = false)}
|
||||
></file-browser-enhanced>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ export class AppHeader extends LitElement {
|
|||
this.dispatchEvent(new CustomEvent('clean-exited-sessions'));
|
||||
}
|
||||
|
||||
private handleOpenFileBrowser() {
|
||||
this.dispatchEvent(new CustomEvent('open-file-browser'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const runningSessions = this.sessions.filter((session) => session.status === 'running');
|
||||
const exitedSessions = this.sessions.filter((session) => session.status === 'exited');
|
||||
|
|
@ -116,6 +120,13 @@ export class AppHeader extends LitElement {
|
|||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn-secondary font-mono text-xs px-3 py-2"
|
||||
@click=${this.handleOpenFileBrowser}
|
||||
title="Browse Files"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary font-mono text-xs px-4 py-2 vt-create-button"
|
||||
@click=${this.handleCreateSession}
|
||||
|
|
@ -180,6 +191,13 @@ export class AppHeader extends LitElement {
|
|||
</button>
|
||||
`
|
||||
: ''}
|
||||
<button
|
||||
class="btn-secondary font-mono text-xs px-4 py-2"
|
||||
@click=${this.handleOpenFileBrowser}
|
||||
title="Browse Files (⌘O)"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary font-mono text-xs px-4 py-2 vt-create-button"
|
||||
@click=${this.handleCreateSession}
|
||||
|
|
|
|||
766
web/src/client/components/file-browser-enhanced.ts
Normal file
766
web/src/client/components/file-browser-enhanced.ts
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
size: number;
|
||||
modified: string;
|
||||
permissions?: string;
|
||||
isGitTracked?: boolean;
|
||||
gitStatus?: 'modified' | 'added' | 'deleted' | 'untracked' | 'unchanged';
|
||||
}
|
||||
|
||||
interface DirectoryListing {
|
||||
path: string;
|
||||
fullPath: string;
|
||||
gitStatus: GitStatus | null;
|
||||
files: FileInfo[];
|
||||
}
|
||||
|
||||
interface GitStatus {
|
||||
isGitRepo: boolean;
|
||||
branch?: string;
|
||||
modified: string[];
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
untracked: string[];
|
||||
}
|
||||
|
||||
interface FilePreview {
|
||||
type: 'image' | 'text' | 'binary';
|
||||
content?: string;
|
||||
language?: string;
|
||||
url?: string;
|
||||
mimeType?: string;
|
||||
size: number;
|
||||
humanSize?: string;
|
||||
}
|
||||
|
||||
interface FileDiff {
|
||||
path: string;
|
||||
diff: string;
|
||||
hasDiff: boolean;
|
||||
}
|
||||
|
||||
@customElement('file-browser-enhanced')
|
||||
export class FileBrowserEnhanced extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
max-width: 1400px;
|
||||
max-height: 900px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.split-view {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
gap: 1px;
|
||||
background: #2d2d30;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
background: #1e1e1e;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.preview-pane {
|
||||
flex: 2;
|
||||
background: #1e1e1e;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: 8px 16px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.15s;
|
||||
border-left: 3px solid transparent;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: #094771;
|
||||
border-left-color: #007acc;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.git-status {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.git-status.modified {
|
||||
background: #4b3c00;
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.git-status.added {
|
||||
background: #0e3a0e;
|
||||
color: #73c991;
|
||||
}
|
||||
|
||||
.git-status.deleted {
|
||||
background: #5a1d1d;
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.git-status.untracked {
|
||||
background: #373737;
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.monaco-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #1e1e1e;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-preview img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.binary-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #cccccc;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
padding: 4px 8px;
|
||||
background: #3e3e42;
|
||||
border: 1px solid #5a5a5a;
|
||||
color: #cccccc;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.filter-button:hover {
|
||||
background: #4e4e52;
|
||||
}
|
||||
|
||||
.filter-button.active {
|
||||
background: #007acc;
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.path-breadcrumb {
|
||||
padding: 8px 16px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
font-size: 12px;
|
||||
color: #cccccc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #3e3e42;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.diff-preview {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.diff-line {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diff-line.added {
|
||||
background: #0e3a0e;
|
||||
color: #73c991;
|
||||
}
|
||||
|
||||
.diff-line.deleted {
|
||||
background: #5a1d1d;
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.diff-line.context {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.diff-line.header {
|
||||
color: #569cd6;
|
||||
font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: String }) currentPath = '.';
|
||||
@property({ type: Boolean }) visible = false;
|
||||
@property({ type: String }) mode: 'browse' | 'select' = 'browse';
|
||||
|
||||
@state() private files: FileInfo[] = [];
|
||||
@state() private loading = false;
|
||||
@state() private selectedFile: FileInfo | null = null;
|
||||
@state() private preview: FilePreview | null = null;
|
||||
@state() private diff: FileDiff | null = null;
|
||||
@state() private gitFilter: 'all' | 'changed' = 'all';
|
||||
@state() private showHidden = false;
|
||||
@state() private gitStatus: GitStatus | null = null;
|
||||
@state() private previewLoading = false;
|
||||
@state() private showDiff = false;
|
||||
|
||||
private monacoEditor: any = null;
|
||||
private monacoContainer: HTMLElement | null = null;
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
console.log(
|
||||
`[FileBrowser] Connected, visible: ${this.visible}, currentPath: ${this.currentPath}`
|
||||
);
|
||||
if (this.visible) {
|
||||
await this.loadDirectory(this.currentPath);
|
||||
}
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
}
|
||||
|
||||
async updated(changedProperties: Map<string, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has('visible')) {
|
||||
console.log(`[FileBrowser] Visibility changed to: ${this.visible}`);
|
||||
if (this.visible) {
|
||||
await this.loadDirectory(this.currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.preview?.type === 'text' && this.monacoContainer && !this.monacoEditor) {
|
||||
this.initMonacoEditor();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDirectory(dirPath: string) {
|
||||
this.loading = true;
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
path: dirPath,
|
||||
showHidden: this.showHidden.toString(),
|
||||
gitFilter: this.gitFilter,
|
||||
});
|
||||
|
||||
console.log(`[FileBrowser] Loading directory: ${dirPath}`);
|
||||
const response = await fetch(`/api/fs/browse?${params}`);
|
||||
console.log(`[FileBrowser] Response status: ${response.status}`);
|
||||
|
||||
if (response.ok) {
|
||||
const data: DirectoryListing = await response.json();
|
||||
console.log(`[FileBrowser] Received ${data.files?.length || 0} files`);
|
||||
this.currentPath = data.path;
|
||||
this.files = data.files || [];
|
||||
this.gitStatus = data.gitStatus;
|
||||
} else {
|
||||
const errorData = await response.text();
|
||||
console.error(`[FileBrowser] Failed to load directory: ${response.status}`, errorData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FileBrowser] Error loading directory:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPreview(file: FileInfo) {
|
||||
if (file.type === 'directory') return;
|
||||
|
||||
this.previewLoading = true;
|
||||
this.selectedFile = file;
|
||||
this.showDiff = false;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/fs/preview?path=${encodeURIComponent(file.path)}`);
|
||||
if (response.ok) {
|
||||
this.preview = await response.json();
|
||||
if (this.preview?.type === 'text') {
|
||||
// Update Monaco editor if it exists
|
||||
this.updateMonacoContent();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading preview:', error);
|
||||
} finally {
|
||||
this.previewLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadDiff(file: FileInfo) {
|
||||
if (file.type === 'directory' || !file.gitStatus || file.gitStatus === 'unchanged') return;
|
||||
|
||||
this.previewLoading = true;
|
||||
this.showDiff = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/fs/diff?path=${encodeURIComponent(file.path)}`);
|
||||
if (response.ok) {
|
||||
this.diff = await response.json();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading diff:', error);
|
||||
} finally {
|
||||
this.previewLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private initMonacoEditor() {
|
||||
if (!window.monaco || !this.monacoContainer) return;
|
||||
|
||||
this.monacoEditor = window.monaco.editor.create(this.monacoContainer, {
|
||||
value: this.preview?.content || '',
|
||||
language: this.preview?.language || 'plaintext',
|
||||
theme: 'vs-dark',
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
});
|
||||
}
|
||||
|
||||
private updateMonacoContent() {
|
||||
if (!this.monacoEditor || !this.preview) return;
|
||||
|
||||
this.monacoEditor.setValue(this.preview.content || '');
|
||||
window.monaco.editor.setModelLanguage(
|
||||
this.monacoEditor.getModel(),
|
||||
this.preview.language || 'plaintext'
|
||||
);
|
||||
}
|
||||
|
||||
private handleFileClick(file: FileInfo) {
|
||||
if (file.type === 'directory') {
|
||||
this.loadDirectory(file.path);
|
||||
} else {
|
||||
this.loadPreview(file);
|
||||
}
|
||||
}
|
||||
|
||||
private handleParentClick() {
|
||||
const parentPath = this.currentPath.split('/').slice(0, -1).join('/') || '.';
|
||||
this.loadDirectory(parentPath);
|
||||
}
|
||||
|
||||
private toggleGitFilter() {
|
||||
this.gitFilter = this.gitFilter === 'all' ? 'changed' : 'all';
|
||||
this.loadDirectory(this.currentPath);
|
||||
}
|
||||
|
||||
private toggleHidden() {
|
||||
this.showHidden = !this.showHidden;
|
||||
this.loadDirectory(this.currentPath);
|
||||
}
|
||||
|
||||
private toggleDiff() {
|
||||
if (
|
||||
this.selectedFile &&
|
||||
this.selectedFile.gitStatus &&
|
||||
this.selectedFile.gitStatus !== 'unchanged'
|
||||
) {
|
||||
if (this.showDiff) {
|
||||
this.loadPreview(this.selectedFile);
|
||||
} else {
|
||||
this.loadDiff(this.selectedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelect() {
|
||||
if (this.mode === 'select' && this.currentPath) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('directory-selected', {
|
||||
detail: this.currentPath,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCancel() {
|
||||
this.dispatchEvent(new CustomEvent('browser-cancel'));
|
||||
}
|
||||
|
||||
private handleOverlayClick(e: Event) {
|
||||
if (e.target === e.currentTarget) {
|
||||
this.handleCancel();
|
||||
}
|
||||
}
|
||||
|
||||
private renderFileIcon(file: FileInfo) {
|
||||
if (file.type === 'directory') {
|
||||
return '📁';
|
||||
}
|
||||
|
||||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||
const iconMap: Record<string, string> = {
|
||||
js: '📜',
|
||||
ts: '📘',
|
||||
jsx: '⚛️',
|
||||
tsx: '⚛️',
|
||||
json: '📋',
|
||||
md: '📝',
|
||||
txt: '📄',
|
||||
html: '🌐',
|
||||
css: '🎨',
|
||||
scss: '🎨',
|
||||
png: '🖼️',
|
||||
jpg: '🖼️',
|
||||
jpeg: '🖼️',
|
||||
gif: '🖼️',
|
||||
svg: '🖼️',
|
||||
pdf: '📑',
|
||||
zip: '📦',
|
||||
tar: '📦',
|
||||
gz: '📦',
|
||||
};
|
||||
|
||||
return iconMap[ext || ''] || '📄';
|
||||
}
|
||||
|
||||
private renderGitStatus(status?: FileInfo['gitStatus']) {
|
||||
if (!status || status === 'unchanged') return '';
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
modified: 'M',
|
||||
added: 'A',
|
||||
deleted: 'D',
|
||||
untracked: '?',
|
||||
};
|
||||
|
||||
return html` <span class="git-status ${status}">${labels[status]}</span> `;
|
||||
}
|
||||
|
||||
private renderPreview() {
|
||||
if (this.previewLoading) {
|
||||
return html`<div class="loading">Loading preview...</div>`;
|
||||
}
|
||||
|
||||
if (this.showDiff && this.diff) {
|
||||
return this.renderDiff();
|
||||
}
|
||||
|
||||
if (!this.preview) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📄</div>
|
||||
<div>Select a file to preview</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
switch (this.preview.type) {
|
||||
case 'image':
|
||||
return html`
|
||||
<div class="image-preview">
|
||||
<img src="${this.preview.url}" alt="${this.selectedFile?.name}" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
case 'text':
|
||||
return html`
|
||||
<div
|
||||
class="monaco-container"
|
||||
@connected=${(e: Event) => {
|
||||
this.monacoContainer = e.target as HTMLElement;
|
||||
this.initMonacoEditor();
|
||||
}}
|
||||
></div>
|
||||
`;
|
||||
|
||||
case 'binary':
|
||||
return html`
|
||||
<div class="binary-preview">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📦</div>
|
||||
<div style="font-size: 18px; margin-bottom: 8px;">Binary File</div>
|
||||
<div style="color: #888;">
|
||||
${this.preview.humanSize || this.preview.size + ' bytes'}
|
||||
</div>
|
||||
<div style="color: #888; margin-top: 8px;">
|
||||
${this.preview.mimeType || 'Unknown type'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private renderDiff() {
|
||||
if (!this.diff || !this.diff.diff) {
|
||||
return html`
|
||||
<div class="empty-state">
|
||||
<div>No changes in this file</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const lines = this.diff.diff.split('\n');
|
||||
return html`
|
||||
<div class="diff-preview">
|
||||
${lines.map((line) => {
|
||||
let className = 'diff-line context';
|
||||
if (line.startsWith('+')) className = 'diff-line added';
|
||||
else if (line.startsWith('-')) className = 'diff-line deleted';
|
||||
else if (line.startsWith('@@')) className = 'diff-line header';
|
||||
|
||||
return html`<div class="${className}">${line}</div>`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="modal-overlay" @click=${this.handleOverlayClick}>
|
||||
<div class="modal-container" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<div class="toolbar">
|
||||
<button
|
||||
class="filter-button ${this.gitFilter === 'changed' ? 'active' : ''}"
|
||||
@click=${this.toggleGitFilter}
|
||||
title="Show only Git changes"
|
||||
>
|
||||
Git Changes
|
||||
</button>
|
||||
<button
|
||||
class="filter-button ${this.showHidden ? 'active' : ''}"
|
||||
@click=${this.toggleHidden}
|
||||
title="Show hidden files"
|
||||
>
|
||||
Hidden Files
|
||||
</button>
|
||||
${this.gitStatus
|
||||
? html`
|
||||
<div
|
||||
style="margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; color: #888;"
|
||||
>
|
||||
<span>📍 ${this.gitStatus.branch}</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
<div class="path-breadcrumb">
|
||||
<span>📂 ${this.currentPath}</span>
|
||||
<button class="close-button" @click=${this.handleCancel} title="Close (Esc)">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="split-view" style="flex: 1;">
|
||||
<div class="file-list">
|
||||
${this.loading
|
||||
? html`<div class="loading">Loading...</div>`
|
||||
: html`
|
||||
${this.currentPath !== '.' && this.currentPath !== '/'
|
||||
? html`
|
||||
<div class="file-item" @click=${this.handleParentClick}>
|
||||
<span class="file-icon">⬆️</span>
|
||||
<span class="file-name">..</span>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${this.files.map(
|
||||
(file) => html`
|
||||
<div
|
||||
class="file-item ${this.selectedFile?.path === file.path
|
||||
? 'selected'
|
||||
: ''}"
|
||||
@click=${() => this.handleFileClick(file)}
|
||||
>
|
||||
<span class="file-icon">${this.renderFileIcon(file)}</span>
|
||||
<span class="file-name">${file.name}</span>
|
||||
${this.renderGitStatus(file.gitStatus)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="preview-pane">
|
||||
${this.selectedFile
|
||||
? html`
|
||||
<div class="preview-header">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span>${this.renderFileIcon(this.selectedFile)}</span>
|
||||
<span>${this.selectedFile.name}</span>
|
||||
${this.renderGitStatus(this.selectedFile.gitStatus)}
|
||||
</div>
|
||||
${this.selectedFile.gitStatus && this.selectedFile.gitStatus !== 'unchanged'
|
||||
? html`
|
||||
<button
|
||||
class="filter-button ${this.showDiff ? 'active' : ''}"
|
||||
@click=${this.toggleDiff}
|
||||
>
|
||||
${this.showDiff ? 'View File' : 'View Diff'}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
<div class="preview-content">${this.renderPreview()}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.mode === 'select'
|
||||
? html`
|
||||
<div class="p-4 border-t border-dark-border flex gap-4">
|
||||
<button class="btn-ghost font-mono flex-1" @click=${this.handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn-primary font-mono flex-1" @click=${this.handleSelect}>
|
||||
Select Directory
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
if (this.monacoEditor) {
|
||||
this.monacoEditor.dispose();
|
||||
this.monacoEditor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && this.visible) {
|
||||
e.preventDefault();
|
||||
this.handleCancel();
|
||||
}
|
||||
};
|
||||
}
|
||||
99
web/src/client/components/file-browser-fab.ts
Normal file
99
web/src/client/components/file-browser-fab.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
@customElement('file-browser-fab')
|
||||
export class FileBrowserFAB extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fab:hover {
|
||||
background: #005a9e;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.fab:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
margin-bottom: 8px;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fab:hover + .tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:host {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.fab {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Boolean }) visible = true;
|
||||
|
||||
private handleClick() {
|
||||
this.dispatchEvent(new CustomEvent('open-file-browser'));
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.visible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<button class="fab" @click=${this.handleClick} title="Browse Files (⌘O)">
|
||||
<span class="icon">📁</span>
|
||||
</button>
|
||||
<div class="tooltip">Browse Files (⌘O)</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import { LitElement, PropertyValues, html } from 'lit';
|
|||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import type { Session } from './session-list.js';
|
||||
import './terminal.js';
|
||||
import './file-browser-fab.js';
|
||||
import './file-browser-enhanced.js';
|
||||
import type { Terminal } from './terminal.js';
|
||||
import { CastConverter } from '../utils/cast-converter.js';
|
||||
import {
|
||||
|
|
@ -35,6 +37,7 @@ export class SessionView extends LitElement {
|
|||
@state() private terminalMaxCols = 0;
|
||||
@state() private showWidthSelector = false;
|
||||
@state() private customWidth = '';
|
||||
@state() private showFileBrowser = false;
|
||||
|
||||
private preferencesManager = TerminalPreferencesManager.getInstance();
|
||||
@state() private reconnectCount = 0;
|
||||
|
|
@ -48,6 +51,12 @@ export class SessionView extends LitElement {
|
|||
private lastResizeHeight = 0;
|
||||
|
||||
private keyboardHandler = (e: KeyboardEvent) => {
|
||||
// Handle Cmd+O / Ctrl+O to open file browser
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'o') {
|
||||
e.preventDefault();
|
||||
this.showFileBrowser = true;
|
||||
return;
|
||||
}
|
||||
if (!this.session) return;
|
||||
|
||||
// Allow important browser shortcuts to pass through
|
||||
|
|
@ -847,6 +856,14 @@ export class SessionView extends LitElement {
|
|||
return commonWidth ? commonWidth.label : this.terminalMaxCols.toString();
|
||||
}
|
||||
|
||||
private handleOpenFileBrowser() {
|
||||
this.showFileBrowser = true;
|
||||
}
|
||||
|
||||
private handleCloseFileBrowser() {
|
||||
this.showFileBrowser = false;
|
||||
}
|
||||
|
||||
private async sendInputText(text: string) {
|
||||
if (!this.session) return;
|
||||
|
||||
|
|
@ -1359,6 +1376,19 @@ export class SessionView extends LitElement {
|
|||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<!-- File Browser FAB -->
|
||||
<file-browser-fab
|
||||
.visible=${!this.showFileBrowser}
|
||||
@open-file-browser=${this.handleOpenFileBrowser}
|
||||
></file-browser-fab>
|
||||
|
||||
<!-- File Browser Modal -->
|
||||
<file-browser-enhanced
|
||||
.visible=${this.showFileBrowser}
|
||||
.mode=${'browse'}
|
||||
@browser-cancel=${this.handleCloseFileBrowser}
|
||||
></file-browser-enhanced>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
8
web/src/client/types/monaco.d.ts
vendored
Normal file
8
web/src/client/types/monaco.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
declare global {
|
||||
interface Window {
|
||||
monaco: any;
|
||||
require: any;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Entry point for the server - imports the modular server which starts automatically
|
||||
import { startVibeTunnelForward } from './server/fwd.js';
|
||||
import { startVibeTunnelServer } from './server/server.js';
|
||||
import { VERSION } from './server/version.js';
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
|
|
@ -14,7 +15,10 @@ process.on('unhandledRejection', (reason, promise) => {
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.argv[2] === 'fwd') {
|
||||
if (process.argv[2] === 'version') {
|
||||
console.log(`VibeTunnel Linux v${VERSION}`);
|
||||
process.exit(0);
|
||||
} else if (process.argv[2] === 'fwd') {
|
||||
startVibeTunnelForward(process.argv.slice(3)).catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { HQClient } from './services/hq-client.js';
|
|||
import { createAuthMiddleware } from './middleware/auth.js';
|
||||
import { createSessionRoutes } from './routes/sessions.js';
|
||||
import { createRemoteRoutes } from './routes/remotes.js';
|
||||
import { createFilesystemRoutes } from './routes/filesystem.js';
|
||||
import { ControlDirWatcher } from './services/control-dir-watcher.js';
|
||||
import { BufferAggregator } from './services/buffer-aggregator.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -354,6 +355,9 @@ export function createApp(): AppInstance {
|
|||
})
|
||||
);
|
||||
|
||||
// Mount filesystem routes
|
||||
app.use('/api', createFilesystemRoutes());
|
||||
|
||||
// WebSocket endpoint for buffer updates
|
||||
wss.on('connection', (ws, _req) => {
|
||||
if (bufferAggregator) {
|
||||
|
|
@ -438,6 +442,7 @@ export function createApp(): AppInstance {
|
|||
remoteRegistry,
|
||||
isHQMode: config.isHQMode,
|
||||
hqClient,
|
||||
ptyManager,
|
||||
});
|
||||
controlDirWatcher.start();
|
||||
});
|
||||
|
|
|
|||
462
web/src/server/routes/filesystem.ts
Normal file
462
web/src/server/routes/filesystem.ts
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
import { Router, Request, Response } from 'express';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import mime from 'mime-types';
|
||||
import { createReadStream, statSync } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'directory';
|
||||
size: number;
|
||||
modified: string;
|
||||
permissions?: string;
|
||||
isGitTracked?: boolean;
|
||||
gitStatus?: 'modified' | 'added' | 'deleted' | 'untracked' | 'unchanged';
|
||||
}
|
||||
|
||||
interface GitStatus {
|
||||
isGitRepo: boolean;
|
||||
branch?: string;
|
||||
modified: string[];
|
||||
added: string[];
|
||||
deleted: string[];
|
||||
untracked: string[];
|
||||
}
|
||||
|
||||
export function createFilesystemRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
// Helper to check if path is safe (no directory traversal)
|
||||
function isPathSafe(requestedPath: string, basePath: string): boolean {
|
||||
const resolved = path.resolve(basePath, requestedPath);
|
||||
return resolved.startsWith(path.resolve(basePath));
|
||||
}
|
||||
|
||||
// Helper to get Git status for a directory
|
||||
async function getGitStatus(dirPath: string): Promise<GitStatus | null> {
|
||||
try {
|
||||
// Check if directory is a git repository
|
||||
await execAsync('git rev-parse --git-dir', { cwd: dirPath });
|
||||
|
||||
// Get current branch
|
||||
const { stdout: branch } = await execAsync('git branch --show-current', { cwd: dirPath });
|
||||
|
||||
// Get status
|
||||
const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: dirPath });
|
||||
|
||||
const status: GitStatus = {
|
||||
isGitRepo: true,
|
||||
branch: branch.trim(),
|
||||
modified: [],
|
||||
added: [],
|
||||
deleted: [],
|
||||
untracked: [],
|
||||
};
|
||||
|
||||
// Parse git status output
|
||||
statusOutput.split('\n').forEach((line) => {
|
||||
if (!line) return;
|
||||
|
||||
const statusCode = line.substring(0, 2);
|
||||
const filename = line.substring(3);
|
||||
|
||||
if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') {
|
||||
status.modified.push(filename);
|
||||
} else if (statusCode === 'A ' || statusCode === 'AM') {
|
||||
status.added.push(filename);
|
||||
} else if (statusCode === ' D' || statusCode === 'D ') {
|
||||
status.deleted.push(filename);
|
||||
} else if (statusCode === '??') {
|
||||
status.untracked.push(filename);
|
||||
}
|
||||
});
|
||||
|
||||
return status;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get file Git status
|
||||
function getFileGitStatus(filePath: string, gitStatus: GitStatus | null): FileInfo['gitStatus'] {
|
||||
if (!gitStatus) return undefined;
|
||||
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
if (gitStatus.modified.includes(relativePath)) return 'modified';
|
||||
if (gitStatus.added.includes(relativePath)) return 'added';
|
||||
if (gitStatus.deleted.includes(relativePath)) return 'deleted';
|
||||
if (gitStatus.untracked.includes(relativePath)) return 'untracked';
|
||||
|
||||
return 'unchanged';
|
||||
}
|
||||
|
||||
// Browse directory endpoint
|
||||
router.get('/fs/browse', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const requestedPath = (req.query.path as string) || '.';
|
||||
const showHidden = req.query.showHidden === 'true';
|
||||
const gitFilter = req.query.gitFilter as string; // 'all' | 'changed' | 'none'
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(requestedPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), requestedPath);
|
||||
|
||||
// Check if path exists and is a directory
|
||||
const stats = await fs.stat(fullPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return res.status(400).json({ error: 'Path is not a directory' });
|
||||
}
|
||||
|
||||
// Get Git status if requested
|
||||
const gitStatus = gitFilter !== 'none' ? await getGitStatus(fullPath) : null;
|
||||
|
||||
// Read directory contents
|
||||
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
||||
|
||||
// Build file list
|
||||
const files: FileInfo[] = await Promise.all(
|
||||
entries
|
||||
.filter((entry) => showHidden || !entry.name.startsWith('.'))
|
||||
.map(async (entry) => {
|
||||
const entryPath = path.join(fullPath, entry.name);
|
||||
const stats = await fs.stat(entryPath);
|
||||
const relativePath = path.relative(process.cwd(), entryPath);
|
||||
|
||||
const fileInfo: FileInfo = {
|
||||
name: entry.name,
|
||||
path: relativePath,
|
||||
type: entry.isDirectory() ? 'directory' : 'file',
|
||||
size: stats.size,
|
||||
modified: stats.mtime.toISOString(),
|
||||
permissions: stats.mode.toString(8).slice(-3),
|
||||
isGitTracked: gitStatus?.isGitRepo || false,
|
||||
gitStatus: getFileGitStatus(entryPath, gitStatus),
|
||||
};
|
||||
|
||||
return fileInfo;
|
||||
})
|
||||
);
|
||||
|
||||
// Filter by Git status if requested
|
||||
let filteredFiles = files;
|
||||
if (gitFilter === 'changed' && gitStatus) {
|
||||
filteredFiles = files.filter((file) => file.gitStatus && file.gitStatus !== 'unchanged');
|
||||
}
|
||||
|
||||
// Sort: directories first, then by name
|
||||
filteredFiles.sort((a, b) => {
|
||||
if (a.type !== b.type) {
|
||||
return a.type === 'directory' ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
res.json({
|
||||
path: requestedPath,
|
||||
fullPath,
|
||||
gitStatus,
|
||||
files: filteredFiles,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Browse error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get file preview
|
||||
router.get('/fs/preview', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const requestedPath = req.query.path as string;
|
||||
if (!requestedPath) {
|
||||
return res.status(400).json({ error: 'Path is required' });
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(requestedPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), requestedPath);
|
||||
const stats = await fs.stat(fullPath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return res.status(400).json({ error: 'Cannot preview directories' });
|
||||
}
|
||||
|
||||
// Determine file type
|
||||
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
||||
const isText =
|
||||
mimeType.startsWith('text/') ||
|
||||
mimeType === 'application/json' ||
|
||||
mimeType === 'application/javascript' ||
|
||||
mimeType === 'application/typescript' ||
|
||||
mimeType === 'application/xml';
|
||||
const isImage = mimeType.startsWith('image/');
|
||||
|
||||
if (isImage) {
|
||||
// For images, return URL to fetch the image
|
||||
res.json({
|
||||
type: 'image',
|
||||
mimeType,
|
||||
url: `/api/fs/raw?path=${encodeURIComponent(requestedPath)}`,
|
||||
size: stats.size,
|
||||
});
|
||||
} else if (isText || stats.size < 1024 * 1024) {
|
||||
// Text or small files (< 1MB)
|
||||
const content = await fs.readFile(fullPath, 'utf-8');
|
||||
const language = getLanguageFromPath(fullPath);
|
||||
|
||||
res.json({
|
||||
type: 'text',
|
||||
content,
|
||||
language,
|
||||
mimeType,
|
||||
size: stats.size,
|
||||
});
|
||||
} else {
|
||||
// Binary or large files
|
||||
res.json({
|
||||
type: 'binary',
|
||||
mimeType,
|
||||
size: stats.size,
|
||||
humanSize: formatBytes(stats.size),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Preview error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Serve raw file content
|
||||
router.get('/fs/raw', (req: Request, res: Response) => {
|
||||
try {
|
||||
const requestedPath = req.query.path as string;
|
||||
if (!requestedPath) {
|
||||
return res.status(400).json({ error: 'Path is required' });
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(requestedPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), requestedPath);
|
||||
|
||||
// Check if file exists
|
||||
if (!statSync(fullPath).isFile()) {
|
||||
return res.status(404).json({ error: 'File not found' });
|
||||
}
|
||||
|
||||
// Set appropriate content type
|
||||
const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimeType);
|
||||
|
||||
// Stream the file
|
||||
const stream = createReadStream(fullPath);
|
||||
stream.pipe(res);
|
||||
} catch (error: any) {
|
||||
console.error('Raw file error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get file content (text files only)
|
||||
router.get('/fs/content', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const requestedPath = req.query.path as string;
|
||||
if (!requestedPath) {
|
||||
return res.status(400).json({ error: 'Path is required' });
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(requestedPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), requestedPath);
|
||||
const content = await fs.readFile(fullPath, 'utf-8');
|
||||
|
||||
res.json({
|
||||
path: requestedPath,
|
||||
content,
|
||||
language: getLanguageFromPath(fullPath),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Content error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get Git diff for a file
|
||||
router.get('/fs/diff', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const requestedPath = req.query.path as string;
|
||||
if (!requestedPath) {
|
||||
return res.status(400).json({ error: 'Path is required' });
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(requestedPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), requestedPath);
|
||||
const relativePath = path.relative(process.cwd(), fullPath);
|
||||
|
||||
// Get git diff
|
||||
const { stdout: diff } = await execAsync(`git diff HEAD -- "${relativePath}"`, {
|
||||
cwd: process.cwd(),
|
||||
});
|
||||
|
||||
res.json({
|
||||
path: requestedPath,
|
||||
diff,
|
||||
hasDiff: diff.length > 0,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Diff error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create directory
|
||||
router.post('/fs/mkdir', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { path: dirPath, name } = req.body;
|
||||
|
||||
if (!dirPath || !name) {
|
||||
return res.status(400).json({ error: 'Path and name are required' });
|
||||
}
|
||||
|
||||
// Validate name (no slashes, no dots at start)
|
||||
if (name.includes('/') || name.includes('\\') || name.startsWith('.')) {
|
||||
return res.status(400).json({ error: 'Invalid directory name' });
|
||||
}
|
||||
|
||||
// Security check
|
||||
if (!isPathSafe(dirPath, process.cwd())) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
|
||||
const fullPath = path.resolve(process.cwd(), dirPath, name);
|
||||
|
||||
// Create directory
|
||||
await fs.mkdir(fullPath, { recursive: true });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
path: path.relative(process.cwd(), fullPath),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Mkdir error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
// Helper function to determine language from file path
|
||||
function getLanguageFromPath(filePath: string): string {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const languageMap: Record<string, string> = {
|
||||
'.js': 'javascript',
|
||||
'.jsx': 'javascript',
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'typescript',
|
||||
'.py': 'python',
|
||||
'.java': 'java',
|
||||
'.c': 'c',
|
||||
'.cpp': 'cpp',
|
||||
'.cs': 'csharp',
|
||||
'.php': 'php',
|
||||
'.rb': 'ruby',
|
||||
'.go': 'go',
|
||||
'.rs': 'rust',
|
||||
'.swift': 'swift',
|
||||
'.kt': 'kotlin',
|
||||
'.scala': 'scala',
|
||||
'.r': 'r',
|
||||
'.m': 'objective-c',
|
||||
'.mm': 'objective-c',
|
||||
'.h': 'c',
|
||||
'.hpp': 'cpp',
|
||||
'.sh': 'shell',
|
||||
'.bash': 'shell',
|
||||
'.zsh': 'shell',
|
||||
'.fish': 'shell',
|
||||
'.ps1': 'powershell',
|
||||
'.html': 'html',
|
||||
'.htm': 'html',
|
||||
'.xml': 'xml',
|
||||
'.css': 'css',
|
||||
'.scss': 'scss',
|
||||
'.sass': 'sass',
|
||||
'.less': 'less',
|
||||
'.json': 'json',
|
||||
'.yaml': 'yaml',
|
||||
'.yml': 'yaml',
|
||||
'.toml': 'toml',
|
||||
'.ini': 'ini',
|
||||
'.cfg': 'ini',
|
||||
'.conf': 'ini',
|
||||
'.sql': 'sql',
|
||||
'.md': 'markdown',
|
||||
'.markdown': 'markdown',
|
||||
'.tex': 'latex',
|
||||
'.dockerfile': 'dockerfile',
|
||||
'.makefile': 'makefile',
|
||||
'.cmake': 'cmake',
|
||||
'.gradle': 'gradle',
|
||||
'.vue': 'vue',
|
||||
'.svelte': 'svelte',
|
||||
'.elm': 'elm',
|
||||
'.clj': 'clojure',
|
||||
'.cljs': 'clojure',
|
||||
'.ex': 'elixir',
|
||||
'.exs': 'elixir',
|
||||
'.erl': 'erlang',
|
||||
'.hrl': 'erlang',
|
||||
'.fs': 'fsharp',
|
||||
'.fsx': 'fsharp',
|
||||
'.fsi': 'fsharp',
|
||||
'.ml': 'ocaml',
|
||||
'.mli': 'ocaml',
|
||||
'.pas': 'pascal',
|
||||
'.pp': 'pascal',
|
||||
'.pl': 'perl',
|
||||
'.pm': 'perl',
|
||||
'.t': 'perl',
|
||||
'.lua': 'lua',
|
||||
'.dart': 'dart',
|
||||
'.nim': 'nim',
|
||||
'.nims': 'nim',
|
||||
'.zig': 'zig',
|
||||
'.jl': 'julia',
|
||||
};
|
||||
|
||||
return languageMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
// Helper function to format bytes
|
||||
function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { RemoteRegistry } from '../services/remote-registry.js';
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as net from 'net';
|
||||
|
||||
interface SessionRoutesConfig {
|
||||
ptyManager: PtyManager;
|
||||
|
|
@ -121,12 +122,47 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
|
||||
// Create new session (local or on remote)
|
||||
router.post('/sessions', async (req, res) => {
|
||||
const { command, workingDir, name, remoteId } = req.body;
|
||||
const { command, workingDir, name, remoteId, spawn_terminal } = req.body;
|
||||
|
||||
if (!command || !Array.isArray(command) || command.length === 0) {
|
||||
return res.status(400).json({ error: 'Command array is required' });
|
||||
}
|
||||
|
||||
// If spawn_terminal is true, use the spawn-terminal logic
|
||||
if (spawn_terminal) {
|
||||
try {
|
||||
// Generate session ID
|
||||
const sessionId = generateSessionId();
|
||||
const sessionName = name || `session_${Date.now()}`;
|
||||
|
||||
// Request Mac app to spawn terminal
|
||||
const spawnResult = await requestTerminalSpawn({
|
||||
sessionId,
|
||||
sessionName,
|
||||
command,
|
||||
workingDir: resolvePath(workingDir, process.cwd()),
|
||||
});
|
||||
|
||||
if (!spawnResult.success) {
|
||||
throw new Error(spawnResult.error || 'Failed to spawn terminal');
|
||||
}
|
||||
|
||||
// Wait a bit for the session to be created
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Return the session ID - client will poll for the session to appear
|
||||
res.json({ sessionId, message: 'Terminal spawn requested' });
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Error spawning terminal:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to spawn terminal',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// If remoteId is specified and we're in HQ mode, forward to remote
|
||||
if (remoteId && isHQMode && remoteRegistry) {
|
||||
|
|
@ -773,3 +809,85 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
|
||||
return router;
|
||||
}
|
||||
|
||||
// Generate a unique session ID
|
||||
function generateSessionId(): string {
|
||||
// Generate UUID v4
|
||||
const bytes = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bytes[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
|
||||
// Set version (4) and variant bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
// Convert to hex string with dashes
|
||||
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20, 32),
|
||||
].join('-');
|
||||
}
|
||||
|
||||
// Request terminal spawn from Mac app
|
||||
async function requestTerminalSpawn(params: {
|
||||
sessionId: string;
|
||||
sessionName: string;
|
||||
command: string[];
|
||||
workingDir: string;
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const socketPath = '/tmp/vibetunnel-terminal.sock';
|
||||
|
||||
// Check if socket exists
|
||||
if (!fs.existsSync(socketPath)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Terminal spawn service not available. Is the Mac app running?',
|
||||
};
|
||||
}
|
||||
|
||||
const spawnRequest = {
|
||||
workingDir: params.workingDir,
|
||||
sessionId: params.sessionId,
|
||||
command: params.command.join(' '),
|
||||
terminal: null, // Let Mac app use default terminal
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const client = net.createConnection(socketPath, () => {
|
||||
console.log(`Connected to terminal spawn service for session ${params.sessionId}`);
|
||||
client.write(JSON.stringify(spawnRequest));
|
||||
});
|
||||
|
||||
client.on('data', (data) => {
|
||||
try {
|
||||
const response = JSON.parse(data.toString());
|
||||
console.log(`Terminal spawn response:`, response);
|
||||
resolve({ success: response.success, error: response.error });
|
||||
} catch (error) {
|
||||
console.error('Failed to parse terminal spawn response:', error);
|
||||
resolve({ success: false, error: 'Invalid response from terminal spawn service' });
|
||||
}
|
||||
client.end();
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
console.error('Failed to connect to terminal spawn service:', error);
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Connection failed: ${error.message}`,
|
||||
});
|
||||
});
|
||||
|
||||
client.on('timeout', () => {
|
||||
client.destroy();
|
||||
resolve({ success: false, error: 'Terminal spawn request timed out' });
|
||||
});
|
||||
|
||||
client.setTimeout(5000); // 5 second timeout
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,7 @@ export function startVibeTunnelServer() {
|
|||
// When running with tsx, the main module check is different
|
||||
// NOTE: When bundled as 'vibetunnel' executable, index.ts handles the startup
|
||||
const isMainModule =
|
||||
process.argv[1]?.endsWith('server.ts') ||
|
||||
process.argv[1]?.endsWith('server/index.ts');
|
||||
process.argv[1]?.endsWith('server.ts') || process.argv[1]?.endsWith('server/index.ts');
|
||||
if (isMainModule) {
|
||||
startVibeTunnelServer();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import chalk from 'chalk';
|
|||
import { RemoteRegistry } from './remote-registry.js';
|
||||
import { HQClient } from './hq-client.js';
|
||||
import { isShuttingDown } from './shutdown-state.js';
|
||||
import { PtyManager } from '../pty/index.js';
|
||||
|
||||
interface ControlDirWatcherConfig {
|
||||
controlDir: string;
|
||||
remoteRegistry: RemoteRegistry | null;
|
||||
isHQMode: boolean;
|
||||
hqClient: HQClient | null;
|
||||
ptyManager?: PtyManager;
|
||||
}
|
||||
|
||||
export class ControlDirWatcher {
|
||||
|
|
@ -57,6 +59,17 @@ export class ControlDirWatcher {
|
|||
|
||||
console.log(chalk.blue(`Detected new external session: ${sessionId}`));
|
||||
|
||||
// Check if PtyManager already knows about this session
|
||||
if (this.config.ptyManager) {
|
||||
const existingSession = this.config.ptyManager.getSession(sessionId);
|
||||
if (!existingSession) {
|
||||
// This is a new external session, PtyManager needs to track it
|
||||
console.log(chalk.green(`Attaching to external session: ${sessionId}`));
|
||||
// PtyManager will pick it up through its own session listing
|
||||
// since it reads from the control directory
|
||||
}
|
||||
}
|
||||
|
||||
// If we're a remote server registered with HQ, immediately notify HQ
|
||||
if (this.config.hqClient && !isShuttingDown()) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Version information for VibeTunnel Server
|
||||
// This file is updated during the build process
|
||||
|
||||
export const VERSION = '1.0.0';
|
||||
export const VERSION = '1.0.0-beta.3';
|
||||
// BUILD_DATE will be replaced by build script, fallback to current time in dev
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || new Date().toISOString();
|
||||
export const BUILD_TIMESTAMP = process.env.BUILD_TIMESTAMP || Date.now();
|
||||
|
|
@ -31,4 +31,4 @@ export function printVersionBanner() {
|
|||
console.log(`Built: ${BUILD_DATE}`);
|
||||
console.log(`Platform: ${PLATFORM}/${ARCH} Node ${NODE_VERSION}`);
|
||||
console.log(`PID: ${process.pid}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue