From a843098d609e60b683186979c152c7f3fe862440 Mon Sep 17 00:00:00 2001 From: tomaszbk Date: Thu, 30 Oct 2025 17:38:35 +0000 Subject: [PATCH 1/2] refactor v installation. user can choose stable, latest or custom --- package-lock.json | 221 +++++++++++++++++++---- package.json | 42 +++-- src/commands.ts | 9 +- src/exec.ts | 6 +- src/extension.ts | 24 +-- src/langserver.ts | 26 ++- src/utils.ts | 93 +--------- src/vUtils.ts | 439 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 690 insertions(+), 170 deletions(-) create mode 100644 src/vUtils.ts diff --git a/package-lock.json b/package-lock.json index a2621d3..6ba8ec7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.2.0", "license": "MIT", "dependencies": { + "extract-zip": "^2.0.1", + "tar": "^7.5.1", "vscode-languageclient": "10.0.0-next.15" }, "devDependencies": { @@ -930,6 +932,18 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1351,7 +1365,7 @@ "version": "24.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.14.0" @@ -1385,6 +1399,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", @@ -2227,7 +2251,6 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "engines": { "node": "*" } @@ -2490,7 +2513,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2773,8 +2795,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -3106,6 +3126,26 @@ "node": ">=6" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3175,7 +3215,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "dependencies": { "pend": "~1.2.0" } @@ -3355,6 +3394,21 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4959,12 +5013,23 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4976,7 +5041,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -5092,7 +5156,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -5363,8 +5426,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { "version": "1.1.1", @@ -5453,8 +5515,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6199,6 +6259,22 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -6229,6 +6305,24 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -6396,7 +6490,7 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -6770,8 +6864,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/wsl-utils": { "version": "0.1.0", @@ -6808,7 +6901,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -7331,6 +7423,14 @@ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "requires": { + "minipass": "^7.0.4" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7658,7 +7758,7 @@ "version": "24.7.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", - "dev": true, + "devOptional": true, "requires": { "undici-types": "~7.14.0" } @@ -7687,6 +7787,15 @@ "integrity": "sha512-Lotk3CTFlGZN8ray4VxJE7axIyLZZETQJVWi/lYoUVQuqfRxlQhVOfoejsD2V3dVXPSbS15ov5ZyowMAzgUqcw==", "dev": true }, + "@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "8.46.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", @@ -8215,8 +8324,7 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" }, "buffer-equal-constant-time": { "version": "1.0.1", @@ -8399,7 +8507,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "requires": { "ms": "^2.1.3" } @@ -8581,8 +8688,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "optional": true, "requires": { "once": "^1.4.0" } @@ -8807,6 +8912,17 @@ "dev": true, "optional": true }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8857,7 +8973,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, "requires": { "pend": "~1.2.0" } @@ -8987,6 +9102,14 @@ "es-object-atoms": "^1.0.0" } }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -10001,8 +10124,15 @@ "minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "requires": { + "minipass": "^7.1.2" + } }, "mkdirp-classic": { "version": "0.5.3", @@ -10014,8 +10144,7 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mute-stream": { "version": "0.0.8", @@ -10110,7 +10239,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -10295,8 +10423,7 @@ "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "picocolors": { "version": "1.1.1", @@ -10353,8 +10480,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -10839,6 +10964,30 @@ } } }, + "tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "dependencies": { + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" + }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" + } + } + }, "tar-fs": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", @@ -10978,7 +11127,7 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "dev": true + "devOptional": true }, "unicorn-magic": { "version": "0.3.0", @@ -11248,8 +11397,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "wsl-utils": { "version": "0.1.0", @@ -11276,7 +11424,6 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/package.json b/package.json index 03320b4..e3d04db 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,32 @@ "type": "object", "title": "V", "properties": { + "v.releaseChannel": { + "type": "string", + "default": "stable", + "enum": [ + "stable", + "nightly", + "custom" + ], + "description": "Specify the V version to use", + "enumDescriptions": [ + "Get the latest stable release of V. (Default)", + "Gets the latest development version of V.", + "Uses a custom version of V." + ] + }, + "v.buildPath": { + "type": "string", + "default": "", + "description": "Custom path to the V repo. Used only if 'v.releaseChannel' is set to 'custom'." + }, + "v.forceCleanInstall": { + "scope": "resource", + "type": "boolean", + "default": false, + "description": "Removes any existing V installation and forces a clean install on startup." + }, "v.vls.command": { "type": "string", "default": "v", @@ -114,18 +140,6 @@ "type": "boolean", "description": "Enables / disables the language server's debug mode.\nSetting it to true will create a log file to your workspace folder for bug reports." }, - "v.vls.customVrootPath": { - "scope": "resource", - "type": "string", - "default": "", - "description": "Custom path to the V installation directory (VROOT).\nNOTE: Setting this won't change the V compiler executable to be used." - }, - "v.vls.customPath": { - "scope": "resource", - "type": "string", - "default": "", - "description": "Custom path to the VLS (V Language Server) executable." - }, "v.vls.enable": { "scope": "resource", "type": "boolean", @@ -266,15 +280,17 @@ ], "main": "./out/extension.js", "dependencies": { + "extract-zip": "^2.0.1", + "tar": "^7.5.1", "vscode-languageclient": "10.0.0-next.15" }, "devDependencies": { - "esbuild": "^0.25.10", "@types/node": "24", "@types/vscode": "1.105.0", "@typescript-eslint/eslint-plugin": "^8.46", "@typescript-eslint/parser": "^8.46", "@vscode/vsce": "^3.6.2", + "esbuild": "^0.25.10", "eslint": "^9.37.0", "globals": "^15.9.0", "markdownlint-cli": "^0.45.0", diff --git a/src/commands.ts b/src/commands.ts index 1c92dc1..307d3c2 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -90,10 +90,13 @@ export function registerCommands(context: ExtensionContext): Promise { return Promise.resolve() } -export function registerVlsCommands(context: ExtensionContext, client?: LanguageClient): void { +export function registerVlsCommands( + context: ExtensionContext, + getClient?: () => LanguageClient | undefined, +): void { context.subscriptions.push( - commands.registerCommand("v.vls.update", () => updateVls(client)), - commands.registerCommand("v.vls.restart", () => restartVls(client)), + commands.registerCommand("v.vls.update", () => updateVls(getClient?.())), + commands.registerCommand("v.vls.restart", () => restartVls(getClient?.())), commands.registerCommand("v.vls.openOutput", () => { vlsOutputChannel.show() }), diff --git a/src/exec.ts b/src/exec.ts index da5a861..6a6dc14 100644 --- a/src/exec.ts +++ b/src/exec.ts @@ -26,5 +26,9 @@ export async function execVInTerminalOnBG(args: string[], cwd = "/"): Promise { const clientOptions: LanguageClientOptions = { documentSelector: [{ scheme: "file", language: "v" }], outputChannel: vlsOutputChannel, + traceOutputChannel: vlsOutputChannel, synchronize: { fileEvents: vscode.workspace.createFileSystemWatcher("**/*.v"), }, @@ -33,19 +34,7 @@ export async function activate(context: ExtensionContext): Promise { // Register output channels so users can open them even without VLS. context.subscriptions.push(outputChannel, vlsOutputChannel) - // Check for V only if it's not installed - if (!(await isVInstalled())) { - const selection = await vscode.window.showInformationMessage( - "The V programming language is not detected on this system. Would you like to install it?", - { modal: true }, // Modal makes the user have to choose before continuing - "Yes", - "No", - ) - - if (selection === "Yes") { - await installV() - } - } + await handleVinstallation() // Register commands regardless of whether VLS is enabled await registerCommands(context) @@ -65,10 +54,10 @@ export async function activate(context: ExtensionContext): Promise { log("VLS is disabled in settings.") } - registerVlsCommands(context, client) + registerVlsCommands(context, () => client) // React to configuration changes: enable/disable or request restart. - workspace.onDidChangeConfiguration(async (e: ConfigurationChangeEvent) => { + const configListener = workspace.onDidChangeConfiguration(async (e: ConfigurationChangeEvent) => { const vlsEnabled = isVlsEnabled() if (e.affectsConfiguration("v.vls.enable")) { @@ -109,8 +98,11 @@ export async function activate(context: ExtensionContext): Promise { } } }) + } else if (e.affectsConfiguration("v.releaseChannel")) { + await handleVinstallation() } }) + context.subscriptions.push(configListener) } export function deactivate(): Promise | undefined { diff --git a/src/langserver.ts b/src/langserver.ts index 614687f..5612342 100644 --- a/src/langserver.ts +++ b/src/langserver.ts @@ -8,7 +8,7 @@ import { exec as _exec } from "child_process" import { promises as fs } from "fs" import { promisify } from "util" import { execVInTerminalOnBG } from "./exec" -import { isVInstalled } from "./utils" +import { isVInstalled } from "./vUtils" const exec = promisify(_exec) @@ -16,11 +16,11 @@ export const BINARY_NAME = process.platform === "win32" ? "vls.exe" : "vls" export const USER_BIN_PATH = path.join(os.homedir(), ".local", "bin") -export const VLS_PATH = path.join(USER_BIN_PATH, BINARY_NAME) // ~/.local/bin/vls if not tmp enabled +export const VLS_PATH = path.join(USER_BIN_PATH, BINARY_NAME) // ~/.local/bin/vls export async function getVls(): Promise { if (vlsConfig().get("forceCleanInstall")) { - await fs.rm(VLS_PATH, { recursive: true, force: true }) + await fs.rm(VLS_PATH, { force: true }) log("forceCleanInstall is enabled, removed existing VLS.") } else if (await isVlsInstalled()) { // dont check if installed if forceCleanInstall is true @@ -65,22 +65,32 @@ export function installVls(): Promise { } export async function buildVls(): Promise { - if (!(await isVInstalled())) { + const { installed: isInstalled, version } = await isVInstalled() + if (!isInstalled) { throw new Error("V must be installed to build VLS.") } - let buildPath + if (version) { + log(`Detected V version ${version}.`) + } + let buildPath: string try { log("Building VLS...") window.showInformationMessage("Building VLS...") - if (vlsConfig().get("buildPath") !== "") { - buildPath = vlsConfig().get("buildPath") + const configuredPath = vlsConfig().get("buildPath")?.trim() ?? "" + if (configuredPath !== "") { + buildPath = path.resolve(configuredPath) + try { + await fs.access(buildPath) + } catch { + throw new Error(`Configured VLS build path not found: ${buildPath}`) + } } else { // Use temporary directory for cross-platform compatibility buildPath = path.join(os.tmpdir(), "vls") // Remove any existing directory at buildPath await fs.rm(buildPath, { recursive: true, force: true }) // Clone the repo into buildPath - await exec(`git clone --depth 1 https://github.com/vlang/vls.git ${buildPath}`) + await exec(`git clone --depth 1 https://github.com/vlang/vls.git "${buildPath}"`) } await execVInTerminalOnBG(["."], buildPath) // build diff --git a/src/utils.ts b/src/utils.ts index a207690..79a7aef 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,9 @@ -import { exec as _exec } from "child_process" -import { getVExecCommand } from "exec" -import * as fs from "fs" -import { USER_BIN_PATH } from "langserver" -import { log } from "logger" -import * as os from "os" -import * as path from "path" -import { promisify } from "util" -import { ProgressLocation, Uri, window, workspace, WorkspaceFolder } from "vscode" +import { Uri, window, workspace, WorkspaceFolder } from "vscode" export const config = () => workspace.getConfiguration("v") export const vlsConfig = () => workspace.getConfiguration("v.vls") -const exec = promisify(_exec) /** Get current working directory. * @param uri The URI of document @@ -34,85 +25,3 @@ export function getWorkspaceFolder(uri?: Uri): WorkspaceFolder { return workspace.workspaceFolders[0] } } - -/** - * Checks if the 'v' command is available in the system's PATH. - * @returns A promise that resolves to true if 'v' is installed, otherwise false. - */ -export async function isVInstalled(): Promise { - const vexec = getVExecCommand() - try { - // A simple command to check if V is installed and in the PATH. - const version = await exec(`${vexec} --version`) - log(`V is already installed, version: ${version.stdout.trim()}`) - return true - } catch (error) { - log(`V is not detected in PATH: ${error}`) - return false - } -} - -/** - * Clone and build the `v` compiler - * - * Returns: absolute path to the `v` binary (string) - * Error: rejects if any git/make step fails - */ -export async function installV(): Promise { - const installDir = USER_BIN_PATH - const vRepoPath = path.join(installDir, "v") - const repoUrl = "https://github.com/vlang/v" - - await window.withProgress( - { - location: ProgressLocation.Notification, - title: "Installing V Language", - cancellable: false, - }, - async (progress) => { - try { - // 0. Clean up any previous failed attempts - progress.report({ message: "Preparing workspace..." }) - if (fs.existsSync(installDir)) { - fs.rmSync(installDir, { recursive: true, force: true }) - } - fs.mkdirSync(installDir) - - // 1. Clone the repository - progress.report({ message: "Cloning V repository..." }) - await exec(`git clone --depth=1 ${repoUrl}`, { cwd: installDir }) - - // 2. Build V using make - progress.report({ message: "Building V from source (this may take a moment)..." }) - await exec("make", { cwd: vRepoPath }) - - // 3. Create a symlink - // This command often requires sudo/admin privileges. - // We run it and inform the user to run it manually if it fails. - progress.report({ message: "Attempting to create symlink..." }) - - try { - // On Windows, the build script handles the path. On Linux/macOS, symlink is used. - const symlinkCommand = - os.platform() === "win32" ? "v.exe symlink" : "./v symlink" - await exec(symlinkCommand, { cwd: vRepoPath }) - - window.showInformationMessage( - "V language installed and linked successfully! Please restart VS Code to use the `v` command.", - ) - } catch (symlinkError) { - console.error(symlinkError) - window.showWarningMessage( - `V was built successfully, but the automatic symlink failed (likely due to permissions). Please run '${path.join(vRepoPath, "v")} symlink' manually with administrator/sudo rights.`, - "OK", - ) - } - } catch (error) { - console.error(error) - window.showErrorMessage( - `Failed to install V. Please check the logs for details. Error: ${error}`, - ) - } - }, - ) -} diff --git a/src/vUtils.ts b/src/vUtils.ts new file mode 100644 index 0000000..fdb1a60 --- /dev/null +++ b/src/vUtils.ts @@ -0,0 +1,439 @@ +import { exec as childExec, ExecOptions } from "child_process" +import { getVExecCommand } from "exec" +import extract from "extract-zip" +import * as fs from "fs" +import { log } from "logger" +import { pipeline } from "node:stream/promises" +import * as os from "os" +import * as path from "path" +import * as tar from "tar" +import { config } from "utils" +import * as vscode from "vscode" +import { ProgressLocation, window } from "vscode" +import { USER_BIN_PATH } from "./langserver" + +interface ExecResult { + stdout: string + stderr: string +} + +async function execAsync(command: string, options?: ExecOptions): Promise { + return new Promise((resolve, reject) => { + childExec(command, options ?? {}, (error, stdout, stderr) => { + if (error) { + reject(error) + return + } + const stdoutText = typeof stdout === "string" ? stdout : stdout.toString() + const stderrText = typeof stderr === "string" ? stderr : stderr.toString() + resolve({ stdout: stdoutText, stderr: stderrText }) + }) + }) +} + +// simple util to normalize unknown errors +function toError(e: unknown): Error { + return e instanceof Error ? e : new Error(String(e)) +} + +interface GitHubAsset { + name: string + browser_download_url: string +} + +interface GitHubRelease { + draft: boolean + prerelease: boolean + tag_name: string + assets: GitHubAsset[] +} + +function isGitHubAsset(value: unknown): value is GitHubAsset { + if (typeof value !== "object" || value === null) return false + const record = value as Record + return typeof record.name === "string" && typeof record.browser_download_url === "string" +} + +function isGitHubRelease(value: unknown): value is GitHubRelease { + if (typeof value !== "object" || value === null) return false + const record = value as Record + return ( + typeof record.draft === "boolean" && + typeof record.prerelease === "boolean" && + typeof record.tag_name === "string" && + Array.isArray(record.assets) && + record.assets.every(isGitHubAsset) + ) +} + +function selectAssetForPlatform(assets: GitHubAsset[]): GitHubAsset | undefined { + const platform = process.platform + const arch = process.arch + const matchAsset = (needles: string[], bans: string[] = []): GitHubAsset | undefined => { + const wanted = needles.map((value) => value.toLowerCase()) + const blocked = bans.map((value) => value.toLowerCase()) + return assets.find((asset) => { + const name = asset.name.toLowerCase() + return ( + wanted.every((fragment) => name.includes(fragment)) && + !blocked.some((fragment) => name.includes(fragment)) + ) + }) + } + + if (platform === "win32") { + return matchAsset(["v_windows"]) ?? matchAsset(["windows"]) ?? matchAsset(["win"], ["arm"]) + } + + if (platform === "linux") { + if (arch === "arm64") { + return ( + matchAsset(["v_linux_arm64"]) ?? + matchAsset(["linux", "arm64"]) ?? + matchAsset(["linux", "aarch64"]) + ) + } + return ( + matchAsset(["v_linux"], ["arm64", "aarch64"]) ?? + matchAsset(["linux"], ["arm64", "aarch64"]) + ) + } + + if (platform === "darwin") { + if (arch === "arm64") { + return ( + matchAsset(["v_macos_arm64"]) ?? + matchAsset(["macos", "arm64"]) ?? + matchAsset(["darwin", "arm64"]) + ) + } + return ( + matchAsset(["v_macos_x86_64"]) ?? + matchAsset(["macos", "x86_64"]) ?? + matchAsset(["macos", "x64"]) ?? + matchAsset(["darwin", "x64"]) ?? + matchAsset(["macos"]) + ) + } + + return undefined +} +export interface VInstalledStatus { + installed: boolean + version?: string +} + +/** + * Checks if the 'v' command is available in the system's PATH. + * @returns A promise that resolves to the installation status and detected version (if any). + */ +export async function isVInstalled(): Promise { + const vexec = getVExecCommand() + try { + // A simple command to check if V is installed and in the PATH. + const version = await execAsync(`${vexec} --version`) + const versionText = version.stdout.trim() + log(`V is already installed${versionText ? `, version: ${versionText}` : ""}`) + return { installed: true, version: versionText || undefined } + } catch (error: unknown) { + const err = toError(error) + log(`V is not detected in PATH: ${err.message}`) + return { installed: false } + } +} +/** + * Clone and build the `v` compiler + * + * Returns: absolute path to the `v` binary (string) + * Error: rejects if any git/make step fails + */ +export async function installV(): Promise { + const releaseChannel = config().get("releaseChannel") + switch (releaseChannel) { + case "stable": { + const url = await getLatestStableAssetUrl() + await installVFromAsset(url) + return + } + case "nightly": { + const url = await getLatestAssetUrl() + await installVFromAsset(url) + return + } + case "custom": + await buildVfromPath() + return + default: + await buildVfromPath() + } +} + +export async function buildVfromPath(): Promise { + const vRepoPath = config().get("buildPath") + + if (!vRepoPath || !fs.existsSync(vRepoPath)) { + void window.showErrorMessage( + `Custom V path "${vRepoPath}" does not exist. Please check your settings.`, + ) + throw new Error("Custom V path does not exist.") + } + + const binaryPath = path.join(vRepoPath, os.platform() === "win32" ? "v.exe" : "v") + + await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing V Language", + cancellable: false, + }, + async (progress) => { + try { + progress.report({ + message: "Building V from source (this may take a moment)...", + }) + await execAsync("make", { cwd: vRepoPath }) + + progress.report({ message: "Attempting to create symlink..." }) + + try { + const symlinkCommand = + os.platform() === "win32" ? "v.exe symlink" : "./v symlink" + await execAsync(symlinkCommand, { cwd: vRepoPath }) + + window.showInformationMessage( + "V language installed and linked successfully! Please restart VS Code to use the `v` command.", + ) + } catch (symlinkError: unknown) { + console.error(symlinkError) + window.showWarningMessage( + `V was built successfully, but the automatic symlink failed (likely due to permissions). Please run '${path.join(vRepoPath, "v")} symlink' manually with administrator/sudo rights.`, + "OK", + ) + } + } catch (error: unknown) { + console.error(error) + window.showErrorMessage( + `Failed to install V. Please check the logs for details. Error: ${toError(error).message}`, + ) + throw toError(error) + } + }, + ) + + return binaryPath +} + +/** + * Try to locate the V binary on the system. + */ +async function findVBinary(): Promise { + try { + const cmd = process.platform === "win32" ? "where v" : "which v" + const { stdout } = await execAsync(cmd) + return stdout.split("\n")[0].trim() || null + } catch { + return null + } +} + +export async function removeV(): Promise { + const vPath = await findVBinary() + + if (!vPath || !fs.existsSync(vPath)) { + log("No V binary found on this system.") + return + } + + try { + await fs.promises.unlink(vPath) + window.showInformationMessage(`Removed V binary at ${vPath}.`) + } catch (err: unknown) { + const error = toError(err) + window.showErrorMessage(`Failed to remove V binary: ${error.message}`) + throw error + } +} + +export async function getLatestStableAssetUrl(): Promise { + const res = await fetch("https://api.github.com/repos/vlang/v/releases", { + headers: { "User-Agent": "vscode-vlang-extension" }, + redirect: "follow", + }) + + if (!res.ok) { + throw new Error(`Failed to fetch releases: ${res.status} ${res.statusText}`) + } + + const parsed = (await res.json()) as unknown + + if (!Array.isArray(parsed)) { + throw new Error("Unexpected release payload format") + } + + const releases = parsed.filter(isGitHubRelease) + + // Find first non-weekly, non-draft, non-prerelease release + const stable = releases.find( + (release) => + !release.draft && !release.prerelease && !release.tag_name.startsWith("weekly."), + ) + + if (!stable) { + throw new Error("No stable release found") + } + + const asset = selectAssetForPlatform(stable.assets) + if (!asset) { + throw new Error("No matching binary for this OS") + } + + return asset.browser_download_url +} + +export async function getLatestAssetUrl(): Promise { + const res = await fetch("https://api.github.com/repos/vlang/v/releases/latest", { + headers: { "User-Agent": "vscode-vlang-extension" }, + redirect: "follow", + }) + + if (!res.ok) { + throw new Error(`Failed to fetch latest release: ${res.status} ${res.statusText}`) + } + + const parsed = (await res.json()) as unknown + + if (!isGitHubRelease(parsed)) { + throw new Error("Unexpected release payload format") + } + + const asset = selectAssetForPlatform(parsed.assets) + if (!asset) { + throw new Error("No matching binary for this OS") + } + + return asset.browser_download_url +} + +export async function installVFromAsset(assetUrl: string): Promise { + return await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Installing V Language", + cancellable: false, + }, + async (progress) => { + progress.report({ + message: "Downloading V...", + }) + const storageDir = USER_BIN_PATH + const vDir = path.join(storageDir, "v") + const vExecPath = path.join(storageDir, process.platform === "win32" ? "v.exe" : "v") + + // Remove remnants of a previous download so we do not mix files from different releases. + try { + if (fs.existsSync(vDir)) { + progress.report({ message: "Cleaning previous V installation..." }) + await fs.promises.rm(vDir, { recursive: true, force: true }) + } + if (fs.existsSync(vExecPath)) { + await fs.promises.rm(vExecPath, { force: true }) + } + } catch (cleanupError) { + console.warn("Failed to fully clean previous V installation:", cleanupError) + } + + fs.mkdirSync(storageDir, { recursive: true }) + const tmpFile = path.join(storageDir, path.basename(assetUrl)) + if (fs.existsSync(tmpFile)) { + await fs.promises.rm(tmpFile, { force: true }) + } + + await downloadFile(assetUrl, tmpFile) + + progress.report({ + message: "Extracting V...", + }) + const lowerUrl = assetUrl.toLowerCase() + if (lowerUrl.endsWith(".zip")) { + await extract(tmpFile, { dir: storageDir }) + } else if (lowerUrl.endsWith(".tar.gz")) { + await tar.x({ file: tmpFile, cwd: storageDir }) + } else { + throw new Error(`Unknown archive format: ${assetUrl}`) + } + progress.report({ + message: "Finishing", + }) + fs.unlinkSync(tmpFile) + // On Windows, the build script handles the path. On Linux/macOS, symlink is used. + const symlinkCommand = os.platform() === "win32" ? "v.exe symlink" : "./v symlink" + try { + await execAsync(symlinkCommand, { cwd: vDir }) + + window.showInformationMessage( + "V language installed and linked successfully! Please restart VS Code to use the `v` command.", + ) + } catch (symlinkError) { + console.error(symlinkError) + window.showWarningMessage( + `V was built successfully, but the automatic symlink failed (likely due to permissions). Please run 'cd ${vDir} && ${symlinkCommand}' manually with administrator/sudo rights.`, + "OK", + ) + } + + if (fs.existsSync(vExecPath)) { + fs.chmodSync(vExecPath, 0o755) + } + + try { + const { installed, version } = await isVInstalled() + if (installed) { + const suffix = version ? ` (${version})` : "" + window.showInformationMessage(`V installed successfully${suffix}.`) + } + } catch (err: unknown) { + const error = toError(err) + window.showErrorMessage(`Failed to run V after install: ${error.message}`) + throw error + } + + return vExecPath + }, + ) +} + +async function downloadFile(url: string, dest: string): Promise { + const res = await fetch(url, { + redirect: "follow", + headers: { "User-Agent": "vscode-vlang-extension" }, + }) + + if (!res.ok || !res.body) { + throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`) + } + + await pipeline(res.body, fs.createWriteStream(dest)) +} + +export async function handleVinstallation(): Promise { + if (config().get("forceCleanInstall")) { + log("forceCleanInstall is enabled, removing V if exists.") + vscode.window.showInformationMessage("forceCleanInstall is enabled, removing V if exists.") + await removeV() + } + + // Check for V only if it's not installed + const { installed } = await isVInstalled() + if (!installed) { + const selection = await vscode.window.showInformationMessage( + "The V programming language is not detected on this system. Would you like to install it?", + { modal: true }, // Modal makes the user have to choose before continuing + "Yes", + "No", + ) + + if (selection === "Yes") { + await installV() + } + } +} From 46c41343efb7c934c3e10f7a4c2bbcc9710a3c0b Mon Sep 17 00:00:00 2001 From: tomaszbk Date: Mon, 3 Nov 2025 12:13:00 +0000 Subject: [PATCH 2/2] add check for updates --- package.json | 1 + src/vUtils.ts | 133 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index e3d04db..cce8e1e 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "v.vls.buildPath": { "scope": "resource", "type": "string", + "default": "", "description": "Path to vls source to build." }, "v.vls.enableFeatures": { diff --git a/src/vUtils.ts b/src/vUtils.ts index fdb1a60..23af596 100644 --- a/src/vUtils.ts +++ b/src/vUtils.ts @@ -3,10 +3,13 @@ import { getVExecCommand } from "exec" import extract from "extract-zip" import * as fs from "fs" import { log } from "logger" +import { Readable } from "node:stream" import { pipeline } from "node:stream/promises" +import { ReadableStream as NodeReadableStream } from "node:stream/web" import * as os from "os" import * as path from "path" import * as tar from "tar" +import { inspect } from "util" import { config } from "utils" import * as vscode from "vscode" import { ProgressLocation, window } from "vscode" @@ -33,7 +36,13 @@ async function execAsync(command: string, options?: ExecOptions): Promise @@ -151,13 +165,13 @@ export async function installV(): Promise { const releaseChannel = config().get("releaseChannel") switch (releaseChannel) { case "stable": { - const url = await getLatestStableAssetUrl() - await installVFromAsset(url) + const { url, version } = await getLatestStableAssetUrl() + await installVFromAsset(url, version) return } case "nightly": { - const url = await getLatestAssetUrl() - await installVFromAsset(url) + const { url, version } = await getLatestAssetUrl() + await installVFromAsset(url, version) return } case "custom": @@ -254,7 +268,7 @@ export async function removeV(): Promise { } } -export async function getLatestStableAssetUrl(): Promise { +export async function getLatestStableAssetUrl(): Promise { const res = await fetch("https://api.github.com/repos/vlang/v/releases", { headers: { "User-Agent": "vscode-vlang-extension" }, redirect: "follow", @@ -287,10 +301,13 @@ export async function getLatestStableAssetUrl(): Promise { throw new Error("No matching binary for this OS") } - return asset.browser_download_url + return { + url: asset.browser_download_url, + version: stable.tag_name, + } } -export async function getLatestAssetUrl(): Promise { +export async function getLatestAssetUrl(): Promise { const res = await fetch("https://api.github.com/repos/vlang/v/releases/latest", { headers: { "User-Agent": "vscode-vlang-extension" }, redirect: "follow", @@ -311,14 +328,21 @@ export async function getLatestAssetUrl(): Promise { throw new Error("No matching binary for this OS") } - return asset.browser_download_url + return { + url: asset.browser_download_url, + version: parsed.tag_name, + } } -export async function installVFromAsset(assetUrl: string): Promise { +export async function installVFromAsset( + assetUrl: string, + tagName: string, + isUpdate: boolean = false, +): Promise { return await window.withProgress( { location: ProgressLocation.Notification, - title: "Installing V Language", + title: isUpdate ? "Updating V Language" : "Installing V Language", cancellable: false, }, async (progress) => { @@ -388,8 +412,10 @@ export async function installVFromAsset(assetUrl: string): Promise { try { const { installed, version } = await isVInstalled() if (installed) { - const suffix = version ? ` (${version})` : "" - window.showInformationMessage(`V installed successfully${suffix}.`) + const suffix = version ? ` (${version})` : ` (${tagName})` + window.showInformationMessage( + `V ${isUpdate ? "updated" : "installed"} successfully${suffix}.`, + ) } } catch (err: unknown) { const error = toError(err) @@ -412,7 +438,37 @@ async function downloadFile(url: string, dest: string): Promise { throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`) } - await pipeline(res.body, fs.createWriteStream(dest)) + const nodeStream = Readable.fromWeb(res.body as unknown as NodeReadableStream) + await pipeline(nodeStream, fs.createWriteStream(dest)) +} + +async function updateNightlyBuild(): Promise { + const vexec = getVExecCommand() + await window.withProgress( + { + location: ProgressLocation.Notification, + title: "Updating V Nightly", + cancellable: false, + }, + async () => { + try { + const { stdout, stderr } = await execAsync(`${vexec} up`) + const combined = `${stdout}${stderr}`.trim() + const lines = + combined === "" + ? [] + : combined.split(/\r?\n/).filter((line) => line.trim() !== "") + const tail = lines.slice(-2).join("\n") + window.showInformationMessage( + tail !== "" ? tail : "V nightly build updated successfully.", + ) + } catch (error: unknown) { + const err = toError(error) + log(`Failed to update nightly build: ${err.message}`) + window.showWarningMessage(`Failed to update V nightly build: ${err.message}`) + } + }, + ) } export async function handleVinstallation(): Promise { @@ -423,6 +479,7 @@ export async function handleVinstallation(): Promise { } // Check for V only if it's not installed + const releaseChannel = config().get("releaseChannel") const { installed } = await isVInstalled() if (!installed) { const selection = await vscode.window.showInformationMessage( @@ -435,5 +492,53 @@ export async function handleVinstallation(): Promise { if (selection === "Yes") { await installV() } + } else { + log("V is already installed, checking for updates.") + await checkUpdates(releaseChannel) + } +} + +function extractVersionIdentifier(value: string | undefined): string | undefined { + if (!value) return undefined + const normalized = value.trim().toLowerCase() + if (normalized === "") return undefined + const weeklyMatch = normalized.match(/weekly\.\d{4}\.\d+/) + if (weeklyMatch) return weeklyMatch[0] + const semverMatch = normalized.match(/(\d+\.\d+\.\d+)/) + if (semverMatch) return semverMatch[1] + return normalized.replace(/^v[\s-]?/, "") +} + +function isUpToDate(currentVersion: string | undefined, targetVersion: string): boolean { + const current = extractVersionIdentifier(currentVersion) + const target = extractVersionIdentifier(targetVersion) + if (!target) return false + if (!current) return false + return current === target +} + +async function checkUpdates(releaseChannel: string | undefined): Promise { + const channel = releaseChannel ?? "stable" + if (channel === "custom") { + log("Skipping auto-update for custom release channel.") + return + } + + try { + if (channel === "nightly") { + await updateNightlyBuild() + return + } + + const latest = await getLatestStableAssetUrl() + const { version: installedVersion } = await isVInstalled() + if (isUpToDate(installedVersion, latest.version)) { + log(`V is up to date (${latest.version}).`) + return + } + log(`Updating V to ${latest.version}.`) + await installVFromAsset(latest.url, latest.version, true) + } catch (error: unknown) { + log(`Failed to check or update V: ${toError(error).message}`) } }