diff --git a/bun.lock b/bun.lock index edacee9f..2515b40d 100644 --- a/bun.lock +++ b/bun.lock @@ -38,12 +38,12 @@ "globals": "^17.6.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.1", - "vitest": "^4.1.9", + "vitest": "^3.2.0", }, }, "packages/app": { "name": "@prover-coder-ai/docker-git", - "version": "1.3.10", + "version": "1.3.12", "bin": { "docker-git": "dist/src/docker-git/main.js", }, @@ -154,7 +154,7 @@ }, "packages/docker-git-session-sync": { "name": "@prover-coder-ai/docker-git-session-sync", - "version": "1.0.66", + "version": "1.0.68", "bin": { "docker-git-session-sync": "dist/docker-git-session-sync.js", }, @@ -714,6 +714,56 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], @@ -850,19 +900,19 @@ "@vitest/eslint-plugin": ["@vitest/eslint-plugin@1.6.20", "", { "dependencies": { "@typescript-eslint/scope-manager": "^8.58.0", "@typescript-eslint/utils": "^8.58.0" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "*", "eslint": ">=8.57.0", "typescript": ">=5.0.0", "vitest": "*" }, "optionalPeers": ["@typescript-eslint/eslint-plugin", "typescript", "vitest"] }, "sha512-xRwWHFG0Utp6hXtbGiWk4VdKXCGdExD8kbWrrmFEiG5dk8anOJ+vbWbeOa8EbkocKQRTsx7JAWETccZiBgFp/Q=="], - "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + "@vitest/expect": ["@vitest/expect@3.2.6", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ=="], - "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + "@vitest/mocker": ["@vitest/mocker@3.2.6", "", { "dependencies": { "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw=="], - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.6", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA=="], - "@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + "@vitest/runner": ["@vitest/runner@3.2.6", "", { "dependencies": { "@vitest/utils": "3.2.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q=="], - "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + "@vitest/snapshot": ["@vitest/snapshot@3.2.6", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw=="], - "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + "@vitest/spy": ["@vitest/spy@3.2.6", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg=="], - "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + "@vitest/utils": ["@vitest/utils@3.2.6", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], @@ -940,6 +990,8 @@ "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "get-intrinsic": "1.3.0", "set-function-length": "1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -952,7 +1004,7 @@ "canonicalize": ["canonicalize@2.1.0", "", { "bin": { "canonicalize": "bin/canonicalize.js" } }, "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ=="], - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "4.3.0", "supports-color": "7.2.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -968,6 +1020,8 @@ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "cheerio": ["cheerio@1.1.2", "", { "dependencies": { "cheerio-select": "2.1.0", "dom-serializer": "2.0.0", "domhandler": "5.0.3", "domutils": "3.2.2", "encoding-sniffer": "0.2.1", "htmlparser2": "10.0.0", "parse5": "7.3.0", "parse5-htmlparser2-tree-adapter": "7.1.0", "parse5-parser-stream": "7.1.2", "undici": "7.16.0", "whatwg-mimetype": "4.0.0" } }, "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg=="], "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "1.0.0", "css-select": "5.2.2", "css-what": "6.2.2", "domelementtype": "2.3.0", "domhandler": "5.0.3", "domutils": "3.2.2" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], @@ -1028,6 +1082,8 @@ "dedent": ["dedent@1.7.0", "", {}, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "1.0.1", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], @@ -1464,6 +1520,8 @@ "loop-controls": ["loop-controls@1.1.0", "", {}, "sha512-otnxF3ngIuLecg99p7On7nJF6ws1mT2kNOiGOPFykEHQfhJtdsjcQMxM4LEHsUi3LeMrm2Ic0hFdykJcG0N1YQ=="], + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1608,6 +1666,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], @@ -1714,6 +1774,8 @@ "rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="], + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "1.2.3" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "1.0.8", "call-bound": "1.0.4", "get-intrinsic": "1.3.0", "has-symbols": "1.1.0", "isarray": "2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -1782,7 +1844,7 @@ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "1.3.0", "internal-slot": "1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], @@ -1808,6 +1870,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + "structured-field-values": ["structured-field-values@2.0.4", "", {}, "sha512-5zpJXYLPwW3WYUD/D58tQjIBs10l3Yx64jZfcKGs/RH79E2t9Xm/b9+ydwdMNVSksnsIY+HR/2IlQmgo0AcTAg=="], "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], @@ -1820,11 +1884,15 @@ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -1894,9 +1962,11 @@ "vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="], + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="], - "vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "vitest": ["vitest@3.2.6", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.6", "@vitest/mocker": "3.2.6", "@vitest/pretty-format": "^3.2.6", "@vitest/runner": "3.2.6", "@vitest/snapshot": "3.2.6", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.6", "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -1972,6 +2042,8 @@ "@digitalbazaar/http-client/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], + "@effect-template/lib/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "@effect/experimental/@effect/platform": ["@effect/platform@0.96.0", "", { "dependencies": { "find-my-way-ts": "0.1.6", "msgpackr": "1.11.5", "multipasta": "0.2.7" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-U7PLhkVzg7zzrgFvyWATOzD6reL87KG/fcdOxgLWBQ/J5CCU6qdPAVG+0o6o+IxcsLoqGwxs+rFxaFzrdtDV1A=="], "@effect/experimental/effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "fast-check": "3.23.2" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], @@ -2044,6 +2116,14 @@ "@prover-coder-ai/dist-deps-prune/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@prover-coder-ai/docker-git/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + + "@prover-coder-ai/docker-git-container/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + + "@prover-coder-ai/docker-git-session-sync/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + + "@prover-coder-ai/docker-git-terminal/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node": ["@effect/platform-node@0.106.0", "", { "dependencies": { "@effect/platform-node-shared": "0.59.0", "mime": "3.0.0", "undici": "7.16.0", "ws": "8.18.3" }, "peerDependencies": { "@effect/cluster": "0.58.0", "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "effect": "3.21.0" } }, "sha512-mpsJK2jNLVd0jQAjHKBo8j3wdKWznSGvfnKBcAuG/9Rr4mb8bMRZFLXHHT9wUP7EvnZ0tDZJgEDxkC+j+ByRag=="], "@prover-coder-ai/eslint-plugin-suggest-members/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", "@typescript-eslint/typescript-estree": "8.57.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg=="], @@ -2072,6 +2152,14 @@ "@types/ws/@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@vitest/coverage-v8/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@vitest/coverage-v8/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "@vitest/coverage-v8/vitest": ["vitest@4.1.9", "", { "dependencies": { "@vitest/expect": "4.1.9", "@vitest/mocker": "4.1.9", "@vitest/pretty-format": "4.1.9", "@vitest/runner": "4.1.9", "@vitest/snapshot": "4.1.9", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.9", "@vitest/browser-preview": "4.1.9", "@vitest/browser-webdriverio": "4.1.9", "@vitest/coverage-istanbul": "4.1.9", "@vitest/coverage-v8": "4.1.9", "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ=="], + "@vitest/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "@typescript-eslint/visitor-keys": "8.61.0" } }, "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA=="], "@vitest/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.61.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", "@typescript-eslint/typescript-estree": "8.61.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA=="], @@ -2170,10 +2258,20 @@ "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "1.0.10", "esprima": "4.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "rollup/@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "1.2.8" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "vite-node/vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], + + "vitest/vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], + "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": "2.1.2" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -2194,6 +2292,26 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@effect-template/lib/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@effect-template/lib/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@effect-template/lib/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@effect-template/lib/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@effect-template/lib/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@effect-template/lib/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@effect-template/lib/vitest/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@effect-template/lib/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@effect-template/lib/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "@effect-template/lib/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "@effect/experimental/@effect/platform/msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "3.0.3" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], "@effect/experimental/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], @@ -2226,6 +2344,10 @@ "@effect/vitest/vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "@effect/vitest/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@effect/vitest/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "@effect/vitest/vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "6.5.0", "picomatch": "4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "@effect/vitest/vitest/tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], @@ -2302,6 +2424,86 @@ "@prover-coder-ai/dist-deps-prune/effect/fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "@prover-coder-ai/docker-git-container/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@prover-coder-ai/docker-git-container/vitest/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@prover-coder-ai/docker-git-container/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@prover-coder-ai/docker-git-container/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "@prover-coder-ai/docker-git-container/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@prover-coder-ai/docker-git-terminal/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@prover-coder-ai/docker-git-terminal/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "@prover-coder-ai/docker-git-terminal/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], + + "@prover-coder-ai/docker-git/vitest/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "@prover-coder-ai/docker-git/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "@prover-coder-ai/docker-git/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node/@effect/cluster": ["@effect/cluster@0.58.0", "", { "dependencies": { "kubernetes-types": "1.30.0" }, "peerDependencies": { "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "@effect/sql": "0.51.0", "@effect/workflow": "0.18.0", "effect": "3.21.0" } }, "sha512-0Zog7s7XdntWcTqdqWPoj6nc7hPaWIzp0k0DsFUWyCynXNPK9dAtgFrSce04NhddNqqbhtZck/lhuqJwNBrprQ=="], "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node/@effect/platform": ["@effect/platform@0.96.0", "", { "dependencies": { "find-my-way-ts": "0.1.6", "msgpackr": "1.11.5", "multipasta": "0.2.7" }, "peerDependencies": { "effect": "3.21.0" } }, "sha512-U7PLhkVzg7zzrgFvyWATOzD6reL87KG/fcdOxgLWBQ/J5CCU6qdPAVG+0o6o+IxcsLoqGwxs+rFxaFzrdtDV1A=="], @@ -2342,6 +2544,22 @@ "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@vitest/coverage-v8/vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], + + "@vitest/coverage-v8/vitest/@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], + + "@vitest/coverage-v8/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.9", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A=="], + + "@vitest/coverage-v8/vitest/@vitest/runner": ["@vitest/runner@4.1.9", "", { "dependencies": { "@vitest/utils": "4.1.9", "pathe": "^2.0.3" } }, "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg=="], + + "@vitest/coverage-v8/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], + + "@vitest/coverage-v8/vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], + + "@vitest/coverage-v8/vitest/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "@vitest/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.61.0", "", {}, "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg=="], "@vitest/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.61.0", "", { "dependencies": { "@typescript-eslint/types": "8.61.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ=="], @@ -2458,6 +2676,8 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@effect-template/lib/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@effect/experimental/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], "@effect/printer-ansi/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], @@ -2470,6 +2690,8 @@ "@effect/vitest/vitest/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@effect/vitest/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@effect/vitest/vitest/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "3.3.11", "picocolors": "1.1.1", "source-map-js": "1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], "@effect/vitest/vitest/vite/rolldown": ["rolldown@1.0.0-rc.10", "", { "dependencies": { "@oxc-project/types": "0.120.0", "@rolldown/pluginutils": "1.0.0-rc.10" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA=="], @@ -2498,6 +2720,14 @@ "@prover-coder-ai/dist-deps-prune/effect/fast-check/pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "@prover-coder-ai/docker-git-container/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "@prover-coder-ai/docker-git-session-sync/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "@prover-coder-ai/docker-git-terminal/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "@prover-coder-ai/docker-git/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node/@effect/cluster/@effect/workflow": ["@effect/workflow@0.18.0", "", { "peerDependencies": { "@effect/experimental": "0.60.0", "@effect/platform": "0.96.0", "@effect/rpc": "0.75.0", "effect": "3.21.0" } }, "sha512-9Zp+x9ADtR0H6CRhU6wLyPcIRjO1PXjvSpUlFlBQ8piw7ldjPmnUWEY8YQuH6eExV2dalQ4z2LMiZ5Bd7XAJbA=="], "@prover-coder-ai/eslint-plugin-suggest-members/@effect/platform-node/@effect/platform/msgpackr": ["msgpackr@1.11.5", "", { "optionalDependencies": { "msgpackr-extract": "3.0.3" } }, "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA=="], @@ -2526,6 +2756,8 @@ "@ton-ai-core/vibecode-linter/jscpd/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "@vitest/coverage-v8/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "@vitest/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.61.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.61.0", "@typescript-eslint/types": "^8.61.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA=="], "@vitest/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.61.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ=="], diff --git a/packages/api/package.json b/packages/api/package.json index d2b9f357..7c025c90 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -54,6 +54,6 @@ "globals": "^17.6.0", "typescript": "^6.0.3", "typescript-eslint": "^8.61.1", - "vitest": "^4.1.9" + "vitest": "^3.2.0" } } diff --git a/packages/api/src/api/contracts.ts b/packages/api/src/api/contracts.ts index e6efd92d..efdc4b97 100644 --- a/packages/api/src/api/contracts.ts +++ b/packages/api/src/api/contracts.ts @@ -809,3 +809,29 @@ export type ApiEvent = { readonly at: string readonly payload: unknown } + +export type ShareLinkInfo = { + readonly token: string + readonly projectKey: string + readonly projectDir: string + readonly displayName: string + readonly sshAlias: string + readonly sshConfigSnippet: string + readonly cfSshConfigSnippet: string | null + readonly vscodeUri: string + readonly cfVscodeUri: string | null + readonly workspacePath: string + readonly sshPassword: string | null + readonly createdAt: string + readonly expiresAt: string +} + +export type CreateShareLinkRequest = { + readonly ttlMs?: number | undefined +} + +export type CreateShareLinkResponse = { + readonly ok: true + readonly link: ShareLinkInfo + readonly url: string +} diff --git a/packages/api/src/api/openapi.ts b/packages/api/src/api/openapi.ts index 4c4da351..4944752e 100644 --- a/packages/api/src/api/openapi.ts +++ b/packages/api/src/api/openapi.ts @@ -30,6 +30,10 @@ import { ProjectDatabaseSessionSchema, ProjectPortForwardRequestSchema, ProjectSkillUpdateRequestSchema, + CreateShareLinkRequestSchema, + ShareLinkInfoResponseSchema, + CreateShareLinkResponseSchema, + ShareLinksListResponseSchema, StartPanelCloudflareTunnelRequestSchema, StartProjectTerminalSessionRequestSchema, UpProjectRequestSchema @@ -735,6 +739,8 @@ const TasksGroup = HttpApiGroup.make("tasks") .addSuccess(OutputResponseSchema) ) +const ShareLinkTokenParam = HttpApiSchema.param("token", Schema.String) + const SharingGroup = HttpApiGroup.make("sharing") .add(endpoint.get("readPanelCloudflareTunnel", "/cloudflare-tunnels/panel").addSuccess(PanelCloudflareTunnelResponseSchema)) .add( @@ -743,6 +749,14 @@ const SharingGroup = HttpApiGroup.make("sharing") .addSuccess(PanelCloudflareTunnelResponseSchema, { status: 202 }) ) .add(endpoint.del("stopPanelCloudflareTunnel", "/cloudflare-tunnels/panel").addSuccess(PanelCloudflareTunnelResponseSchema)) + .add(endpoint.get("getShareLink")`/share-links/${ShareLinkTokenParam}`.addSuccess(ShareLinkInfoResponseSchema)) + .add( + endpoint.post("createShareLink")`/projects/by-key/${ProjectKeyParam}/share-links` + .setPayload(CreateShareLinkRequestSchema) + .addSuccess(CreateShareLinkResponseSchema, { status: 201 }) + ) + .add(endpoint.get("listShareLinks")`/projects/by-key/${ProjectKeyParam}/share-links`.addSuccess(ShareLinksListResponseSchema)) + .add(endpoint.del("deleteShareLink")`/projects/by-key/${ProjectKeyParam}/share-links/${ShareLinkTokenParam}`.addSuccess(OkResponseSchema)) export const DockerGitApi = HttpApi.make("docker-git") .annotate(OpenApi.Title, "docker-git API") diff --git a/packages/api/src/api/schema.ts b/packages/api/src/api/schema.ts index 6412840b..92d8a1e7 100644 --- a/packages/api/src/api/schema.ts +++ b/packages/api/src/api/schema.ts @@ -392,3 +392,37 @@ export type ProjectPortForwardRequestInput = Schema.Schema.Type export type CreateAgentRequestInput = Schema.Schema.Type export type CreateFollowRequestInput = Schema.Schema.Type + +export const CreateShareLinkRequestSchema = Schema.Struct({ + ttlMs: Schema.optional(Schema.Number) +}) + +export const ShareLinkInfoSchema = Schema.Struct({ + token: Schema.String, + projectKey: Schema.String, + projectDir: Schema.String, + displayName: Schema.String, + sshAlias: Schema.String, + sshConfigSnippet: Schema.String, + cfSshConfigSnippet: Schema.NullOr(Schema.String), + vscodeUri: Schema.String, + cfVscodeUri: Schema.NullOr(Schema.String), + workspacePath: Schema.String, + sshPassword: Schema.NullOr(Schema.String), + createdAt: Schema.String, + expiresAt: Schema.String +}) + +export const ShareLinkInfoResponseSchema = Schema.Struct({ + link: ShareLinkInfoSchema +}) + +export const CreateShareLinkResponseSchema = Schema.Struct({ + ok: Schema.Literal(true), + link: ShareLinkInfoSchema, + url: Schema.String +}) + +export const ShareLinksListResponseSchema = Schema.Struct({ + links: Schema.Array(ShareLinkInfoSchema) +}) diff --git a/packages/api/src/http.ts b/packages/api/src/http.ts index 94c4752c..4d3d4212 100644 --- a/packages/api/src/http.ts +++ b/packages/api/src/http.ts @@ -38,6 +38,7 @@ import { ProjectPortForwardRequestSchema, ProjectPromptUpdateRequestSchema, ProjectSkillUpdateRequestSchema, + CreateShareLinkRequestSchema, StartProjectTerminalSessionRequestSchema, StartPanelCloudflareTunnelRequestSchema, StateCommitRequestSchema, @@ -130,6 +131,24 @@ import { startPanelCloudflareTunnel, stopPanelCloudflareTunnel } from "./services/panel-cloudflare-tunnel.js" +import { + createShareLink, + deleteShareLink, + listShareLinks, + resolveShareLink +} from "./services/project-share-links.js" +import { + getSshShareLinkTunnelHostname, + startSshShareLinkTunnel, + stopSshShareLinkTunnel +} from "./services/ssh-share-link-tunnels.js" +import { startSshProjectTunnel } from "./services/ssh-project-tunnels.js" +import { + disableContainerPasswordAuth, + enableContainerPasswordAuth, + generateSshPassword +} from "./services/ssh-password-setup.js" +import { buildShareLinkSshAccess } from "@effect-template/lib/usecases/ssh-access" import { deleteProjectDatabaseForward, deleteProjectDatabaseProfile, @@ -556,6 +575,13 @@ const skillScopeFromBody = (scope: string): ProjectSkillScope | null => const readProjectPortForwardRequest = () => HttpServerRequest.schemaBodyJson(ProjectPortForwardRequestSchema) const readStartPanelCloudflareTunnelRequest = () => HttpServerRequest.schemaBodyJson(StartPanelCloudflareTunnelRequestSchema) +const readCreateShareLinkRequest = () => + HttpServerRequest.schemaBodyJson(CreateShareLinkRequestSchema) + +const ShareLinkTokenParamsSchema = Schema.Struct({ token: Schema.String }) +const ShareLinkByProjectKeyParamsSchema = Schema.Struct({ projectKey: Schema.String, token: Schema.String }) +const shareLinkTokenParams = HttpRouter.schemaParams(ShareLinkTokenParamsSchema) +const shareLinkByProjectKeyParams = HttpRouter.schemaParams(ShareLinkByProjectKeyParamsSchema) const readProjectDatabaseProfileRequest = () => HttpServerRequest.schemaBodyJson(ProjectDatabaseProfileRequestSchema) const readStateInitRequest = () => HttpServerRequest.schemaBodyJson(StateInitRequestSchema) const readStateCommitRequest = () => HttpServerRequest.schemaBodyJson(StateCommitRequestSchema) @@ -1104,6 +1130,144 @@ export const makeRouter = () => { Effect.flatMap((tunnel) => jsonResponse({ tunnel }, 200)), Effect.catchAll(errorResponse) ) + ), + HttpRouter.get( + "/share-links/:token", + Effect.gen(function*(_) { + const request = yield* _(HttpServerRequest.HttpServerRequest) + const { token } = yield* _(shareLinkTokenParams) + const projectsRoot = defaultProjectsRoot(process.cwd()) + const link = yield* _(resolveShareLink(projectsRoot, token)) + if (link === null) { + return yield* _(Effect.fail(new ApiNotFoundError({ message: `Share link not found or expired: ${token}` }))) + } + const project = yield* _(getProjectItemByKey(link.projectKey)) + const clientHost = new URL(request.url, "http://localhost").searchParams.get("host") + ?? resolvePortPublicHost(request) + ?? "localhost" + const sshCfHostname = getSshShareLinkTunnelHostname(link.token) + const sshAccess = buildShareLinkSshAccess({ + containerName: project.containerName, + sshUser: project.sshUser, + sshPort: project.sshPort, + sshKeyPath: null, + targetDir: project.targetDir, + clientHost, + sshCfHostname + }) + const shareLinkInfo = { + token: link.token, + projectKey: link.projectKey, + projectDir: link.projectDir, + displayName: project.displayName, + sshAlias: sshAccess.alias, + sshConfigSnippet: sshAccess.configSnippet, + cfSshConfigSnippet: sshAccess.cfConfigSnippet, + vscodeUri: sshAccess.vscodeUri, + cfVscodeUri: sshAccess.cfVscodeUri, + workspacePath: sshAccess.workspacePath, + sshPassword: link.sshPassword ?? null, + createdAt: link.createdAt, + expiresAt: link.expiresAt + } + return yield* _(jsonResponse({ link: shareLinkInfo }, 200)) + }).pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.post( + "/projects/by-key/:projectKey/share-links", + Effect.gen(function*(_) { + const request = yield* _(HttpServerRequest.HttpServerRequest) + const { projectKey } = yield* _(projectKeyParams) + const body = yield* _(readCreateShareLinkRequest()) + const project = yield* _(getProjectItemByKey(projectKey)) + const projectsRoot = defaultProjectsRoot(process.cwd()) + const sshPassword = generateSshPassword() + yield* _( + enableContainerPasswordAuth(project.containerName, sshPassword).pipe( + Effect.orElse(() => Effect.void) + ) + ) + const link = yield* _(createShareLink(projectsRoot, project.projectDir, projectKey, sshPassword, body.ttlMs)) + const clientHost = resolvePortPublicHost(request) ?? "localhost" + const sshCfHostname = yield* _( + startSshShareLinkTunnel(link.token, project.sshPort).pipe( + Effect.orElse(() => Effect.succeed(null)) + ) + ) + const sshAccess = buildShareLinkSshAccess({ + containerName: project.containerName, + sshUser: project.sshUser, + sshPort: project.sshPort, + sshKeyPath: null, + targetDir: project.targetDir, + clientHost, + sshCfHostname + }) + const shareLinkInfo = { + token: link.token, + projectKey: link.projectKey, + projectDir: link.projectDir, + displayName: project.displayName, + sshAlias: sshAccess.alias, + sshConfigSnippet: sshAccess.configSnippet, + cfSshConfigSnippet: sshAccess.cfConfigSnippet, + vscodeUri: sshAccess.vscodeUri, + cfVscodeUri: sshAccess.cfVscodeUri, + workspacePath: sshAccess.workspacePath, + sshPassword, + createdAt: link.createdAt, + expiresAt: link.expiresAt + } + const url = `${resolveRequestOrigin(request)}/ssh/${encodeURIComponent(projectKey)}?t=${link.token}` + return yield* _(jsonResponse({ ok: true, link: shareLinkInfo, url }, 201)) + }).pipe(Effect.catchAll(errorResponse)) + ), + HttpRouter.get( + "/projects/by-key/:projectKey/share-links", + projectKeyParams.pipe( + Effect.flatMap(({ projectKey }) => + Effect.gen(function*(_) { + const project = yield* _(getProjectItemByKey(projectKey)) + const projectsRoot = defaultProjectsRoot(process.cwd()) + const links = yield* _(listShareLinks(projectsRoot, project.projectDir)) + return { links } + }) + ), + Effect.flatMap((payload) => jsonResponse(payload, 200)), + Effect.catchAll(errorResponse) + ) + ), + HttpRouter.del( + "/projects/by-key/:projectKey/share-links/:token", + shareLinkByProjectKeyParams.pipe( + Effect.flatMap(({ projectKey, token }) => + Effect.gen(function*(_) { + const project = yield* _(getProjectItemByKey(projectKey)) + const projectsRoot = defaultProjectsRoot(process.cwd()) + yield* _(deleteShareLink(projectsRoot, project.projectDir, token)) + yield* _(stopSshShareLinkTunnel(token)) + const remaining = yield* _(listShareLinks(projectsRoot, project.projectDir)) + if (remaining.length === 0) { + yield* _(disableContainerPasswordAuth(project.containerName)) + } + }) + ), + Effect.flatMap(() => jsonResponse({ ok: true }, 200)), + Effect.catchAll(errorResponse) + ) + ), + HttpRouter.post( + "/projects/by-key/:projectKey/ssh-tunnel", + Effect.gen(function*(_) { + const { projectKey } = yield* _(projectKeyParams) + const project = yield* _(getProjectItemByKey(projectKey)) + const result = yield* _( + startSshProjectTunnel(projectKey, project.sshPort, project.containerName).pipe( + Effect.orElse(() => Effect.succeed({ hostname: null, sshPassword: "" })) + ) + ) + return yield* _(jsonResponse(result, 200)) + }).pipe(Effect.catchAll(errorResponse)) ) ) diff --git a/packages/api/src/services/project-share-links.ts b/packages/api/src/services/project-share-links.ts new file mode 100644 index 00000000..87a8f4af --- /dev/null +++ b/packages/api/src/services/project-share-links.ts @@ -0,0 +1,150 @@ +// CHANGE: add URL-based container access sharing with time-limited tokens +// WHY: enables sharing container access via URL without exposing full panel +// QUOTE(ТЗ): "я даю эту же ссылку условно в VS Code и он подключается к текущему контейнеру" +// REF: issue-428 +// FORMAT THEOREM: ∀ token ∈ ShareLinks: valid(token) → project(token) ∧ notExpired(token) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: tokens are cryptographically random 16 hex chars (8 bytes = 2^64 space) +// COMPLEXITY: O(1) lookup via in-memory index, O(n) scan on cold start + +import * as FileSystem from "@effect/platform/FileSystem" +import { randomBytes } from "node:crypto" +import * as nodePath from "node:path" +import { Effect, Either } from "effect" +import * as ParseResult from "effect/ParseResult" +import * as Schema from "effect/Schema" + +import { ApiInternalError, ApiNotFoundError } from "../api/errors.js" + +export type ShareLink = { + readonly token: string + readonly projectKey: string + readonly projectDir: string + readonly createdAt: string + readonly expiresAt: string + readonly sshPassword: string | null +} + +type ShareLinksFile = { + readonly schemaVersion: 1 + readonly links: ReadonlyArray +} + +const ShareLinkSchema = Schema.Struct({ + token: Schema.String, + projectKey: Schema.String, + projectDir: Schema.String, + createdAt: Schema.String, + expiresAt: Schema.String, + sshPassword: Schema.optionalWith(Schema.NullOr(Schema.String), { default: () => null }) +}) + +const ShareLinksFileSchema = Schema.Struct({ + schemaVersion: Schema.Literal(1), + links: Schema.Array(ShareLinkSchema) +}) + +const ShareLinksFileJsonSchema = Schema.parseJson(ShareLinksFileSchema) + +const defaultShareLinksFile = (): ShareLinksFile => ({ schemaVersion: 1, links: [] }) + +const decodeShareLinksFile = (input: string): ShareLinksFile | null => + Either.match(ParseResult.decodeUnknownEither(ShareLinksFileJsonSchema)(input), { + onLeft: () => null, + onRight: (value) => value + }) + +// Global file at projectsRoot/share-links.json holds all tokens across projects. +// This avoids scanning project directories on every token lookup. +const resolveGlobalStatePath = (projectsRoot: string): string => + nodePath.join(nodePath.resolve(projectsRoot), "share-links.json") + +const defaultTtlMs = 7 * 24 * 60 * 60 * 1000 + +const readShareLinksFile = ( + projectsRoot: string +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const statePath = resolveGlobalStatePath(projectsRoot) + const exists = yield* _(Effect.either(fs.exists(statePath))) + const fileExists = Either.match(exists, { onLeft: () => false, onRight: (v) => v }) + if (!fileExists) { + return defaultShareLinksFile() + } + const contents = yield* _(Effect.either(fs.readFileString(statePath))) + return Either.match(contents, { + onLeft: () => defaultShareLinksFile(), + onRight: (raw) => decodeShareLinksFile(raw) ?? defaultShareLinksFile() + }) + }).pipe(Effect.catchAll(() => Effect.succeed(defaultShareLinksFile()))) + +const writeShareLinksFile = ( + projectsRoot: string, + file: ShareLinksFile +): Effect.Effect => + Effect.gen(function*(_) { + const fs = yield* _(FileSystem.FileSystem) + const statePath = resolveGlobalStatePath(projectsRoot) + yield* _( + fs.writeFileString(statePath, JSON.stringify(file, null, 2)).pipe( + Effect.catchAll((err) => + Effect.fail(new ApiInternalError({ message: `Failed to write share-links: ${String(err)}` })) + ) + ) + ) + }) + +const isExpired = (link: ShareLink): boolean => new Date(link.expiresAt).getTime() <= Date.now() + +export const createShareLink = ( + projectsRoot: string, + projectDir: string, + projectKey: string, + sshPassword: string | null, + ttlMs?: number +): Effect.Effect => + Effect.gen(function*(_) { + const token = randomBytes(8).toString("hex") + const now = new Date().toISOString() + const expiresAt = new Date(Date.now() + (ttlMs ?? defaultTtlMs)).toISOString() + const link: ShareLink = { token, projectKey, projectDir, createdAt: now, expiresAt, sshPassword } + const file = yield* _(readShareLinksFile(projectsRoot)) + const activeLinks = file.links.filter((l) => !isExpired(l)) + yield* _(writeShareLinksFile(projectsRoot, { schemaVersion: 1, links: [...activeLinks, link] })) + return link + }) + +export const resolveShareLink = ( + projectsRoot: string, + token: string +): Effect.Effect => + readShareLinksFile(projectsRoot).pipe( + Effect.map((file) => file.links.find((l) => l.token === token && !isExpired(l)) ?? null), + Effect.catchAll(() => Effect.succeed(null)) + ) + +export const deleteShareLink = ( + projectsRoot: string, + projectDir: string, + token: string +): Effect.Effect => + Effect.gen(function*(_) { + const file = yield* _(readShareLinksFile(projectsRoot)) + const before = file.links.length + const updated = file.links.filter((l) => !(l.token === token && l.projectDir === projectDir)) + if (updated.length === before) { + return yield* _(Effect.fail(new ApiNotFoundError({ message: `Share link not found: ${token}` }))) + } + yield* _(writeShareLinksFile(projectsRoot, { schemaVersion: 1, links: updated })) + }) + +export const listShareLinks = ( + projectsRoot: string, + projectDir: string +): Effect.Effect, never, FileSystem.FileSystem> => + readShareLinksFile(projectsRoot).pipe( + Effect.map((file) => file.links.filter((l) => l.projectDir === projectDir && !isExpired(l))), + Effect.catchAll(() => Effect.succeed([] as ReadonlyArray)) + ) diff --git a/packages/api/src/services/ssh-password-setup.ts b/packages/api/src/services/ssh-password-setup.ts new file mode 100644 index 00000000..da8e7f71 --- /dev/null +++ b/packages/api/src/services/ssh-password-setup.ts @@ -0,0 +1,88 @@ +// CHANGE: enable/disable SSH password auth in workspace containers for share links +// WHY: containers default to PasswordAuthentication no + locked dev user; +// share links need password access so recipients without SSH keys can connect +// QUOTE(ТЗ): "можно сделать что бы работало по паролю? Без ssh ключа?" +// REF: issue-428 +// FORMAT THEOREM: ∀ container: enabled(container, pw) → sshable(container, pw) +// PURITY: SHELL +// EFFECT: spawns docker exec processes +// INVARIANT: dev.conf always restored to PasswordAuthentication no on disable +// COMPLEXITY: O(1) per call (one docker exec each) + +import { execFile } from "node:child_process" +import { randomBytes } from "node:crypto" +import { promisify } from "node:util" + +import { Effect } from "effect" + +import { ApiInternalError } from "../api/errors.js" + +const execFileAsync = promisify(execFile) + +// Avoids ambiguous chars: 0/O, 1/l/I +const PASSWORD_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789" + +export const generateSshPassword = (): string => { + const bytes = randomBytes(12) + return Array.from(bytes).map((b) => PASSWORD_CHARS[b % PASSWORD_CHARS.length]).join("") +} + +const dockerExec = ( + containerName: string, + env: Record, + script: string +): Effect.Effect => + Effect.tryPromise({ + catch: (cause) => new ApiInternalError({ message: `docker exec ${containerName} failed`, cause }), + try: async () => { + const envArgs = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]) + const { stdout } = await execFileAsync("docker", ["exec", ...envArgs, containerName, "sh", "-c", script]) + return stdout + } + }) + +/** + * Enables password SSH auth for the dev user in a workspace container. + * Sets PasswordAuthentication yes in sshd_config.d/dev.conf, sets the given + * password on the dev user, then reloads sshd. + * + * @pure false + * @effect docker exec: sshd_config mutation, chpasswd, sshd reload + * @invariant subsequent `ssh dev@host -p port` with the password will succeed + * @throws Never – errors are typed as ApiInternalError + */ +export const enableContainerPasswordAuth = ( + containerName: string, + password: string +): Effect.Effect => { + const script = [ + "sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config.d/dev.conf", + "printf 'dev:%s' \"$SSHPW\" | chpasswd", + "kill -HUP $(pgrep -xo sshd) 2>/dev/null || true" + ].join(" && ") + return dockerExec(containerName, { SSHPW: password }, script).pipe(Effect.asVoid) +} + +/** + * Disables password SSH auth for the dev user in a workspace container. + * Restores PasswordAuthentication no and locks the dev user account. + * Called when the last share link for a container is deleted. + * + * @pure false + * @effect docker exec: sshd_config mutation, passwd lock, sshd reload + * @invariant dev user is locked; PasswordAuthentication reverted to no + * @throws Never – best-effort, errors are silently swallowed + */ +export const disableContainerPasswordAuth = ( + containerName: string +): Effect.Effect => { + const script = [ + "sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config.d/dev.conf", + "passwd -l dev", + "kill -HUP $(pgrep -xo sshd) 2>/dev/null || true" + ].join(" && ") + return dockerExec(containerName, {}, script).pipe( + Effect.asVoid, + Effect.orElse(() => Effect.void) + ) +} diff --git a/packages/api/src/services/ssh-project-tunnels.ts b/packages/api/src/services/ssh-project-tunnels.ts new file mode 100644 index 00000000..c2212ef8 --- /dev/null +++ b/packages/api/src/services/ssh-project-tunnels.ts @@ -0,0 +1,280 @@ +// CHANGE: per-project SSH cloudflared tunnels for VS Code Remote SSH access +// WHY: share-link tunnels are tied to tokens and expire; containers need a +// persistent tunnel that lives as long as the container is running +// QUOTE(ТЗ): "запускать cloudflare tunnel под каждый контейнер" +// REF: issue-428 +// FORMAT THEOREM: ∀ projectKey: started(projectKey, port) → ∃ hostname: cfSsh(hostname, port) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: at most one tunnel record per projectKey is active at any time +// COMPLEXITY: O(1) lookup, O(startWaitAttempts * 250ms) start wait + +import { spawn, type ChildProcess } from "node:child_process" +import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs" +import { join } from "node:path" +import { randomUUID } from "node:crypto" + +import { Duration, Effect, Fiber } from "effect" + +import { ApiInternalError } from "../api/errors.js" +import { parseTryCloudflareUrl } from "./panel-cloudflare-tunnel-core.js" +import { parseLinuxDefaultGatewayIp } from "./project-port-proxy-core.js" +import { generateSshPassword, enableContainerPasswordAuth } from "./ssh-password-setup.js" + +type SshTunnelRecord = { + readonly homeDir: string + process: ChildProcess | null + processClosed: boolean + hostname: string | null + sshPassword: string + stopping: boolean + stopFiber: Fiber.RuntimeFiber | null + stdoutRemainder: string + stderrRemainder: string +} + +const projectTunnelMap = new Map() +const projectTunnelLock = Effect.unsafeMakeSemaphore(1) +const startWaitAttempts = 60 + +const sshTunnelHomeDir = (id: string): string => join("/tmp", "docker-git-project-tunnels", id) + +const processEnv = (homeDir: string): Readonly> => ({ + HOME: homeDir, + NO_COLOR: "1", + PATH: process.env["PATH"], + SSL_CERT_DIR: process.env["SSL_CERT_DIR"], + SSL_CERT_FILE: process.env["SSL_CERT_FILE"] +}) + +const readDefaultGatewayIp = (): Effect.Effect => + Effect.try(() => parseLinuxDefaultGatewayIp(readFileSync("/proc/net/route", "utf8"))).pipe( + Effect.orElse(() => Effect.succeed(null)) + ) + +const defaultLocalhostHost = (): Effect.Effect => { + const configured = process.env["DOCKER_GIT_PANEL_TUNNEL_LOCALHOST_HOST"]?.trim() + if (configured !== undefined && configured.length > 0) { + return Effect.succeed(configured) + } + return existsSync("/.dockerenv") + ? readDefaultGatewayIp().pipe(Effect.map((ip) => ip ?? "172.17.0.1")) + : Effect.succeed("127.0.0.1") +} + +const appendLog = (record: SshTunnelRecord, text: string): void => { + if (record.hostname !== null) return + const url = parseTryCloudflareUrl(text) + if (url === null) return + try { + record.hostname = new URL(url).hostname + } catch { + // ignore malformed URL + } +} + +const consumeChunk = ( + record: SshTunnelRecord, + stream: "stderr" | "stdout", + chunk: Buffer +): void => { + const incoming = chunk.toString("utf8").replaceAll("\r", "\n") + const withRemainder = (stream === "stdout" ? record.stdoutRemainder : record.stderrRemainder) + incoming + const lines = withRemainder.split("\n") + const tail = lines.pop() ?? "" + for (const line of lines) { + appendLog(record, line) + } + if (stream === "stdout") { + record.stdoutRemainder = tail + } else { + record.stderrRemainder = tail + } +} + +const cleanupRecord = (record: SshTunnelRecord): void => { + try { + rmSync(record.homeDir, { force: true, recursive: true }) + } catch { + // best effort + } +} + +const waitForChildClose = ( + record: SshTunnelRecord, + child: ChildProcess +): Effect.Effect => { + if (record.processClosed) { + return Effect.void + } + return Effect.async((resume) => { + const alreadyExited = child.exitCode !== null || child.signalCode !== null + let completed = false + let killTimer: ReturnType | null = null + const complete = (): void => { + if (completed) return + completed = true + child.off("close", complete) + child.off("error", complete) + if (killTimer !== null) clearTimeout(killTimer) + resume(Effect.void) + } + child.once("close", complete) + child.once("error", complete) + if (!alreadyExited && !child.killed) { + try { + child.kill("SIGTERM") + } catch { + complete() + return + } + } + if (!alreadyExited) { + killTimer = setTimeout(() => { + try { + if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL") + } catch { + complete() + } + }, 2_000) + killTimer.unref() + } + }) +} + +const stopRecord = (record: SshTunnelRecord): Effect.Effect => { + if (record.stopFiber !== null) { + return Fiber.join(record.stopFiber).pipe(Effect.asVoid) + } + const child = record.process + record.stopping = true + const fiber = Effect.runFork( + (child === null ? Effect.void : waitForChildClose(record, child)).pipe( + Effect.tap(() => Effect.sync(() => { cleanupRecord(record) })) + ) + ) + record.stopFiber = fiber + return Fiber.join(fiber).pipe(Effect.asVoid) +} + +const attachHandlers = (record: SshTunnelRecord, child: ChildProcess): void => { + record.process = child + record.processClosed = false + child.stdout?.on("data", (chunk: Buffer) => { consumeChunk(record, "stdout", chunk) }) + child.stderr?.on("data", (chunk: Buffer) => { consumeChunk(record, "stderr", chunk) }) + child.on("close", () => { record.processClosed = true }) + child.on("error", () => { record.processClosed = true }) +} + +const waitForHostname = ( + record: SshTunnelRecord, + remainingAttempts: number +): Effect.Effect => + Effect.gen(function*(_) { + if (record.hostname !== null || record.stopping || record.processClosed || remainingAttempts <= 0) { + return record.hostname + } + yield* _(Effect.sleep(Duration.millis(250))) + return yield* _(waitForHostname(record, remainingAttempts - 1)) + }) + +/** + * Starts a dedicated SSH cloudflared quick tunnel for the given project. + * Idempotent — returns existing hostname if a tunnel is already running. + * + * @param projectKey - Project key used as the map key (one tunnel per project). + * @param sshPort - Host-mapped SSH port for the container. + * @returns CF hostname (e.g. "abc.trycloudflare.com") or null if startup timed out. + * @pure false + * @effect Spawns cloudflared process, reads /proc/net/route, writes to /tmp. + * @invariant Only one active record per projectKey — prior record is stopped before restart. + * @precondition sshPort > 0 + * @postcondition On success, getSshProjectTunnelHostname(projectKey) returns the same hostname. + * @complexity O(startWaitAttempts * 250ms) time for startup wait. + * @throws Never - failures are typed as ApiInternalError in the Effect error channel. + */ +export const startSshProjectTunnel = ( + projectKey: string, + sshPort: number, + containerName: string +): Effect.Effect<{ hostname: string | null; sshPassword: string }, ApiInternalError> => + Effect.gen(function*(_) { + const existing = projectTunnelMap.get(projectKey) + if (existing !== undefined && !existing.stopping && !existing.processClosed && existing.hostname !== null) { + return { hostname: existing.hostname, sshPassword: existing.sshPassword } + } + if (existing !== undefined) { + yield* _(stopRecord(existing).pipe(Effect.orElse(() => Effect.void))) + projectTunnelMap.delete(projectKey) + } + + const sshPassword = generateSshPassword() + yield* _(enableContainerPasswordAuth(containerName, sshPassword)) + + const localhostHost = yield* _(defaultLocalhostHost()) + const sshUrl = `ssh://${localhostHost}:${sshPort}` + const homeDir = sshTunnelHomeDir(randomUUID()) + const record: SshTunnelRecord = { + homeDir, + hostname: null, + process: null, + processClosed: false, + sshPassword, + stderrRemainder: "", + stdoutRemainder: "", + stopFiber: null, + stopping: false + } + projectTunnelMap.set(projectKey, record) + + yield* _( + Effect.try({ + catch: (cause) => new ApiInternalError({ message: "Failed to start project SSH cloudflared tunnel.", cause }), + try: () => { + mkdirSync(record.homeDir, { recursive: true }) + const child = spawn( + "cloudflared", + ["tunnel", "--no-autoupdate", "--url", sshUrl], + { + cwd: process.cwd(), + env: processEnv(record.homeDir), + stdio: ["ignore", "pipe", "pipe"] + } + ) + attachHandlers(record, child) + } + }) + ) + + const hostname = yield* _(waitForHostname(record, startWaitAttempts)) + return { hostname, sshPassword } + }).pipe(projectTunnelLock.withPermits(1)) + +/** + * Stops and removes the SSH cloudflared tunnel for the given project key. + * + * @param projectKey - Project key whose tunnel should be stopped. + * @pure false + * @effect Sends SIGTERM/SIGKILL to cloudflared, removes tunnel home directory. + * @invariant No-op when no tunnel exists for the projectKey. + * @complexity O(process close timeout) time. + * @throws Never - this effect has no typed failure channel. + */ +export const stopSshProjectTunnel = (projectKey: string): Effect.Effect => + Effect.gen(function*(_) { + const record = projectTunnelMap.get(projectKey) + if (record === undefined) return + projectTunnelMap.delete(projectKey) + yield* _(stopRecord(record).pipe(Effect.orElse(() => Effect.void))) + }).pipe(projectTunnelLock.withPermits(1)) + +/** + * Returns the current CF hostname for the SSH tunnel associated with the given project key. + * + * @param projectKey - Project key to look up. + * @returns CF hostname string or null if tunnel not running / hostname not yet available. + * @pure true (read-only snapshot) + * @complexity O(1) + */ +export const getSshProjectTunnelHostname = (projectKey: string): string | null => + projectTunnelMap.get(projectKey)?.hostname ?? null diff --git a/packages/api/src/services/ssh-share-link-tunnels.ts b/packages/api/src/services/ssh-share-link-tunnels.ts new file mode 100644 index 00000000..8cf7df63 --- /dev/null +++ b/packages/api/src/services/ssh-share-link-tunnels.ts @@ -0,0 +1,272 @@ +// CHANGE: per-share-link SSH cloudflared tunnels for VS Code Remote SSH access +// WHY: the panel CF tunnel is HTTP-only and cannot forward raw SSH traffic; +// VS Code Remote SSH needs a dedicated ssh:// origin tunnel per container +// QUOTE(ТЗ): "давай поднимем отдельный SSH тунель на контейнер" +// REF: issue-428 +// FORMAT THEOREM: ∀ token: started(token, port) → ∃ hostname: cfSsh(hostname, port) +// PURITY: SHELL +// EFFECT: Effect +// INVARIANT: at most one tunnel record per share-link token is active at any time +// COMPLEXITY: O(1) lookup, O(startWaitAttempts * 250ms) start wait + +import { spawn, type ChildProcess } from "node:child_process" +import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs" +import { join } from "node:path" +import { randomUUID } from "node:crypto" + +import { Duration, Effect, Fiber } from "effect" + +import { ApiInternalError } from "../api/errors.js" +import { parseTryCloudflareUrl } from "./panel-cloudflare-tunnel-core.js" +import { parseLinuxDefaultGatewayIp } from "./project-port-proxy-core.js" + +type SshTunnelRecord = { + readonly homeDir: string + process: ChildProcess | null + processClosed: boolean + hostname: string | null + stopping: boolean + stopFiber: Fiber.RuntimeFiber | null + stdoutRemainder: string + stderrRemainder: string +} + +const tunnelMap = new Map() +const tunnelLock = Effect.unsafeMakeSemaphore(1) +const startWaitAttempts = 60 + +const sshTunnelHomeDir = (id: string): string => join("/tmp", "docker-git-ssh-tunnels", id) + +const processEnv = (homeDir: string): Readonly> => ({ + HOME: homeDir, + NO_COLOR: "1", + PATH: process.env["PATH"], + SSL_CERT_DIR: process.env["SSL_CERT_DIR"], + SSL_CERT_FILE: process.env["SSL_CERT_FILE"] +}) + +const readDefaultGatewayIp = (): Effect.Effect => + Effect.try(() => parseLinuxDefaultGatewayIp(readFileSync("/proc/net/route", "utf8"))).pipe( + Effect.orElse(() => Effect.succeed(null)) + ) + +const defaultLocalhostHost = (): Effect.Effect => { + const configured = process.env["DOCKER_GIT_PANEL_TUNNEL_LOCALHOST_HOST"]?.trim() + if (configured !== undefined && configured.length > 0) { + return Effect.succeed(configured) + } + return existsSync("/.dockerenv") + ? readDefaultGatewayIp().pipe(Effect.map((ip) => ip ?? "172.17.0.1")) + : Effect.succeed("127.0.0.1") +} + +const appendLog = (record: SshTunnelRecord, text: string): void => { + if (record.hostname !== null) return + const url = parseTryCloudflareUrl(text) + if (url === null) return + try { + record.hostname = new URL(url).hostname + } catch { + // ignore malformed URL + } +} + +const consumeChunk = ( + record: SshTunnelRecord, + stream: "stderr" | "stdout", + chunk: Buffer +): void => { + const incoming = chunk.toString("utf8").replaceAll("\r", "\n") + const withRemainder = (stream === "stdout" ? record.stdoutRemainder : record.stderrRemainder) + incoming + const lines = withRemainder.split("\n") + const tail = lines.pop() ?? "" + for (const line of lines) { + appendLog(record, line) + } + if (stream === "stdout") { + record.stdoutRemainder = tail + } else { + record.stderrRemainder = tail + } +} + +const cleanupRecord = (record: SshTunnelRecord): void => { + try { + rmSync(record.homeDir, { force: true, recursive: true }) + } catch { + // best effort + } +} + +const waitForChildClose = ( + record: SshTunnelRecord, + child: ChildProcess +): Effect.Effect => { + if (record.processClosed) { + return Effect.void + } + return Effect.async((resume) => { + const alreadyExited = child.exitCode !== null || child.signalCode !== null + let completed = false + let killTimer: ReturnType | null = null + const complete = (): void => { + if (completed) return + completed = true + child.off("close", complete) + child.off("error", complete) + if (killTimer !== null) clearTimeout(killTimer) + resume(Effect.void) + } + child.once("close", complete) + child.once("error", complete) + if (!alreadyExited && !child.killed) { + try { + child.kill("SIGTERM") + } catch { + complete() + return + } + } + if (!alreadyExited) { + killTimer = setTimeout(() => { + try { + if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL") + } catch { + complete() + } + }, 2_000) + killTimer.unref() + } + }) +} + +const stopRecord = (record: SshTunnelRecord): Effect.Effect => { + if (record.stopFiber !== null) { + return Fiber.join(record.stopFiber).pipe(Effect.asVoid) + } + const child = record.process + record.stopping = true + const fiber = Effect.runFork( + (child === null ? Effect.void : waitForChildClose(record, child)).pipe( + Effect.tap(() => Effect.sync(() => { cleanupRecord(record) })) + ) + ) + record.stopFiber = fiber + return Fiber.join(fiber).pipe(Effect.asVoid) +} + +const attachHandlers = (record: SshTunnelRecord, child: ChildProcess): void => { + record.process = child + record.processClosed = false + child.stdout?.on("data", (chunk: Buffer) => { consumeChunk(record, "stdout", chunk) }) + child.stderr?.on("data", (chunk: Buffer) => { consumeChunk(record, "stderr", chunk) }) + child.on("close", () => { record.processClosed = true }) + child.on("error", () => { record.processClosed = true }) +} + +const waitForHostname = ( + record: SshTunnelRecord, + remainingAttempts: number +): Effect.Effect => + Effect.gen(function*(_) { + if (record.hostname !== null || record.stopping || remainingAttempts <= 0) { + return record.hostname + } + yield* _(Effect.sleep(Duration.millis(250))) + return yield* _(waitForHostname(record, remainingAttempts - 1)) + }) + +/** + * Starts a dedicated SSH cloudflared quick tunnel for the given share link token. + * The tunnel forwards `ssh://gatewayIp:sshPort` and returns the CF hostname once ready. + * + * @param token - Share link token used as the map key (one tunnel per link). + * @param sshPort - Host-mapped SSH port for the container. + * @returns CF hostname (e.g. "abc.trycloudflare.com") or null if startup timed out. + * @pure false + * @effect Spawns cloudflared process, reads /proc/net/route, writes to /tmp. + * @invariant Only one active record per token — prior record is stopped before restart. + * @precondition sshPort > 0, token matches /^[0-9a-f]{16}$/u + * @postcondition On success, getSshShareLinkTunnelHostname(token) returns the same hostname. + * @complexity O(startWaitAttempts * 250ms) time for startup wait. + * @throws Never - failures are typed as ApiInternalError in the Effect error channel. + */ +export const startSshShareLinkTunnel = ( + token: string, + sshPort: number +): Effect.Effect => + Effect.gen(function*(_) { + const existing = tunnelMap.get(token) + if (existing !== undefined && !existing.stopping && existing.hostname !== null) { + return existing.hostname + } + if (existing !== undefined) { + yield* _(stopRecord(existing).pipe(Effect.orElse(() => Effect.void))) + tunnelMap.delete(token) + } + + const localhostHost = yield* _(defaultLocalhostHost()) + const sshUrl = `ssh://${localhostHost}:${sshPort}` + const homeDir = sshTunnelHomeDir(randomUUID()) + const record: SshTunnelRecord = { + homeDir, + hostname: null, + process: null, + processClosed: false, + stderrRemainder: "", + stdoutRemainder: "", + stopFiber: null, + stopping: false + } + tunnelMap.set(token, record) + + yield* _( + Effect.try({ + catch: (cause) => new ApiInternalError({ message: "Failed to start SSH cloudflared tunnel.", cause }), + try: () => { + mkdirSync(record.homeDir, { recursive: true }) + const child = spawn( + "cloudflared", + ["tunnel", "--no-autoupdate", "--url", sshUrl], + { + cwd: process.cwd(), + env: processEnv(record.homeDir), + stdio: ["ignore", "pipe", "pipe"] + } + ) + attachHandlers(record, child) + } + }) + ) + + return yield* _(waitForHostname(record, startWaitAttempts)) + }).pipe(tunnelLock.withPermits(1)) + +/** + * Stops and removes the SSH cloudflared tunnel for the given share link token. + * + * @param token - Share link token whose tunnel should be stopped. + * @pure false + * @effect Sends SIGTERM/SIGKILL to cloudflared, removes tunnel home directory. + * @invariant No-op when no tunnel exists for the token. + * @complexity O(process close timeout) time. + * @throws Never - this effect has no typed failure channel. + */ +export const stopSshShareLinkTunnel = (token: string): Effect.Effect => + Effect.gen(function*(_) { + const record = tunnelMap.get(token) + if (record === undefined) return + tunnelMap.delete(token) + yield* _(stopRecord(record).pipe(Effect.orElse(() => Effect.void))) + }).pipe(tunnelLock.withPermits(1)) + +/** + * Returns the current CF hostname for the SSH tunnel associated with the given token. + * + * @param token - Share link token to look up. + * @returns CF hostname string or null if tunnel not running / hostname not yet available. + * @pure true (read-only snapshot) + * @complexity O(1) + */ +export const getSshShareLinkTunnelHostname = (token: string): string | null => + tunnelMap.get(token)?.hostname ?? null diff --git a/packages/app/src/web/api-share-links.ts b/packages/app/src/web/api-share-links.ts new file mode 100644 index 00000000..2f689e18 --- /dev/null +++ b/packages/app/src/web/api-share-links.ts @@ -0,0 +1,87 @@ +import * as Schema from "@effect/schema/Schema" +import { Effect } from "effect" + +import { requestJson } from "./api-http.js" + +const ShareLinkInfoSchema = Schema.Struct({ + token: Schema.String, + projectKey: Schema.String, + projectDir: Schema.String, + displayName: Schema.String, + sshAlias: Schema.String, + sshConfigSnippet: Schema.String, + cfSshConfigSnippet: Schema.NullOr(Schema.String), + vscodeUri: Schema.String, + cfVscodeUri: Schema.NullOr(Schema.String), + workspacePath: Schema.String, + sshPassword: Schema.NullOr(Schema.String), + createdAt: Schema.String, + expiresAt: Schema.String +}) + +export type ShareLinkInfo = Schema.Schema.Type + +const ShareLinkResponseSchema = Schema.Struct({ link: ShareLinkInfoSchema }) +const CreateShareLinkResponseSchema = Schema.Struct({ + ok: Schema.Literal(true), + link: ShareLinkInfoSchema, + url: Schema.String +}) +const ShareLinksListResponseSchema = Schema.Struct({ + links: Schema.Array(ShareLinkInfoSchema) +}) +const OkResponseSchema = Schema.Struct({ ok: Schema.Literal(true) }) + +export const loadShareLink = ( + token: string, + clientHost: string +): Effect.Effect => + requestJson( + "GET", + `/share-links/${encodeURIComponent(token)}?host=${encodeURIComponent(clientHost)}`, + ShareLinkResponseSchema + ).pipe(Effect.map((r) => r.link)) + +export const createProjectShareLink = ( + projectKey: string, + ttlMs?: number +): Effect.Effect<{ readonly link: ShareLinkInfo; readonly url: string }, string> => + requestJson( + "POST", + `/projects/by-key/${encodeURIComponent(projectKey)}/share-links`, + CreateShareLinkResponseSchema, + ttlMs === undefined ? {} : { ttlMs } + ).pipe(Effect.map(({ link, url }) => ({ link, url }))) + +export const listProjectShareLinks = ( + projectKey: string +): Effect.Effect, string> => + requestJson( + "GET", + `/projects/by-key/${encodeURIComponent(projectKey)}/share-links`, + ShareLinksListResponseSchema + ).pipe(Effect.map((r) => r.links)) + +export const deleteProjectShareLink = ( + projectKey: string, + token: string +): Effect.Effect => + requestJson( + "DELETE", + `/projects/by-key/${encodeURIComponent(projectKey)}/share-links/${encodeURIComponent(token)}`, + OkResponseSchema + ).pipe(Effect.asVoid) + +const SshTunnelResponseSchema = Schema.Struct({ + hostname: Schema.NullOr(Schema.String), + sshPassword: Schema.String +}) + +export const startProjectSshTunnel = ( + projectKey: string +): Effect.Effect<{ readonly hostname: string | null; readonly sshPassword: string }, string> => + requestJson( + "POST", + `/projects/by-key/${encodeURIComponent(projectKey)}/ssh-tunnel`, + SshTunnelResponseSchema + ) diff --git a/packages/app/src/web/app-ready-main-panels.tsx b/packages/app/src/web/app-ready-main-panels.tsx index d362fc63..bbdf4583 100644 --- a/packages/app/src/web/app-ready-main-panels.tsx +++ b/packages/app/src/web/app-ready-main-panels.tsx @@ -61,6 +61,7 @@ const ShareScreen = (props: MainPanelsProps): JSX.Element => ( onRefresh={props.onRefreshPanelShareTunnel} onStart={props.onStartPanelShareTunnel} onStop={props.onStopPanelShareTunnel} + selectedProjectKey={props.selectedProjectSummary?.projectKey ?? null} tunnel={props.panelCloudflareTunnel} /> diff --git a/packages/app/src/web/app-ready-terminal-pane.tsx b/packages/app/src/web/app-ready-terminal-pane.tsx index 5e36b41f..ae9dd216 100644 --- a/packages/app/src/web/app-ready-terminal-pane.tsx +++ b/packages/app/src/web/app-ready-terminal-pane.tsx @@ -1,11 +1,19 @@ import { Effect } from "effect" -import type { CSSProperties, JSX } from "react" +import { type CSSProperties, type JSX, useState } from "react" import { deleteTerminalSessionByPath } from "./api.js" import { canOpenProjectBrowser } from "./app-ready-browser-openable.js" import { TerminalTaskManagerBody } from "./app-ready-terminal-task-manager.js" import type { TerminalPaneProps } from "./app-ready-terminal-types.js" import { TerminalPanel } from "./panel-terminal.js" +import { VsCodeAccessPanel } from "./panel-vscode-access-panel.js" +import { + buildVsCodeAccessInfo, + type CfTunnelState, + startTunnel, + useTunnelAutoStart, + useTunnelPolling +} from "./panel-vscode-access.js" import { type BrowserScreen, projectPickerScreen } from "./screen.js" import type { TerminalExitInfo } from "./terminal-panel-runtime-types.js" import { terminalSessionId } from "./terminal-state.js" @@ -187,8 +195,14 @@ const openTerminalAction = (props: TerminalPaneProps, runtime: TerminalPaneRunti } const TerminalPanelForPane = ( - { bodyContent, props, runtime }: { + { + bodyContent, + onOpenVsCode, + props, + runtime + }: { readonly bodyContent: JSX.Element | undefined + readonly onOpenVsCode: (() => void) | undefined readonly props: TerminalPaneProps readonly runtime: TerminalPaneRuntime } @@ -222,16 +236,41 @@ const TerminalPanelForPane = ( onOpenTaskManager={openTaskManagerAction(props, runtime)} onOpenTerminal={openTerminalAction(props, runtime)} onMessage={props.onTerminalMessage} + onOpenVsCode={onOpenVsCode} session={props.terminalSession} /> ) export const TerminalPane = (props: TerminalPaneProps): JSX.Element => { + const [isVsCodePanelOpen, setVsCodePanelOpen] = useState(false) + const [cfState, setCfState] = useState({ tag: "idle" }) const runtime = resolveTerminalPaneRuntime(props) - const bodyContent = terminalBodyContent(props, runtime) + useTunnelAutoStart(isVsCodePanelOpen, cfState, runtime.browserProjectKey, setCfState) + useTunnelPolling(isVsCodePanelOpen, cfState, runtime.browserProjectKey, setCfState) + const vsCodeInfo = buildVsCodeAccessInfo(props.project) + const onOpenVsCode = vsCodeInfo === null ? undefined : () => { + setVsCodePanelOpen(true) + } + const refresh = (): void => { + if (runtime.browserProjectKey !== undefined) startTunnel(runtime.browserProjectKey, setCfState) + } + const vsCodeBodyContent = isVsCodePanelOpen && vsCodeInfo !== null + ? ( + { + setVsCodePanelOpen(false) + }} + onRefresh={refresh} + onRetry={refresh} + /> + ) + : undefined + const bodyContent = vsCodeBodyContent ?? terminalBodyContent(props, runtime) return (
- +
) } diff --git a/packages/app/src/web/app-share-link-sections.tsx b/packages/app/src/web/app-share-link-sections.tsx new file mode 100644 index 00000000..3053556b --- /dev/null +++ b/packages/app/src/web/app-share-link-sections.tsx @@ -0,0 +1,289 @@ +import { Effect } from "effect" +import { type CSSProperties, type Dispatch, type JSX, type SetStateAction } from "react" + +import type { ShareLinkInfo } from "./api-share-links.js" +import { deleteTerminalSessionByPath } from "./api.js" +import { buttonStyle, codeBlockStyle, copyText, vscodeLinkStyle } from "./app-share-link-utils.js" +import { TerminalPanel } from "./panel-terminal.js" +import { type ActiveTerminalSession } from "./terminal.js" +import type { ViewportLayout } from "./viewport-layout.js" + +export { buttonStyle, centeredBoxStyle, codeBlockStyle, copyText, vscodeLinkStyle } from "./app-share-link-utils.js" + +export type ShareLinkState = + | { readonly _tag: "Loading" } + | { readonly _tag: "Error"; readonly message: string } + | { readonly _tag: "Info"; readonly info: ShareLinkInfo } + | { readonly _tag: "Connecting"; readonly info: ShareLinkInfo } + | { + readonly _tag: "Terminal" + readonly info: ShareLinkInfo + readonly session: ActiveTerminalSession + readonly message: string | null + } + | { readonly _tag: "Closed"; readonly info: ShareLinkInfo; readonly closedMessage: string } + +const sshPasswordBlockStyle: CSSProperties = { + background: "#0d1a14", + border: "1px solid #2a5a38", + borderRadius: "3px", + marginTop: "8px", + padding: "8px" +} + +const terminalAreaStyle: CSSProperties = { + display: "flex", + flex: 1, + flexDirection: "column", + minHeight: 0, + overflow: "hidden" +} + +export const SshConfigBlock = ( + { label, snippet }: { readonly label: string; readonly snippet: string } +): JSX.Element => ( +
+
{label}
+ {snippet} + +
+) + +const WILDCARD_SSH_CONFIG = `Host *.trycloudflare.com + ProxyCommand cloudflared access ssh --hostname %h + StrictHostKeyChecking no + UserKnownHostsFile /dev/null` + +const CfSshConnectSection = ( + { cfHostname }: { readonly cfHostname: string } +): JSX.Element => ( + <> +
After setup, connect to any container:
+ {`ssh dev@${cfHostname}`} + +
+ Requires{" "} + + cloudflared + {" "} + installed on your machine +
+ +) + +export const CfTunnelSetupBlock = ( + { cfHostname }: { readonly cfHostname: string | null } +): JSX.Element | null => { + if (cfHostname === null) return null + return ( +
+
+
One-time setup
+
+ add once to ~/.ssh/config — works for all share links +
+
+ {WILDCARD_SSH_CONFIG} + + +
+ ) +} + +const SshDirectLanSection = ( + { directCmd }: { readonly directCmd: string } +): JSX.Element | null => + directCmd === "" + ? null + : ( + <> +
Direct (LAN):
+ {directCmd} + + + ) + +const parseSshSnippet = (snippet: string): { hostname: string; port: string; user: string } => ({ + hostname: /HostName\s+(\S+)/.exec(snippet)?.[1] ?? "host", + port: /Port\s+(\d+)/.exec(snippet)?.[1] ?? "22", + user: /User\s+(\S+)/.exec(snippet)?.[1] ?? "dev" +}) + +export const SshPasswordBlock = ( + { info }: { readonly info: ShareLinkInfo } +): JSX.Element | null => { + if (info.sshPassword === null) return null + const { hostname, port, user } = parseSshSnippet(info.sshConfigSnippet) + const directCmd = hostname === "localhost" ? "" : `ssh ${user}@${hostname} -p ${port}` + return ( +
+
SSH password
+
+ {info.sshPassword} + +
+ +
+ ) +} + +export const InfoHeader = ( + { + info, + isConnecting, + onConnect + }: { + readonly info: ShareLinkInfo + readonly isConnecting: boolean + readonly onConnect: () => void + } +): JSX.Element => ( +
+
+
{info.displayName}
+
+ open in VS Code + {info.cfVscodeUri !== null && ( + VS Code (CF tunnel) + )} + +
+
+ + + +
+ expires {new Date(info.expiresAt).toLocaleString()} +
+
+) + +export const TerminalView = ( + { + message, + session, + setState, + viewport + }: { + readonly message: string | null + readonly session: ActiveTerminalSession + readonly setState: Dispatch> + readonly viewport: ViewportLayout + } +): JSX.Element => ( +
+ {message !== null && ( +
{message}
+ )} + { + setState((current) => + current._tag === "Terminal" + ? { _tag: "Closed", closedMessage: "Terminal attach failed.", info: current.info } + : current + ) + }} + onDetach={() => { + setState((current) => current._tag === "Terminal" ? { _tag: "Info", info: current.info } : current) + }} + onKill={() => { + void Effect.runPromise( + deleteTerminalSessionByPath(session.closePath).pipe(Effect.either, Effect.asVoid) + ) + setState((current) => current._tag === "Terminal" ? { _tag: "Info", info: current.info } : current) + }} + onMessage={(msg) => { + setState((current) => current._tag === "Terminal" ? { ...current, message: msg } : current) + }} + session={session} + /> +
+) + +export const PlaceholderArea = ({ children }: { readonly children: JSX.Element }): JSX.Element => ( +
+ {children} +
+) diff --git a/packages/app/src/web/app-share-link-utils.ts b/packages/app/src/web/app-share-link-utils.ts new file mode 100644 index 00000000..0885c894 --- /dev/null +++ b/packages/app/src/web/app-share-link-utils.ts @@ -0,0 +1,46 @@ +import { type CSSProperties } from "react" + +export const codeBlockStyle: CSSProperties = { + background: "#0b1017", + border: "1px solid #2a3640", + borderRadius: "2px", + color: "#a8c8f0", + display: "block", + fontFamily: "inherit", + fontSize: "0.85em", + marginBottom: "4px", + marginTop: "4px", + overflowX: "auto", + padding: "6px 8px", + whiteSpace: "pre" +} + +export const buttonStyle: CSSProperties = { + background: "transparent", + border: "none", + color: "#56f39a", + cursor: "pointer", + font: "inherit", + fontWeight: "bold", + padding: "2px 6px" +} + +export const vscodeLinkStyle: CSSProperties = { + color: "#56f39a", + cursor: "pointer", + fontFamily: "inherit", + fontSize: "inherit", + fontWeight: "bold", + padding: "2px 6px", + textDecoration: "none" +} + +export const centeredBoxStyle: CSSProperties = { + border: "1px solid #3a4652", + borderRadius: "4px", + color: "#d6e5f7", + padding: "16px 24px", + textAlign: "center" +} + +export const copyText = (text: string): void => void navigator.clipboard.writeText(text) diff --git a/packages/app/src/web/app-share-link.tsx b/packages/app/src/web/app-share-link.tsx new file mode 100644 index 00000000..edabe9e0 --- /dev/null +++ b/packages/app/src/web/app-share-link.tsx @@ -0,0 +1,188 @@ +import { Effect, Match } from "effect" +import { type CSSProperties, type Dispatch, type JSX, type SetStateAction, useEffect, useState } from "react" + +import { loadShareLink } from "./api-share-links.js" +import type { ShareLinkInfo } from "./api-share-links.js" +import { createProjectTerminalSession } from "./api.js" +import { + centeredBoxStyle, + InfoHeader, + PlaceholderArea, + type ShareLinkState, + TerminalView +} from "./app-share-link-sections.js" +import { buildProjectActiveTerminalSession } from "./terminal.js" +import type { ViewportLayout } from "./viewport-layout.js" + +export type AppShareLinkProps = { + readonly projectKey: string + readonly shareToken: string + readonly viewport: ViewportLayout +} + +const containerStyle: CSSProperties = { + display: "flex", + flexDirection: "column", + height: "100%", + overflow: "hidden", + padding: "8px" +} + +const connectTerminalSession = ( + projectKey: string, + info: ShareLinkInfo, + setState: Dispatch> +): void => { + void Effect.runPromise( + createProjectTerminalSession(projectKey).pipe( + Effect.map(({ session }) => { + const activeSession = buildProjectActiveTerminalSession({ + onExit: () => { + setState((current) => current._tag === "Terminal" ? { _tag: "Info", info: current.info } : current) + }, + projectDisplayName: info.displayName, + projectId: session.projectId, + projectKey, + session + }) + setState((current) => + current._tag === "Connecting" + ? { _tag: "Terminal", info: current.info, message: null, session: activeSession } + : current + ) + }), + Effect.catchAll((error) => + Effect.sync(() => { + setState((current) => + current._tag === "Connecting" + ? { _tag: "Closed", closedMessage: error, info: current.info } + : current + ) + }) + ) + ) + ) +} + +const renderInfoCase = ( + info: ShareLinkInfo, + projectKey: string, + setState: Dispatch> +): JSX.Element => ( +
+ { + setState({ _tag: "Connecting", info }) + connectTerminalSession(projectKey, info, setState) + }} + /> + +
+
Add the one-time setup to ~/.ssh/config
+
+ then click VS Code (CF tunnel) to connect from anywhere +
+
+
+
+) + +const renderClosedCase = ( + info: ShareLinkInfo, + closedMessage: string, + projectKey: string, + setState: Dispatch> +): JSX.Element => ( +
+ { + setState({ _tag: "Connecting", info }) + connectTerminalSession(projectKey, info, setState) + }} + /> + +
+
Session ended
+
{closedMessage}
+
+
+
+) + +const renderState = ( + state: ShareLinkState, + setState: Dispatch>, + projectKey: string, + viewport: ViewportLayout +): JSX.Element => + Match.value(state).pipe( + Match.when({ _tag: "Loading" }, () => ( + +
+
Share link
+
Validating token…
+
+
+ )), + Match.when({ _tag: "Error" }, ({ message }) => ( + +
+
Share link unavailable
+
{message}
+
+
+ )), + Match.when({ _tag: "Info" }, ({ info }) => renderInfoCase(info, projectKey, setState)), + Match.when({ _tag: "Connecting" }, ({ info }) => ( +
+ {}} /> + +
+
Starting SSH terminal session…
+
+
+
+ )), + Match.when({ _tag: "Terminal" }, ({ info, message, session }) => ( +
+ {}} /> + +
+ )), + Match.when({ _tag: "Closed" }, ({ closedMessage, info }) => + renderClosedCase(info, closedMessage, projectKey, setState)), + Match.exhaustive + ) + +export const AppShareLink = ( + { projectKey, shareToken, viewport }: AppShareLinkProps +): JSX.Element => { + const [state, setState] = useState({ _tag: "Loading" }) + + useEffect(() => { + let isCancelled = false + const portSuffix = location.port === "" ? "" : `:${location.port}` + const clientHost = `${location.hostname}${portSuffix}` + void Effect.runPromise( + loadShareLink(shareToken, clientHost).pipe( + Effect.match({ + onFailure: (message) => { + if (!isCancelled) setState({ _tag: "Error", message }) + }, + onSuccess: (info) => { + if (!isCancelled) setState({ _tag: "Info", info }) + } + }) + ) + ) + return () => { + isCancelled = true + } + }, [shareToken]) + + return renderState(state, setState, projectKey, viewport) +} diff --git a/packages/app/src/web/app-terminal-session-core.ts b/packages/app/src/web/app-terminal-session-core.ts index 2b3c8060..21cc79cc 100644 --- a/packages/app/src/web/app-terminal-session-core.ts +++ b/packages/app/src/web/app-terminal-session-core.ts @@ -1,7 +1,11 @@ +import { Effect } from "effect" + import type { ProjectTerminalSessionLookup } from "./api.js" import { type ActiveTerminalSession, buildProjectActiveTerminalSession } from "./terminal.js" -export type WebAppRoute = { readonly tag: "Dashboard" } +export type WebAppRoute = + | { readonly tag: "Dashboard" } + | { readonly tag: "ShareLink"; readonly projectKey: string; readonly shareToken: string } const terminalSessionRoutePrefix = "/ssh/session/" @@ -15,8 +19,33 @@ export const readTerminalSessionRoute = (pathname: string): string | null => { return sessionId.length === 0 ? null : sessionId } -export const resolveWebAppRoute = (_pathname: string): WebAppRoute => { - return { tag: "Dashboard" } +// CHANGE: detect 16-hex share tokens in /ssh/:projectKey?t=:token URLs +// WHY: share tokens (16 hex chars) are distinct from terminal UUIDs (dashed UUID format) +// QUOTE(ТЗ): "принимает ссылку на любой IP который стоит у пользователя в URL" +// REF: issue-428 +// FORMAT THEOREM: ∀t: isShareToken(t) ↔ t ∈ [0-9a-f]{16} +// PURITY: CORE +// INVARIANT: UUID terminal IDs always have dashes — never match the hex-only pattern +// COMPLEXITY: O(1) +const isShareToken = (value: string): boolean => /^[0-9a-f]{16}$/u.test(value) + +const safeDecodeSegment = (value: string): string | null => + Effect.runSync( + Effect.try(() => decodeURIComponent(value)).pipe( + Effect.catchAll(() => Effect.succeed(null)) + ) + ) + +const tryResolveShareLink = (pathname: string, t: string): WebAppRoute | null => { + const rawKey = pathname.slice("/ssh/".length).split("/", 1)[0] ?? "" + const projectKey = safeDecodeSegment(rawKey)?.trim() ?? "" + return projectKey.length > 0 && isShareToken(t) ? { tag: "ShareLink", projectKey, shareToken: t } : null +} + +export const resolveWebAppRoute = (pathname: string, search = ""): WebAppRoute => { + if (!pathname.startsWith("/ssh/")) return { tag: "Dashboard" } + const t = new URLSearchParams(search).get("t") ?? "" + return tryResolveShareLink(pathname, t) ?? { tag: "Dashboard" } } export const buildTerminalOnlyActiveSession = ( diff --git a/packages/app/src/web/app.tsx b/packages/app/src/web/app.tsx index 17d4df06..19eaec4f 100644 --- a/packages/app/src/web/app.tsx +++ b/packages/app/src/web/app.tsx @@ -15,6 +15,7 @@ import { UiProvider } from "../ui/primitives.js" import { loadDashboard, resolveApiBaseUrl } from "./api.js" import { createDashboardRefreshReducer, type DashboardState } from "./app-dashboard-state.js" import { AppReady } from "./app-ready.js" +import { AppShareLink } from "./app-share-link.js" import { resolveWebAppRoute } from "./app-terminal-session-core.js" import { ErrorScreen, LoadingScreen } from "./panels.js" import { resolveViewportLayout, type ViewportLayout, type ViewportSize } from "./viewport-layout.js" @@ -221,12 +222,18 @@ const AppDashboard = ({ viewport }: { readonly viewport: ViewportLayout }): JSX. export const App = (): JSX.Element => { const viewport = useViewportMode() - const [route] = useState(() => resolveWebAppRoute(location.pathname)) + const [route] = useState(() => resolveWebAppRoute(location.pathname, location.search)) return ( {Match.value(route).pipe( Match.when({ tag: "Dashboard" }, () => ), + Match.when( + { tag: "ShareLink" }, + ({ projectKey, shareToken }) => ( + + ) + ), Match.exhaustive )} diff --git a/packages/app/src/web/panel-share.tsx b/packages/app/src/web/panel-share.tsx index 8c2bc832..17c04ff2 100644 --- a/packages/app/src/web/panel-share.tsx +++ b/packages/app/src/web/panel-share.tsx @@ -1,6 +1,9 @@ -import type { JSX } from "react" +import { Effect } from "effect" +import { type Dispatch, type JSX, type SetStateAction, useCallback, useEffect, useRef, useState } from "react" import { Box, Text } from "../ui/primitives.js" +import type { ShareLinkInfo } from "./api-share-links.js" +import { createProjectShareLink, deleteProjectShareLink, listProjectShareLinks } from "./api-share-links.js" import type { PanelCloudflareTunnelSession } from "./api.js" type SharePanelProps = { @@ -8,6 +11,7 @@ type SharePanelProps = { readonly onRefresh: () => void readonly onStart: () => void readonly onStop: () => void + readonly selectedProjectKey: string | null readonly tunnel: PanelCloudflareTunnelSession | null } @@ -125,8 +129,157 @@ const MaybeTunnelError = ( ? null : {tunnel.error} -const tunnelLogTail = (tunnel: PanelCloudflareTunnelSession | null): ReadonlyArray => - tunnel === null ? [] : tunnel.logTail +type ShareLinksState = + | { readonly _tag: "Idle" } + | { readonly _tag: "Loading" } + | { readonly _tag: "Loaded"; readonly links: ReadonlyArray; readonly newUrl: string | null } + | { readonly _tag: "Error"; readonly message: string } + +const runRefreshLinks = ( + projectKey: string, + id: number, + idRef: { readonly current: number }, + setState: Dispatch> +): void => { + setState({ _tag: "Loading" }) + void Effect.runPromise( + listProjectShareLinks(projectKey).pipe( + Effect.match({ + onFailure: (msg) => { + if (idRef.current === id) setState({ _tag: "Error", message: msg }) + }, + onSuccess: (links) => { + if (idRef.current !== id) return + setState((s) => ({ _tag: "Loaded", links, newUrl: s._tag === "Loaded" ? s.newUrl : null })) + } + }) + ) + ) +} + +const runGenerateLink = ( + projectKey: string, + id: number, + idRef: { readonly current: number }, + setState: Dispatch> +): void => { + void Effect.runPromise( + createProjectShareLink(projectKey).pipe( + Effect.flatMap(({ url }) => + listProjectShareLinks(projectKey).pipe( + Effect.map((links) => { + if (idRef.current === id) setState({ _tag: "Loaded", links, newUrl: url }) + }) + ) + ), + Effect.catchAll((msg) => + Effect.sync(() => { + if (idRef.current === id) setState({ _tag: "Error", message: msg }) + }) + ) + ) + ) +} + +const runRevokeLink = ( + projectKey: string, + token: string, + id: number, + idRef: { readonly current: number }, + setState: Dispatch> +): void => { + void Effect.runPromise( + deleteProjectShareLink(projectKey, token).pipe( + Effect.flatMap(() => listProjectShareLinks(projectKey)), + Effect.match({ + onFailure: (msg) => { + if (idRef.current === id) setState({ _tag: "Error", message: msg }) + }, + onSuccess: (links) => { + if (idRef.current !== id) return + setState((s) => ({ _tag: "Loaded", links, newUrl: s._tag === "Loaded" ? s.newUrl : null })) + } + }) + ) + ) +} + +const ShareLinkNewBanner = ( + { newUrl }: { readonly newUrl: string } +): JSX.Element => ( + + New share link + {newUrl} + + { + void navigator.clipboard.writeText(newUrl) + }} + /> + { + openUrl(newUrl) + }} + /> + + +) + +const ShareLinkRow = ( + { link, onRevoke }: { readonly link: ShareLinkInfo; readonly onRevoke: (token: string) => void } +): JSX.Element => ( + + {link.token} + exp {new Date(link.expiresAt).toLocaleDateString()} + { + onRevoke(link.token) + }} + /> + +) + +const ContainerShareLinksSection = ( + { projectKey }: { readonly projectKey: string } +): JSX.Element => { + const [state, setState] = useState({ _tag: "Idle" }) + const idRef = useRef(0) + const refresh = useCallback(() => { + runRefreshLinks(projectKey, ++idRef.current, idRef, setState) + }, [projectKey]) + const generate = useCallback(() => { + runGenerateLink(projectKey, ++idRef.current, idRef, setState) + }, [projectKey]) + const revoke = useCallback((token: string) => { + runRevokeLink(projectKey, token, ++idRef.current, idRef, setState) + }, [projectKey]) + useEffect(() => { + refresh() + }, [refresh]) + return ( + + + Container share links + + + + + + Share links give time-limited SSH and terminal access to this container. + {state._tag === "Loading" && Loading…} + {state._tag === "Error" && {state.message}} + {state._tag === "Loaded" && state.newUrl !== null && } + {state._tag === "Loaded" && state.links.length === 0 && No active share links.} + {state._tag === "Loaded" && + state.links.map((link) => )} + + ) +} export const SharePanel = ( { @@ -134,6 +287,7 @@ export const SharePanel = ( onRefresh, onStart, onStop, + selectedProjectKey, tunnel }: SharePanelProps ): JSX.Element => { @@ -157,7 +311,8 @@ export const SharePanel = ( - + + {selectedProjectKey !== null && } ) } diff --git a/packages/app/src/web/panel-vscode-access-panel.tsx b/packages/app/src/web/panel-vscode-access-panel.tsx new file mode 100644 index 00000000..c02f480f --- /dev/null +++ b/packages/app/src/web/panel-vscode-access-panel.tsx @@ -0,0 +1,231 @@ +import { type CSSProperties, type JSX } from "react" + +import type { CfTunnelState, VsCodeAccessInfo } from "./panel-vscode-access.js" + +type VsCodeAccessPanelProps = { + readonly cfState: CfTunnelState + readonly info: VsCodeAccessInfo + readonly onClose: () => void + readonly onRefresh: () => void + readonly onRetry: () => void +} + +type CfReadyDetailsProps = { + readonly cfSshConfig: string + readonly cfSshCommand: string + readonly cfVscodeUri: string | null + readonly sshPassword: string +} + +type DirectSshSectionProps = { + readonly cfReadyPassword: string | null + readonly directCommand: string + readonly directConfig: string + readonly directVscodeUri: string +} + +type VsCodeAccessValues = { + readonly cfSshConfig: string | null + readonly cfSshCommand: string | null + readonly cfVscodeUri: string | null + readonly directConfig: string + readonly directCommand: string + readonly directVscodeUri: string +} + +const hostSshConfig = (hostname: string, sshUser: string): string => + `Host ${hostname}\n User ${sshUser}\n ProxyCommand cloudflared access ssh --hostname %h\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null` + +const directSshConfig = (host: string, sshPort: number, sshUser: string): string => + `Host ${host}-ssh\n HostName ${host}\n Port ${sshPort}\n User ${sshUser}\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null` + +const copyText = (text: string): void => { + void navigator.clipboard.writeText(text) +} + +const codeStyle: CSSProperties = { + background: "#0b1017", + border: "1px solid #2a3640", + borderRadius: "2px", + color: "#a8c8f0", + display: "block", + fontFamily: "inherit", + fontSize: "0.85em", + marginBottom: "4px", + marginTop: "4px", + overflowX: "auto", + padding: "6px 8px", + whiteSpace: "pre" +} + +const copyBtnStyle: CSSProperties = { + background: "transparent", + border: "none", + color: "#7fdfff", + cursor: "pointer", + font: "inherit", + fontSize: "0.85em", + fontWeight: "bold", + padding: "2px 6px" +} + +const linkStyle: CSSProperties = { + color: "#56f39a", + cursor: "pointer", + fontFamily: "inherit", + fontSize: "inherit", + fontWeight: "bold", + textDecoration: "none" +} + +const panelOuterStyle: CSSProperties = { + background: "#0d1520", + border: "1px solid #2a4060", + borderRadius: "4px", + boxSizing: "border-box", + height: "100%", + overflowY: "auto", + padding: "12px 16px" +} + +const panelHeaderStyle: CSSProperties = { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + marginBottom: "10px" +} + +const buildVsCodeAccessValues = (cfState: CfTunnelState, info: VsCodeAccessInfo): VsCodeAccessValues => { + const directHost = location.hostname + const cfSshCommand = cfState.tag === "ready" + ? `ssh -o "ProxyCommand=cloudflared access ssh --hostname %h" ${info.sshUser}@${cfState.hostname}` + : null + const cfVscodeUri = cfState.tag === "ready" + ? `vscode://ms-vscode-remote.remote-ssh/open?hostName=${ + encodeURIComponent(`${info.sshUser}@${cfState.hostname}`) + }&folder=${encodeURIComponent(info.targetDir)}` + : null + return { + cfSshConfig: cfState.tag === "ready" ? hostSshConfig(cfState.hostname, info.sshUser) : null, + cfSshCommand, + cfVscodeUri, + directConfig: directSshConfig(directHost, info.sshPort, info.sshUser), + directCommand: String + .raw`ssh -p ${info.sshPort} -t ${info.sshUser}@${directHost} "cd ${info.targetDir} && exec \$SHELL"`, + directVscodeUri: `vscode://ms-vscode-remote.remote-ssh/open?hostName=${ + encodeURIComponent(`${directHost}-ssh`) + }&folder=${encodeURIComponent(info.targetDir)}` + } +} + +const CodeCopyRow = ({ text }: { readonly text: string }): JSX.Element => ( + <> + {text} + + +) + +const CfTunnelFailedSection = ({ onRetry }: { readonly onRetry: () => void }): JSX.Element => ( +
+
Tunnel failed to start.
+ +
+) + +const CfReadyDetails = ( + { cfSshCommand, cfSshConfig, cfVscodeUri, sshPassword }: CfReadyDetailsProps +): JSX.Element => ( + <> +
Add to ~/.ssh/config
+
+ requires cloudflared installed on your machine +
+ +
Connect via SSH
+ +
SSH password
+ + {cfVscodeUri !== null && ( + <> +
+ Open in VS Code +
+ + + )} + +) + +const DirectSshSection = ( + { cfReadyPassword, directCommand, directConfig, directVscodeUri }: DirectSshSectionProps +): JSX.Element => ( + <> +
+
Direct SSH (local network)
+
Add to ~/.ssh/config
+
no cloudflared needed — works on same LAN
+ +
Connect via SSH
+ + {cfReadyPassword !== null && ( + <> +
SSH password
+ + + )} +
Open in VS Code
+
requires config entry above in ~/.ssh/config
+ + +) + +export const VsCodeAccessPanel = ( + { cfState, info, onClose, onRefresh, onRetry }: VsCodeAccessPanelProps +): JSX.Element => { + const vals = buildVsCodeAccessValues(cfState, info) + return ( +
+
+
VS Code / SSH access
+
+ {cfState.tag === "ready" && ( + + )} + +
+
+ {cfState.tag === "loading" && ( +
Starting Cloudflare tunnel…
+ )} + {cfState.tag === "failed" && } + {cfState.tag === "ready" && vals.cfSshConfig !== null && vals.cfSshCommand !== null && ( + + )} + +
+ ) +} diff --git a/packages/app/src/web/panel-vscode-access.tsx b/packages/app/src/web/panel-vscode-access.tsx new file mode 100644 index 00000000..17b13b32 --- /dev/null +++ b/packages/app/src/web/panel-vscode-access.tsx @@ -0,0 +1,88 @@ +import { Effect } from "effect" +import { useEffect } from "react" + +import { startProjectSshTunnel } from "./api-share-links.js" +import type { TerminalPaneProps } from "./app-ready-terminal-types.js" + +export type VsCodeAccessInfo = { + readonly sshUser: string + readonly targetDir: string + readonly sshPort: number +} + +export type CfTunnelState = + | { readonly tag: "idle" } + | { readonly tag: "loading" } + | { readonly tag: "ready"; readonly hostname: string; readonly sshPassword: string } + | { readonly tag: "failed" } + +export const buildVsCodeAccessInfo = (project: TerminalPaneProps["project"]): VsCodeAccessInfo | null => { + if (project === null) return null + return { sshUser: project.sshUser, targetDir: project.targetDir, sshPort: project.sshPort } +} + +export const startTunnel = ( + projectKey: string, + setCfState: (s: CfTunnelState) => void +): void => { + setCfState({ tag: "loading" }) + void Effect.runPromise( + startProjectSshTunnel(projectKey).pipe( + Effect.match({ + onFailure: () => { + setCfState({ tag: "failed" }) + }, + onSuccess: ({ hostname, sshPassword }) => { + setCfState( + hostname === null + ? { tag: "failed" } + : { tag: "ready", hostname, sshPassword } + ) + } + }) + ) + ) +} + +export const useTunnelAutoStart = ( + isOpen: boolean, + cfState: CfTunnelState, + projectKey: string | undefined, + setCfState: (s: CfTunnelState) => void +): void => { + useEffect(() => { + if (!isOpen || projectKey === undefined || cfState.tag !== "idle") return + startTunnel(projectKey, setCfState) + }, [isOpen, projectKey, cfState.tag, setCfState]) +} + +export const useTunnelPolling = ( + isOpen: boolean, + cfState: CfTunnelState, + projectKey: string | undefined, + setCfState: (s: CfTunnelState) => void +): void => { + useEffect(() => { + if (!isOpen || cfState.tag !== "ready" || projectKey === undefined) return + let isCancelled = false + const id = setInterval(() => { + void Effect.runPromise( + startProjectSshTunnel(projectKey).pipe( + Effect.match({ + onFailure: () => { + if (!isCancelled) setCfState({ tag: "failed" }) + }, + onSuccess: ({ hostname, sshPassword }) => { + if (isCancelled) return + setCfState(hostname === null ? { tag: "failed" } : { tag: "ready", hostname, sshPassword }) + } + }) + ) + ) + }, 30_000) + return () => { + isCancelled = true + clearInterval(id) + } + }, [isOpen, cfState.tag, projectKey, setCfState]) +} diff --git a/packages/lib/src/usecases/ssh-access.ts b/packages/lib/src/usecases/ssh-access.ts index 23fa225a..a599ad20 100644 --- a/packages/lib/src/usecases/ssh-access.ts +++ b/packages/lib/src/usecases/ssh-access.ts @@ -183,6 +183,98 @@ Add to ~/.ssh/config: ${access.configSnippet}${firstHopNote}` } +// CHANGE: build SSH config for share links using external client host and mapped SSH port +// WHY: share link SSH config must route through the host's external IP + mapped port, not container IP +// QUOTE(ТЗ): "принимает ссылку на любой IP который стоит у пользователя в URL" +// REF: issue-428 +// FORMAT THEOREM: ∀ item, host: buildShareLinkSshAccess(item, host) → configSnippet uses sshPort (not 22) +// PURITY: CORE +// INVARIANT: port is always item.sshPort (host-mapped), never 22 (container-direct) +// COMPLEXITY: O(n)/O(n) where n = |containerName| +export type ShareLinkSshAccess = { + readonly alias: string + readonly configSnippet: string + readonly cfConfigSnippet: string | null + readonly workspacePath: string + readonly vscodeUri: string + readonly cfVscodeUri: string | null +} + +export type ShareLinkSshAccessInput = { + readonly containerName: string + readonly sshUser: string + readonly sshPort: number + readonly sshKeyPath: string | null + readonly targetDir: string + readonly clientHost: string + readonly sshCfHostname: string | null +} + +const buildDirectSshLines = ( + alias: string, + sshHostname: string, + sshUser: string, + sshPort: number, + sshKeyPath: string | null +): Array => { + const lines = [ + `Host ${alias}`, + ` HostName ${sshHostname}`, + ` User ${sshUser}`, + ` Port ${sshPort}`, + ` LogLevel ERROR`, + ` StrictHostKeyChecking no`, + ` UserKnownHostsFile /dev/null` + ] + if (sshKeyPath !== null) lines.push(` IdentityFile ${sshKeyPath}`, ` IdentitiesOnly yes`) + return lines +} + +const buildCfSshLines = ( + cfAlias: string, + cfHostname: string, + sshUser: string, + sshKeyPath: string | null +): Array => [ + `Host ${cfAlias}`, + ` HostName ${cfHostname}`, + ` User ${sshUser}`, + ` Port 22`, + ` ProxyCommand cloudflared access ssh --hostname %h`, + ` LogLevel ERROR`, + ` StrictHostKeyChecking no`, + ` UserKnownHostsFile /dev/null`, + ...(sshKeyPath === null ? [] : [` IdentityFile ${sshKeyPath}`, ` IdentitiesOnly yes`]) +] + +export const buildShareLinkSshAccess = ( + { clientHost, containerName, sshCfHostname, sshKeyPath, sshPort, sshUser, targetDir }: ShareLinkSshAccessInput +): ShareLinkSshAccess => { + const alias = sanitizeSshHostAlias(containerName) + // clientHost may carry a web port suffix (e.g. "192.168.0.206:4174") — strip it. + const sshHostname = clientHost.includes(":") ? clientHost.slice(0, clientHost.lastIndexOf(":")) : clientHost + const cfAlias = `${alias}-cf` + const cfLines = sshCfHostname === null ? null : buildCfSshLines(cfAlias, sshCfHostname, sshUser, sshKeyPath) + const encodedFolder = encodeURIComponent(targetDir) + // CHANGE: use hostName=user@host:port so VS Code Remote SSH connects directly + // SOURCE: https://code.visualstudio.com/docs/remote/ssh#_connect-to-a-remote-host + // "You can also enter a user@host or user@host:port connection string" + const directHostName = `${sshUser}@${sshHostname}:${sshPort}` + const cfHostName = sshCfHostname === null ? null : `${sshUser}@${sshCfHostname}` + return { + alias, + configSnippet: buildDirectSshLines(alias, sshHostname, sshUser, sshPort, sshKeyPath).join("\n"), + cfConfigSnippet: cfLines === null ? null : cfLines.join("\n"), + workspacePath: targetDir, + vscodeUri: `vscode://ms-vscode-remote.remote-ssh/open?hostName=${ + encodeURIComponent(directHostName) + }&folder=${encodedFolder}`, + cfVscodeUri: cfHostName === null + ? null + : `vscode://ms-vscode-remote.remote-ssh/open?hostName=${encodeURIComponent(cfHostName)}&folder=${encodedFolder}` + } +} + // CHANGE: resolve terminal/editor SSH access from the current runtime context // WHY: create/clone and list flows need consistent access info without duplicating fs/docker probing // QUOTE(ТЗ): "как подключиться к SSH к Cursor, VS code" diff --git a/packages/terminal/src/web/panel-terminal-header.tsx b/packages/terminal/src/web/panel-terminal-header.tsx index 97892076..ef62b636 100644 --- a/packages/terminal/src/web/panel-terminal-header.tsx +++ b/packages/terminal/src/web/panel-terminal-header.tsx @@ -26,6 +26,7 @@ type TerminalHeaderProps = | "onOpenSkiller" | "onOpenTaskManager" | "onOpenTerminal" + | "onOpenVsCode" | "session" > & { @@ -168,6 +169,11 @@ const TerminalHeaderActions = (props: TerminalHeaderProps): JSX.Element => ( inlineImagePreviewsEnabled={props.inlineImagePreviewsEnabled} onToggleInlineImagePreviews={props.onToggleInlineImagePreviews} /> + {props.onOpenVsCode !== undefined && ( + + VS Code + + )} Detach diff --git a/packages/terminal/src/web/panel-terminal-types.ts b/packages/terminal/src/web/panel-terminal-types.ts index 6915d120..0c3e8033 100644 --- a/packages/terminal/src/web/panel-terminal-types.ts +++ b/packages/terminal/src/web/panel-terminal-types.ts @@ -1,8 +1,31 @@ import type { JSX } from "react" -import type { TerminalExitInfo } from "./terminal-panel-runtime.js" +import type { MobileTerminalKey } from "./terminal-mobile-controls.js" +import type { TerminalExitInfo, TerminalStatus } from "./terminal-panel-runtime.js" import type { ActiveTerminalSession } from "./terminal.js" +export type RefState = { current: T } + +export type TerminalNotificationHandlers = { + readonly notifyAttachFailure: () => void + readonly notifyExit: (info: TerminalExitInfo) => void + readonly notifyMessage: (message: string) => void +} + +export type InlineImagePreviewState = { + readonly inlineImagePreviewsEnabled: boolean + readonly inlineImagePreviewsEnabledRef: RefState + readonly toggleInlineImagePreviews: () => void +} + +export type MobileTerminalControlState = { + readonly handleMobileKeyPress: (key: MobileTerminalKey) => void + readonly mobileControlsCollapsed: boolean + readonly mobileCtrlArmed: boolean + readonly toggleMobileControls: () => void + readonly toggleMobileCtrl: () => void +} + export type TerminalPanelProps = { readonly keyboardOpen: boolean readonly mobileMode: boolean @@ -18,4 +41,30 @@ export type TerminalPanelProps = { readonly onOpenTerminal?: (() => void) | undefined readonly session: ActiveTerminalSession readonly bodyContent?: JSX.Element | undefined + readonly onOpenVsCode?: (() => void) | undefined } + +export type TerminalPanelLayoutProps = + & Pick< + TerminalPanelProps, + | "bodyContent" + | "keyboardOpen" + | "mobileMode" + | "onApplyProject" + | "onOpenBrowser" + | "onOpenSkiller" + | "onOpenTaskManager" + | "onOpenTerminal" + | "onOpenVsCode" + | "session" + > + & InlineImagePreviewState + & MobileTerminalControlState + & { + readonly compactHeaderMode: boolean + readonly compactTypingMode: boolean + readonly handleDetach: () => void + readonly handleKill: () => void + readonly hostRef: RefState + readonly status: TerminalStatus + } diff --git a/packages/terminal/src/web/panel-terminal.tsx b/packages/terminal/src/web/panel-terminal.tsx index 9b43f22e..02c36335 100644 --- a/packages/terminal/src/web/panel-terminal.tsx +++ b/packages/terminal/src/web/panel-terminal.tsx @@ -16,7 +16,14 @@ import { terminalHostStyle, terminalPanelStyle } from "./panel-terminal-styles.js" -import type { TerminalPanelProps } from "./panel-terminal-types.js" +import { + type InlineImagePreviewState, + type MobileTerminalControlState, + type RefState, + type TerminalNotificationHandlers, + type TerminalPanelLayoutProps, + type TerminalPanelProps +} from "./panel-terminal-types.js" import type { MobileTerminalKey } from "./terminal-mobile-controls.js" import { isTerminalCompactHeaderMode, isTerminalTypingMode } from "./terminal-mobile-layout.js" import { @@ -28,52 +35,6 @@ import { } from "./terminal-panel-runtime.js" import { type ActiveTerminalSession, isPendingActiveTerminalSession } from "./terminal.js" -type RefState = { current: T } - -type TerminalNotificationHandlers = { - readonly notifyAttachFailure: () => void - readonly notifyExit: (info: TerminalExitInfo) => void - readonly notifyMessage: (message: string) => void -} - -type InlineImagePreviewState = { - readonly inlineImagePreviewsEnabled: boolean - readonly inlineImagePreviewsEnabledRef: RefState - readonly toggleInlineImagePreviews: () => void -} - -type MobileTerminalControlState = { - readonly handleMobileKeyPress: (key: MobileTerminalKey) => void - readonly mobileControlsCollapsed: boolean - readonly mobileCtrlArmed: boolean - readonly toggleMobileControls: () => void - readonly toggleMobileCtrl: () => void -} - -type TerminalPanelLayoutProps = - & Pick< - TerminalPanelProps, - | "bodyContent" - | "keyboardOpen" - | "mobileMode" - | "onApplyProject" - | "onOpenBrowser" - | "onOpenSkiller" - | "onOpenTaskManager" - | "onOpenTerminal" - | "session" - > - & InlineImagePreviewState - & MobileTerminalControlState - & { - readonly compactHeaderMode: boolean - readonly compactTypingMode: boolean - readonly handleDetach: () => void - readonly handleKill: () => void - readonly hostRef: RefState - readonly status: TerminalStatus - } - const resolveInitialTerminalStatus = (session: ActiveTerminalSession): TerminalStatus => isPendingActiveTerminalSession(session) && session.pendingConnection.phase === "error" ? "error" : "connecting" @@ -265,6 +226,7 @@ const TerminalPanelLayout = (props: TerminalPanelLayoutProps): JSX.Element => ( onOpenSkiller={props.onOpenSkiller} onOpenTaskManager={props.onOpenTaskManager} onOpenTerminal={props.onOpenTerminal} + onOpenVsCode={props.onOpenVsCode} onToggleInlineImagePreviews={props.toggleInlineImagePreviews} session={props.session} status={props.status}