├── CNAME ├── docs ├── CNAME ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── package.json │ │ │ ├── vue.js.map │ │ │ ├── vitepress___@vueuse_core.js.map │ │ │ └── _metadata.json │ └── config.mts ├── public │ ├── logo.png │ ├── demo-full.gif │ └── performance.png ├── package.json ├── zh-hans │ ├── plugins │ │ ├── library │ │ │ ├── archiver.md │ │ │ ├── json.md │ │ │ ├── html.md │ │ │ ├── strings.md │ │ │ └── http.md │ │ ├── available.md │ │ └── create │ │ │ └── howto_registry.md │ ├── guides │ │ ├── faq.md │ │ ├── intro.md │ │ └── configuration.md │ ├── misc │ │ └── vs-asdf.md │ ├── usage │ │ ├── all-commands.md │ │ ├── shims-path.md │ │ ├── plugins-commands.md │ │ └── core-commands.md │ └── index.md ├── plugins │ ├── library │ │ ├── json.md │ │ ├── archiver.md │ │ ├── html.md │ │ ├── strings.md │ │ └── http.md │ └── available.md ├── usage │ ├── all-commands.md │ ├── shims-path.md │ └── plugins-commands.md ├── guides │ ├── faq.md │ └── intro.md ├── index.md └── misc │ └── vs-asdf.md ├── .tool-versions ├── internal ├── config │ ├── empty_test.yaml │ ├── config.yaml │ ├── registry.go │ ├── proxy.go │ ├── storage.go │ ├── legacy_version_file.go │ ├── config_test.go │ ├── cache.go │ ├── cache_test.go │ └── config.go ├── shim │ ├── binary │ │ ├── shim.exe │ │ ├── checksum.sha256 │ │ └── checksum.sha512 │ ├── shim.go │ ├── shim_unix.go │ └── shim_windows.go ├── plugin │ ├── luai │ │ ├── module │ │ │ ├── archiver │ │ │ │ ├── testdata │ │ │ │ │ └── test.zip │ │ │ │ ├── archiver_test.go │ │ │ │ └── archiver.go │ │ │ ├── file │ │ │ │ ├── file_test.go │ │ │ │ └── file.go │ │ │ ├── module.go │ │ │ └── string │ │ │ │ └── string_test.go │ │ ├── fixtures │ │ │ └── preload.lua │ │ ├── context.go │ │ └── vm.go │ └── testdata │ │ └── plugins │ │ └── java_with_metadata │ │ ├── hooks │ │ ├── pre_install.lua │ │ ├── pre_uninstall.lua │ │ ├── post_install.lua │ │ ├── available.lua │ │ ├── parse_legacy_file.lua │ │ ├── pre_use.lua │ │ └── env_keys.lua │ │ ├── metadata.lua │ │ └── lib │ │ └── util.lua ├── version.go ├── util │ ├── tty.go │ ├── runtime.go │ ├── tty_test.go │ ├── time.go │ ├── time_test.go │ ├── ci.go │ ├── clipboard_test.go │ ├── ci_test.go │ ├── checksum.go │ ├── version.go │ ├── error_store.go │ ├── version_test.go │ ├── downloader.go │ ├── clipboard.go │ └── set_test.go ├── base │ ├── world.go │ └── scope.go ├── env │ ├── env.go │ ├── path_test.go │ ├── path.go │ └── flag.go ├── shell │ ├── clink.go │ ├── zsh.go │ ├── shell_test.go │ └── nushell_test.go ├── registry.go ├── printer │ └── select_test.go ├── logger │ └── logger.go ├── toolset │ ├── file_record.go │ └── tool_version.go └── manager_test.go ├── logo.png ├── cmd ├── commands │ ├── base.go │ ├── upgrade_unix.go │ ├── cd.go │ ├── upgrade_win.go │ ├── available.go │ ├── current.go │ ├── update.go │ ├── remove.go │ ├── unuse.go │ ├── add.go │ ├── uninstall.go │ ├── list.go │ └── activate.go └── cmd.go ├── .gitignore ├── codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── bug_report.md │ ├── config.yml │ └── question_answer.md ├── dependabot.yml ├── FUNDING.yml └── workflows │ ├── ci.yml │ ├── go-releaser.yml │ ├── documentation.yml │ └── compile-inno-setup.yml ├── completions ├── powershell_autocomplete.ps1 ├── zsh_autocomplete └── bash_autocomplete ├── main.go ├── inno_setup ├── vfox_windows_i386.iss ├── vfox_windows_x86_64.iss ├── vfox_windows_aarch64.iss └── environment.iss ├── CONTRIBUTING.md ├── go.mod ├── scripts └── bump.sh └── install.sh /CNAME: -------------------------------------------------------------------------------- 1 | vfox.dev -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | vfox.dev -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.21.7 2 | -------------------------------------------------------------------------------- /internal/config/empty_test.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/logo.png -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/demo-full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/docs/public/demo-full.gif -------------------------------------------------------------------------------- /docs/public/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/docs/public/performance.png -------------------------------------------------------------------------------- /cmd/commands/base.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | const CategorySDK = "SDK" 4 | const CategoryPlugin = "Plugin" 5 | -------------------------------------------------------------------------------- /internal/shim/binary/shim.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/internal/shim/binary/shim.exe -------------------------------------------------------------------------------- /internal/shim/binary/checksum.sha256: -------------------------------------------------------------------------------- 1 | 410f84fe347cf55f92861ea3899d30b2d84a8bbc56bb3451d74697a4a0610b25 *shim.exe 2 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /internal/plugin/luai/module/archiver/testdata/test.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/version-fox/vfox/HEAD/internal/plugin/luai/module/archiver/testdata/test.zip -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /internal/shim/binary/checksum.sha512: -------------------------------------------------------------------------------- 1 | 9ce94adf48f7a31ab5773465582728c39db6f11a560fc43316fe6c1ad0a7b69a76aa3f9b52bb6b2e3be8043e4920985c8ca0bf157be9bf1e4a5a4d7c4ed195ba *shim.exe 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | /docs/node_modules/ 4 | /docs/.vitepress/.temp/ 5 | /docs/.vitepress/dist/ 6 | 7 | 8 | coverage.out 9 | **/available.cache 10 | **/.available.cache 11 | 12 | vfox -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.0.0" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev", 7 | "docs:build": "vitepress build", 8 | "docs:preview": "vitepress preview" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.8.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | # allow coverage to drop by this amount and still post success 7 | threshold: 0.5% 8 | if_ci_failed: error 9 | patch: off # no github status notice for coverage of the PR diff. 10 | -------------------------------------------------------------------------------- /internal/config/config.yaml: -------------------------------------------------------------------------------- 1 | proxy: 2 | enable: false 3 | url: http://test 4 | 5 | storage: 6 | sdkPath: /tmp 7 | 8 | registry: 9 | address: "https://cdn.jsdelivr.net/gh/version-fox/vfox-plugins/plugins" 10 | 11 | legacyVersionFile: 12 | enable: true 13 | 14 | cache: 15 | availableHookDuration: -1 -------------------------------------------------------------------------------- /docs/zh-hans/plugins/library/archiver.md: -------------------------------------------------------------------------------- 1 | # Archiver 标准库 2 | 3 | `vfox` 提供了解压工具, 支持`tar.gz`、`tgz`、`tar.xz`、`zip`、`7z`。在Lua脚本中,你可以使用`require("vfox.archiver")`来访问它。 4 | 例如: 5 | 6 | **Usage** 7 | ```shell 8 | local archiver = require("vfox.archiver") 9 | local err = archiver.decompress("testdata/test.zip", "testdata/test") 10 | ``` -------------------------------------------------------------------------------- /docs/plugins/library/json.md: -------------------------------------------------------------------------------- 1 | # JSON Library 2 | 3 | Based on [gopher-json](https://github.com/layeh/gopher-json) 4 | 5 | **Usage** 6 | ```lua 7 | local json = require("json") 8 | 9 | local obj = { "a", 1, "b", 2, "c", 3 } 10 | local jsonStr = json.encode(obj) 11 | local jsonObj = json.decode(jsonStr) 12 | for i = 1, #obj do 13 | assert(obj[i] == jsonObj[i]) 14 | end 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/plugins/library/archiver.md: -------------------------------------------------------------------------------- 1 | # Archiver Library 2 | 3 | `vfox` provides a decompression tool that supports `tar.gz`, `tgz`, `tar.xz`, `zip`, and `7z`. In Lua scripts, you can 4 | use `require("vfox.archiver")` to access it. 5 | 6 | **Usage** 7 | 8 | ```lua 9 | local archiver = require("vfox.archiver") 10 | local err = archiver.decompress("testdata/test.zip", "testdata/test") 11 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀Feature Request 3 | about: Please describe in detail the features you expect. 4 | title: '[Feature]: Some feature...' 5 | labels: ["enhancement"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### 1. Your usage scenarios? 13 | 14 | #### 2. What is your expected outcome? -------------------------------------------------------------------------------- /docs/zh-hans/plugins/library/json.md: -------------------------------------------------------------------------------- 1 | # Json标准库 2 | 3 | `vfox` 提供的 `json` 库是基于 [gopher-json](https://github.com/layeh/gopher-json) 实现的。 4 | 5 | 6 | **使用** 7 | ```shell 8 | local json = require("json") 9 | 10 | local obj = { "a", 1, "b", 2, "c", 3 } 11 | local jsonStr = json.encode(obj) 12 | local jsonObj = json.decode(jsonStr) 13 | for i = 1, #obj do 14 | assert(obj[i] == jsonObj[i]) 15 | end 16 | ``` 17 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/pre_install.lua: -------------------------------------------------------------------------------- 1 | local util = require("util") 2 | --- Returns some pre-installed information, such as version number, download address, local files, etc. 3 | --- If checksum is provided, vfox will automatically check it for you. 4 | --- @param ctx table 5 | --- @field ctx.version string User-input version 6 | --- @return table Version information 7 | function PLUGIN:PreInstall(ctx) 8 | return util:PreInstall(ctx) 9 | end -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/pre_uninstall.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | function PLUGIN:PreUninstall(ctx) 5 | printTable(ctx) 6 | local mainSdkInfo = ctx.main 7 | local mpath = mainSdkInfo.path 8 | local mversion = mainSdkInfo.version 9 | local mname = mainSdkInfo.name 10 | local sdkInfo = ctx.sdkInfo['sdk-name'] 11 | local path = sdkInfo.path 12 | local version = sdkInfo.version 13 | local name = sdkInfo.name 14 | end -------------------------------------------------------------------------------- /completions/powershell_autocomplete.ps1: -------------------------------------------------------------------------------- 1 | $fn = $($MyInvocation.MyCommand.Name) 2 | $name = $fn -replace "(.*)\.ps1$", '$1' 3 | Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { 4 | param($commandName, $wordToComplete, $cursorPosition) 5 | $other = "$wordToComplete --generate-bash-completion" 6 | Invoke-Expression $other | ForEach-Object { 7 | [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) 8 | } 9 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]: Some problem...' 5 | labels: ["bug"] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | The version you are currently using 12 | **OS** 13 | macOS、Linux、Windows 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **Screenshots[optional]** 19 | If applicable, add screenshots to help explain your problem. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 📜 Documentation 4 | url: https://vfox.dev/ 5 | about: Before submitting a question, please read the documentation. If you are still not satisfied, ask a question again. 6 | - name: 👀 GitHub Discussions 7 | url: https://github.com/version-fox/vfox/discussions 8 | about: If your issue is not a feature or bug, go to the discussion panel and retrieve if your issue already exists before submitting it. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question_answer.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🙋 Question Report 3 | about: Usage question that isn't answered in docs or discussion 4 | title: "[Question] Some question..." 5 | labels: ["question"] 6 | --- 7 | 8 | <-- Please answer these questions before submitting them. Thank you. --> 9 | 10 | **Version** 11 | The version you are currently using 12 | 13 | **OS** 14 | macOS、Linux、Windows 15 | 16 | **Question Description** 17 | A clear and concise description of what the question is. 18 | -------------------------------------------------------------------------------- /docs/zh-hans/plugins/library/html.md: -------------------------------------------------------------------------------- 1 | # Html标准库 2 | 3 | `vfox`提供的`html`库是基于[goquery](https://github.com/PuerkitoBio/goquery)实现的。 4 | 5 | 6 | **使用** 7 | ```shell 8 | local html = require("html") 9 | local doc = html.parse("
456
222
") 10 | local s = doc:find("div"):eq(1) 11 | local f = doc:find("div"):eq(0) 12 | local ss = doc:find("div"):eq(2) 13 | print(ss:text() == "") 14 | assert(s:text() == "222") 15 | assert(f:text() == "456") 16 | ``` 17 | -------------------------------------------------------------------------------- /completions/zsh_autocomplete: -------------------------------------------------------------------------------- 1 | #compdef vfox 2 | 3 | _cli_zsh_autocomplete() { 4 | local -a opts 5 | local cur 6 | cur=${words[-1]} 7 | if [[ "$cur" == "-"* ]]; then 8 | opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") 9 | else 10 | opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") 11 | fi 12 | 13 | if [[ "${opts[1]}" != "" ]]; then 14 | _describe 'values' opts 15 | else 16 | _files 17 | fi 18 | } 19 | 20 | compdef _cli_zsh_autocomplete vfox -------------------------------------------------------------------------------- /internal/plugin/luai/fixtures/preload.lua: -------------------------------------------------------------------------------- 1 | function printTable(t, indent) 2 | indent = indent or 0 3 | local strIndent = string.rep(" ", indent) 4 | for key, value in pairs(t) do 5 | local keyStr = tostring(key) 6 | local valueStr = tostring(value) 7 | if type(value) == "table" then 8 | print(strIndent .. "[" .. keyStr .. "] =>") 9 | printTable(value, indent + 1) 10 | else 11 | print(strIndent .. "[" .. keyStr .. "] => " .. valueStr) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/post_install.lua: -------------------------------------------------------------------------------- 1 | --- Extension point, called after PreInstall, can perform additional operations, 2 | --- such as file operations for the SDK installation directory or compile source code 3 | --- Currently can be left unimplemented! 4 | function PLUGIN:PostInstall(ctx) 5 | --- ctx.rootPath SDK installation directory 6 | local rootPath = ctx.rootPath 7 | local sdkInfo = ctx.sdkInfo['sdk-name'] 8 | local path = sdkInfo.path 9 | local version = sdkInfo.version 10 | local name = sdkInfo.name 11 | end -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /docs/plugins/library/html.md: -------------------------------------------------------------------------------- 1 | # HTML Library 2 | 3 | The HTML library provided by VersionFox is based on [goquery](https://github.com/PuerkitoBio/goquery), with some 4 | functionality encapsulated. You can use `require("html")` to access it, for example: 5 | 6 | 7 | **Usage** 8 | ```lua 9 | local html = require("html") 10 | local doc = html.parse("
456
222
") 11 | local s = doc:find("div"):eq(1) 12 | local f = doc:find("div"):eq(0) 13 | local ss = doc:find("div"):eq(2) 14 | print(ss:text() == "") 15 | assert(s:text() == "222") 16 | assert(f:text() == "456") 17 | ``` 18 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/available.lua: -------------------------------------------------------------------------------- 1 | --- Return all available versions provided by this plugin 2 | --- @param ctx table Empty table used as context, for future extension 3 | --- @return table Descriptions of available versions and accompanying tool descriptions 4 | function PLUGIN:Available(ctx) 5 | print("invoke Available Hook") 6 | return { 7 | { 8 | version = "xxxx", 9 | note = os.time(), 10 | addition = { 11 | { 12 | name = "npm", 13 | version = "8.8.8", 14 | } 15 | } 16 | } 17 | } 18 | end -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/metadata.lua: -------------------------------------------------------------------------------- 1 | --- !!! DO NOT EDIT OR RENAME !!! 2 | PLUGIN = {} 3 | 4 | --- !!! MUST BE SET !!! 5 | --- Plugin name 6 | PLUGIN.name = "java_with_metadata" 7 | --- Plugin version 8 | PLUGIN.version = "0.0.1" 9 | -- Repository URL 10 | PLUGIN.repository = "https://github.com/version-fox/vfox-plugin-template" 11 | 12 | PLUGIN.notes = { 13 | "", 14 | } 15 | 16 | --- !!! OPTIONAL !!! 17 | --- Plugin description 18 | PLUGIN.description = "xxx" 19 | -- minimum compatible vfox version 20 | PLUGIN.minRuntimeVersion = "0.2.2" 21 | 22 | PLUGIN.manifestUrl = "manifest.json" 23 | 24 | PLUGIN.legacyFilenames = { 25 | ".node-version", 26 | ".nvmrc" 27 | } -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | const RuntimeVersion = "0.9.2" 20 | -------------------------------------------------------------------------------- /docs/zh-hans/plugins/library/strings.md: -------------------------------------------------------------------------------- 1 | # Strings 标准库 2 | 3 | `vfox` 提供了一些字符串操作的工具。在Lua脚本中,你可以使用`require("vfox.strings")`来访问它。 4 | 例如: 5 | 6 | **Usage** 7 | ```shell 8 | local strings = require("vfox.strings") 9 | local str_parts = strings.split("hello world", " ") 10 | print(str_parts[1]) -- hello 11 | 12 | assert(strings.has_prefix("hello world", "hello"), [[not strings.has_prefix("hello")]]) 13 | assert(strings.has_suffix("hello world", "world"), [[not strings.has_suffix("world")]]) 14 | assert(strings.trim("hello world", "world") == "hello ", "strings.trim()") 15 | assert(strings.contains("hello world", "hello ") == true, "strings.contains()") 16 | 17 | got = strings.trim_space(tt.input) 18 | 19 | local str = strings.join({"1",3,"4"},";") 20 | assert(str == "1;3;4", "strings.join()") 21 | ``` -------------------------------------------------------------------------------- /internal/plugin/luai/module/archiver/archiver_test.go: -------------------------------------------------------------------------------- 1 | package archiver 2 | 3 | import ( 4 | lua "github.com/yuin/gopher-lua" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestDecompress(t *testing.T) { 10 | const str = ` 11 | local archiver = require("vfox.archiver") 12 | local err = archiver.decompress("testdata/test.zip", "testdata/test") 13 | assert(err == nil, "strings.decompress()") 14 | local f = io.open("testdata/test/test.txt", "r") 15 | if f then 16 | f:close() 17 | else 18 | error("file not found") 19 | end 20 | ` 21 | defer func() { 22 | _ = os.RemoveAll("testdata/test") 23 | }() 24 | eval(str, t) 25 | } 26 | 27 | func eval(str string, t *testing.T) { 28 | s := lua.NewState() 29 | defer s.Close() 30 | 31 | Preload(s) 32 | if err := s.DoString(str); err != nil { 33 | t.Error(err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd/commands/upgrade_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | * Copyright 2025 Han Li and contributors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package commands 20 | 21 | func RequestPermission() error { 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/parse_legacy_file.lua: -------------------------------------------------------------------------------- 1 | --- Parse the legacy file found by vfox to determine the version of the tool. 2 | --- Useful to extract version numbers from files like JavaScript's package.json or Golangs go.mod. 3 | function PLUGIN:ParseLegacyFile(ctx) 4 | printTable(ctx) 5 | local filename = ctx.filename 6 | local filepath = ctx.filepath 7 | 8 | installed = ctx.getInstalledVersions() 9 | if #installed > 0 then 10 | print("Installed: " .. installed[1]) 11 | return { 12 | version = "check-installed" 13 | } 14 | end 15 | 16 | if filename == ".node-version" then 17 | return { 18 | version = "14.17.0" 19 | } 20 | else 21 | return { 22 | version = "0.0.1" 23 | } 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /docs/plugins/library/strings.md: -------------------------------------------------------------------------------- 1 | # Strings Library 2 | 3 | `vfox` provides some utils for string manipulation. In the Lua script, you can use `require("vfox.strings")` to access it. 4 | For example: 5 | 6 | **Usage** 7 | ```lua 8 | local strings = require("vfox.strings") 9 | local str_parts = strings.split("hello world", " ") 10 | print(str_parts[1]) -- hello 11 | 12 | assert(strings.has_prefix("hello world", "hello"), [[not strings.has_prefix("hello")]]) 13 | assert(strings.has_suffix("hello world", "world"), [[not strings.has_suffix("world")]]) 14 | assert(strings.trim("hello world", "world") == "hello ", "strings.trim()") 15 | assert(strings.contains("hello world", "hello ") == true, "strings.contains()") 16 | 17 | got = strings.trim_space(tt.input) 18 | 19 | local str = strings.join({"1",3,"4"},";") 20 | assert(str == "1;3;4", "strings.join()") 21 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/version-fox/vfox/cmd" 21 | "os" 22 | ) 23 | 24 | func main() { 25 | cmd.Execute(os.Args) 26 | } 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: aooohan 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /internal/config/registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | type Registry struct { 20 | Address string `yaml:"address"` 21 | } 22 | 23 | var EmptyRegistry = &Registry{ 24 | Address: "", 25 | } 26 | -------------------------------------------------------------------------------- /internal/config/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | type Proxy struct { 20 | Url string `yaml:"url"` 21 | Enable bool `yaml:"enable"` 22 | } 23 | 24 | var ( 25 | EmptyProxy = &Proxy{ 26 | Url: "", 27 | Enable: false, 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /internal/util/tty.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "os" 21 | 22 | "golang.org/x/term" 23 | ) 24 | 25 | // IsTTY checks if the process is running in a TTY (interactive terminal) 26 | func IsTTY() bool { 27 | return term.IsTerminal(int(os.Stdout.Fd())) 28 | } 29 | -------------------------------------------------------------------------------- /docs/zh-hans/guides/faq.md: -------------------------------------------------------------------------------- 1 | # 常见问题 2 | 3 | ## 如何卸载vfox? 4 | 5 | 请参考[卸载指南](./uninstallation.md)了解如何从系统中完全删除vfox的详细说明。 6 | 7 | ## 切换xxx不生效? `vfox use`命令不生效? 8 | 9 | 如果你看到提示`Warning: The current shell lacks hook support or configuration. It has switched to global scope automatically` 10 | 则说明你没有将`vfox`正确挂在到你的`Shell`上。 11 | 12 | 请按照[快速入门#_2-挂载vfox到你的shell](./quick-start.md#_2-挂载vfox到你的shell)步骤进行手动挂载。 13 | 14 | 15 | ## Windows下PATH环境变量值重复? 16 | 17 | 只有一种情况下会出现这种情况, 就是你全局(`vfox use -g`)使用过SDK, 这个时候`vfox`会操作注册表,将SDK的`PATH`写入用户环境变量当中(为的是, 18 | **不支持Hook功能**的Shell也能使用SDK, 例如`CMD`)。 19 | 20 | 但是因为`.tool-versions`机制的存在, 所以`PATH`就变成了`.tool-versions` + 用户环境变量`PATH`两部分组成。 21 | 22 | ::: warning 23 | 同一个SDK**最多重复两条**, 不会无限重复。如果>2次, 请反馈给我们。 24 | ::: 25 | 26 | ## GitBash下`use`和`search`命令无法进行选择? 27 | 28 | 相关ISSUE: [GitBash下无法进行选择](https://github.com/version-fox/vfox/issues/98) 29 | 30 | 仅限直接打开原生GitBash运行`vfox`会出现无法进行选择的问题, 临时解决办法: 31 | 1. 通过`Windows Terminal`运行`GitBash` 32 | 2. 在`VS Code`中运行`GitBash` 33 | -------------------------------------------------------------------------------- /internal/util/runtime.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import "runtime" 20 | 21 | type ( 22 | OSType string 23 | ArchType string 24 | ) 25 | 26 | func GetOSType() OSType { 27 | return OSType(runtime.GOOS) 28 | } 29 | 30 | func GetArchType() ArchType { 31 | return ArchType(runtime.GOARCH) 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: VersionFox CI 2 | 3 | on: [push] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [macos-latest, ubuntu-latest, windows-latest] 15 | go-version: [ '1.21.x' ] 16 | 17 | steps: 18 | - uses: actions/checkout@v6 19 | - name: Setup Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | - name: Install dependencies 24 | run: | 25 | go get . 26 | - name: Build 27 | run: | 28 | go build . 29 | - name: Test and coverage 30 | run: | 31 | go test ./... -coverprofile=coverage.out -covermode=atomic 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v5 34 | -------------------------------------------------------------------------------- /docs/zh-hans/plugins/library/http.md: -------------------------------------------------------------------------------- 1 | # Http 标准库 2 | 3 | `vfox`提供了一个简单的 http 能力,当前支持`Get`、`Head`两种请求类型,以及文件下载。 4 | 5 | **使用** 6 | 7 | ```lua 8 | local http = require("http") 9 | --- get 请求, 不要用此请求进行文件下载!!! 10 | local resp, err = http.get({ 11 | url = "https://httpbin.org/json", 12 | headers = { 13 | ['Host'] = "localhost" 14 | } 15 | }) 16 | --- 返回参数 17 | assert(err == nil) 18 | assert(resp.status_code == 200) 19 | assert(resp.headers['Content-Type'] == 'application/json') 20 | assert(resp.body == 'xxxxxxxx') 21 | 22 | --- head 请求 23 | resp, err = http.head({ 24 | url = "https://httpbin.org/json", 25 | headers = { 26 | ['Host'] = "localhost" 27 | } 28 | }) 29 | assert(err == nil) 30 | assert(resp.status_code == 200) 31 | assert(resp.content_length ~= 0) 32 | 33 | --- 下载文件, vfox >= 0.4.0 34 | err = http.download_file({ 35 | url = "https://version-fox.github.io/vfox-plugins/index.json", 36 | headers = {} 37 | }, "/usr/local/file") 38 | assert(err == nil, [[must be nil]] ) 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /internal/base/world.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | type HookFunc struct { 4 | Name string 5 | Required bool 6 | Filename string 7 | } 8 | 9 | var ( 10 | // HookFuncMap is a map of built-in hook functions. 11 | HookFuncMap = map[string]HookFunc{ 12 | "Available": {Name: "Available", Required: true, Filename: "available"}, 13 | "PreInstall": {Name: "PreInstall", Required: true, Filename: "pre_install"}, 14 | "EnvKeys": {Name: "EnvKeys", Required: true, Filename: "env_keys"}, 15 | "PostInstall": {Name: "PostInstall", Required: false, Filename: "post_install"}, 16 | "PreUse": {Name: "PreUse", Required: false, Filename: "pre_use"}, 17 | "ParseLegacyFile": {Name: "ParseLegacyFile", Required: false, Filename: "parse_legacy_file"}, 18 | "PreUninstall": {Name: "PreUninstall", Required: false, Filename: "pre_uninstall"}, 19 | } 20 | ) 21 | 22 | const ( 23 | PluginObjKey = "PLUGIN" 24 | NavigatorObjKey = "VFOX_NAVIGATOR" 25 | OsType = "OS_TYPE" 26 | ArchType = "ARCH_TYPE" 27 | Runtime = "RUNTIME" 28 | ) 29 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/pre_use.lua: -------------------------------------------------------------------------------- 1 | --- When user invoke `use` command, this function will be called to get the 2 | --- valid version information. 3 | --- @param ctx table Context information 4 | function PLUGIN:PreUse(ctx) 5 | --- user input version 6 | local version = ctx.version 7 | --- installed sdks 8 | local sdkInfo = ctx.installedSdks['xxxx'] 9 | local path = sdkInfo.path 10 | local name = sdkInfo.name 11 | local sdkVersion = sdkInfo.version 12 | 13 | --- working directory 14 | local cwd = ctx.cwd 15 | 16 | printTable(ctx) 17 | 18 | --- user input scope 19 | local scope = ctx.scope 20 | 21 | if (scope == "global") then 22 | print("return 9.9.9") 23 | return { 24 | version = "9.9.9", 25 | } 26 | end 27 | 28 | if (scope == "project") then 29 | print("return 10.0.0") 30 | return { 31 | version = "10.0.0", 32 | } 33 | end 34 | 35 | print("return 1.0.0") 36 | 37 | return { 38 | version = "1.0.0" 39 | } 40 | end 41 | -------------------------------------------------------------------------------- /internal/shim/shim.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package shim 18 | 19 | // Shim is a struct that contains the binary path and output path which is used to generate the shim. 20 | type Shim struct { 21 | BinaryPath string 22 | OutputPath string 23 | } 24 | 25 | // NewShim creates a new Shim instance. 26 | func NewShim(binaryPath, outputPath string) *Shim { 27 | return &Shim{ 28 | BinaryPath: binaryPath, 29 | OutputPath: outputPath, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /internal/util/tty_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestIsTTY(t *testing.T) { 24 | // IsTTY returns a boolean based on whether stdout is a terminal 25 | // In test environment, it's typically false 26 | result := IsTTY() 27 | // Just verify it returns without panic 28 | if result { 29 | t.Log("Running in a TTY environment") 30 | } else { 31 | t.Log("Not running in a TTY environment") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/plugin/luai/module/archiver/archiver.go: -------------------------------------------------------------------------------- 1 | package archiver 2 | 3 | import ( 4 | "github.com/version-fox/vfox/internal/util" 5 | lua "github.com/yuin/gopher-lua" 6 | ) 7 | 8 | // Preload adds strings to the given Lua state's package.preload table. After it 9 | // has been preloaded, it can be loaded using require: 10 | // 11 | // local strings = require("vfox.archiver") 12 | func Preload(L *lua.LState) { 13 | L.PreloadModule("vfox.archiver", Loader) 14 | } 15 | 16 | // Loader is the module loader function. 17 | func Loader(L *lua.LState) int { 18 | t := L.NewTable() 19 | L.SetFuncs(t, api) 20 | L.Push(t) 21 | return 1 22 | } 23 | 24 | var api = map[string]lua.LGFunction{ 25 | "decompress": decompress, 26 | } 27 | 28 | // decompress lua archiver.decompress(sourceFile, targetPath): port of go string.decompress() returns error 29 | func decompress(L *lua.LState) int { 30 | archiverPath := L.CheckString(1) 31 | targetPath := L.CheckString(2) 32 | 33 | err := util.NewDecompressor(archiverPath).Decompress(targetPath) 34 | if err != nil { 35 | L.Push(lua.LString(err.Error())) 36 | return 1 37 | } else { 38 | L.Push(lua.LNil) 39 | } 40 | return 1 41 | } 42 | -------------------------------------------------------------------------------- /completions/bash_autocomplete: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | : ${PROG:=$(basename ${BASH_SOURCE})} 4 | 5 | # Macs have bash3 for which the bash-completions package doesn't include 6 | # _init_completion. This is a minimal version of that function. 7 | _cli_init_completion() { 8 | COMPREPLY=() 9 | _get_comp_words_by_ref "$@" cur prev words cword 10 | } 11 | 12 | _cli_bash_autocomplete() { 13 | if [[ "${COMP_WORDS[0]}" != "source" ]]; then 14 | local cur opts base words 15 | COMPREPLY=() 16 | cur="${COMP_WORDS[COMP_CWORD]}" 17 | if declare -F _init_completion >/dev/null 2>&1; then 18 | _init_completion -n "=:" || return 19 | else 20 | _cli_init_completion -n "=:" || return 21 | fi 22 | words=("${words[@]:0:$cword}") 23 | if [[ "$cur" == "-"* ]]; then 24 | requestComp="${words[*]} ${cur} --generate-bash-completion" 25 | else 26 | requestComp="${words[*]} --generate-bash-completion" 27 | fi 28 | opts=$(eval "${requestComp}" 2>/dev/null) 29 | COMPREPLY=($(compgen -W "${opts}" -- ${cur})) 30 | return 0 31 | fi 32 | } 33 | 34 | complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG 35 | unset PROG -------------------------------------------------------------------------------- /docs/plugins/library/http.md: -------------------------------------------------------------------------------- 1 | # HTTP Library 2 | 3 | `vfox` provides a simple HTTP library, currently supporting only GET and HEAD requests and download file. In the Lua script, you can 4 | use `require("http")` to access it. For example: 5 | 6 | **Usage** 7 | 8 | ```lua 9 | local http = require("http") 10 | --- get request, do not use this request to download files!!! 11 | local resp, err = http.get({ 12 | url = "https://httpbin.org/json", 13 | headers = { 14 | ['Host'] = "localhost" 15 | } 16 | }) 17 | --- return parameters 18 | assert(err == nil) 19 | assert(resp.status_code == 200) 20 | assert(resp.headers['Content-Type'] == 'application/json') 21 | assert(resp.body == 'xxxxx') 22 | 23 | --- head request 24 | resp, err = http.head({ 25 | url = "https://httpbin.org/json", 26 | headers = { 27 | ['Host'] = "localhost" 28 | } 29 | }) 30 | assert(err == nil) 31 | assert(resp.status_code == 200) 32 | assert(resp.content_length ~= 0) 33 | 34 | --- Download file, vfox >= 0.4.0 35 | err = http.download_file({ 36 | url = "https://version-fox.github.io/vfox-plugins/index.json", 37 | headers = {} 38 | }, "/usr/local/file") 39 | assert(err == nil, [[must be nil]] ) 40 | 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/zh-hans/misc/vs-asdf.md: -------------------------------------------------------------------------------- 1 | # 比asdf-vm好在哪里? 2 | 3 | `vfox` 与 `asdf-vm` 目标一致, 即一个工具管理所有的运行时版本, 且都是用`.tool-versions`文件来记录版本信息。 4 | 5 | 但是 `vfox` 有以下优势: 6 | 7 | ## 操作系统兼容性 8 | 9 | | 操作系统兼容性 | Windows(非WSL) | Linux | macOS | 10 | |------------|---------------|-------|-------| 11 | | asdf-vm | ❌ | ✅ | ✅ | 12 | | VersionFox | ✅ | ✅ | ✅ | 13 | 14 | `asdf-vm`是`Shell`实现的工具, 所以对于**原生Windows**环境并**不支持**! 15 | 16 | 而[vfox](https://github.com/version-fox/vfox)是`Golang` + `Lua`实现的, 因此天然支持`Windows`和其他操作系统。 17 | 18 | 19 | 20 | ## 性能对比 21 | 22 | ![performance.png](/performance.png) 23 | 24 | 上图是对两个工具最核心的功能进行基准测试, 会发现[vfox](https://github.com/version-fox/vfox)大约比`asdf-vm`快**5倍**! 25 | 26 | `asdf-vm`的执行速度之所以较慢,主要是由于其`垫片`机制。简单来说,当你尝试运行如`node`这样的命令时,`asdf-vm` 27 | 会首先查找对应的垫片,然后根据`.tool-versions`文件或全局配置来确定使用哪个版本的`node`。这个查找和确定版本的过程会消耗一定的时间,从而影响了命令的执行速度。 28 | 29 | 相比之下,[vfox](https://github.com/version-fox/vfox) 30 | 采用了直接操作环境变量的方式来管理版本,它会直接设置和切换环境变量,从而避免了查找和确定版本的过程。因此,[vfox](https://github.com/version-fox/vfox) 31 | 在执行速度上要比使用`垫片`机制的`asdf-vm`快得多。 32 | 33 | `asdf-vm`生态很强, 但是他对**Windows原生**无能为力, 虽然[vfox](https://github.com/version-fox/vfox)很新, 34 | 但是性能和平台兼容方面做的比`asdf-vm`更好。 35 | 36 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/hooks/env_keys.lua: -------------------------------------------------------------------------------- 1 | --- Each SDK may have different environment variable configurations. 2 | --- This allows plugins to define custom environment variables (including PATH settings) 3 | --- Note: Be sure to distinguish between environment variable settings for different platforms! 4 | --- @param ctx table Context information 5 | --- @field ctx.path string SDK installation directory 6 | function PLUGIN:EnvKeys(ctx) 7 | --- this variable is same as ctx.sdkInfo['plugin-name'].path 8 | local mainPath = ctx.path 9 | local mainSdkInfo = ctx.main 10 | local mpath = mainSdkInfo.path 11 | local mversion = mainSdkInfo.version 12 | local mname = mainSdkInfo.name 13 | local sdkInfo = ctx.sdkInfo['sdk-name'] 14 | local path = sdkInfo.path 15 | local version = sdkInfo.version 16 | local name = sdkInfo.name 17 | return { 18 | { 19 | key = "JAVA_HOME", 20 | value = mainPath 21 | }, 22 | { 23 | key = "PATH", 24 | value = mainPath .. "/bin" 25 | }, 26 | { 27 | key = "PATH", 28 | value = mainPath .. "/bin2" 29 | }, 30 | 31 | } 32 | 33 | end -------------------------------------------------------------------------------- /internal/plugin/luai/module/file/file_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package html 18 | 19 | import ( 20 | "testing" 21 | 22 | lua "github.com/yuin/gopher-lua" 23 | ) 24 | 25 | func TestRequire(t *testing.T) { 26 | const str = ` 27 | local file = require("file") 28 | assert(type(file) == "table") 29 | assert(type(file.symlink) == "function") 30 | ` 31 | evalLua(str, t) 32 | } 33 | 34 | func evalLua(str string, t *testing.T) { 35 | s := lua.NewState() 36 | defer s.Close() 37 | Preload(s, "") 38 | if err := s.DoString(str); err != nil { 39 | t.Error(err) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /internal/util/time.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "time" 21 | ) 22 | 23 | func GetTimestamp() int64 { 24 | return time.Now().Unix() 25 | } 26 | 27 | func GetBeginOfToday() int64 { 28 | now := time.Now() 29 | return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix() 30 | } 31 | 32 | func IsBeforeToday(timestamp int64) bool { 33 | t := time.Unix(timestamp, 0) 34 | t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) 35 | now := time.Now() 36 | now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 37 | return t.Before(now) 38 | } 39 | -------------------------------------------------------------------------------- /internal/plugin/testdata/plugins/java_with_metadata/lib/util.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | local util = {} 4 | 5 | 6 | 7 | function util:PreInstall(ctx) 8 | local version = ctx.version 9 | print(OS_TYPE) 10 | return { 11 | --- Version number 12 | version = "version", 13 | --- remote URL or local file path [optional] 14 | url = "xxx", 15 | --- SHA256 checksum [optional] 16 | sha256 = "xxx", 17 | --- md5 checksum [optional] 18 | md5 = "xxx", 19 | --- sha1 checksum [optional] 20 | sha1 = "xxx", 21 | --- sha512 checksum [optional] 22 | sha512 = "xx", 23 | --- additional need files [optional] 24 | addition = { 25 | { 26 | --- additional file name ! 27 | name = "xxx", 28 | --- remote URL or local file path [optional] 29 | url = "xxx", 30 | --- SHA256 checksum [optional] 31 | sha256 = "xxx", 32 | --- md5 checksum [optional] 33 | md5 = "xxx", 34 | --- sha1 checksum [optional] 35 | sha1 = "xxx", 36 | --- sha512 checksum [optional] 37 | sha512 = "xx", 38 | } 39 | } 40 | } 41 | end 42 | 43 | 44 | return util -------------------------------------------------------------------------------- /internal/util/time_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | ) 23 | 24 | func TestGetTimestamp(t *testing.T) { 25 | timestamp := GetTimestamp() 26 | if timestamp == 0 { 27 | t.Errorf("GetTimestamp() = %v, want non-zero", timestamp) 28 | } 29 | } 30 | 31 | func TestIsBeforeToday(t *testing.T) { 32 | yesterday := time.Now().AddDate(0, 0, -1).Unix() 33 | if !IsBeforeToday(yesterday) { 34 | t.Errorf("IsBeforeToday(yesterday) = false, want true") 35 | } 36 | 37 | tomorrow := time.Now().AddDate(0, 0, 1).Unix() 38 | if IsBeforeToday(tomorrow) { 39 | t.Errorf("IsBeforeToday(tomorrow) = true, want false") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/env/env.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package env 18 | 19 | import ( 20 | "io" 21 | ) 22 | 23 | type Manager interface { 24 | Flush() error 25 | Load(envs *Envs) error 26 | Get(key string) (string, bool) 27 | Remove(envs *Envs) error 28 | io.Closer 29 | } 30 | 31 | // Vars is a map of environment variables 32 | type Vars map[string]*string 33 | 34 | // Envs is a struct that contains environment variables and PATH. 35 | type Envs struct { 36 | Variables Vars 37 | BinPaths *Paths 38 | Paths *Paths 39 | } 40 | 41 | func (e *Envs) MergePaths(envs *Envs) { 42 | if envs == nil { 43 | return 44 | } 45 | e.BinPaths.Merge(envs.BinPaths) 46 | e.Paths.Merge(envs.Paths) 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/go-releaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: [ 'v*' ] 6 | 7 | permissions: 8 | contents: write 9 | id-token: write 10 | packages: write 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | env: 16 | flags: "" 17 | steps: 18 | - if: ${{ !startsWith(github.ref, 'refs/tags/v') }} 19 | run: echo "flags=--snapshot" >> $GITHUB_ENV 20 | - uses: actions/checkout@v6 21 | with: 22 | fetch-depth: 0 23 | - uses: actions/setup-go@v5 24 | with: 25 | go-version: '1.23' 26 | cache: true 27 | - uses: sigstore/cosign-installer@v3.10.1 28 | - uses: anchore/sbom-action/download-syft@v0.19.0 29 | - uses: goreleaser/goreleaser-action@v6 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean ${{ env.flags }} 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | COSIGN_PWD: ${{ secrets.COSIGN_PWD }} 37 | HOMEBREW_TOKEN: ${{ secrets.HOMEBREW_TOKEN}} 38 | 39 | - name: Publish rpm to Gemfury 40 | env: 41 | FURY_TOKEN: ${{ secrets.FURY_TOKEN }} 42 | run: | 43 | for filename in dist/vfox*.{rpm,deb}; do 44 | curl -F package=@"$filename" https://{$FURY_TOKEN}@push.fury.io/versionfox/ 45 | done -------------------------------------------------------------------------------- /inno_setup/vfox_windows_i386.iss: -------------------------------------------------------------------------------- 1 | #include "environment.iss" 2 | 3 | #define MyAppName "vfox" 4 | #define MyAppVersion GetEnv("VFOX_VERSION") 5 | #define MyAppPublisher "Han Li" 6 | #define MyAppURL "https://github.com/version-fox/vfox" 7 | 8 | [Setup] 9 | AppId={#MyAppName}-fc742fc3-7013-49b7-adcb-96f2d6ddbda0 10 | AppName={#MyAppName} 11 | AppVersion={#MyAppVersion} 12 | AppVerName={#MyAppName} {#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={autopf}\{#MyAppName} 18 | DisableDirPage=yes 19 | DefaultGroupName={#MyAppName} 20 | DisableProgramGroupPage=yes 21 | OutputBaseFilename=vfox_{#MyAppVersion}_windows_setup_i386 22 | Compression=zip 23 | SolidCompression=yes 24 | WizardStyle=modern 25 | ChangesEnvironment=true 26 | 27 | [Languages] 28 | Name: "english"; MessagesFile: "compiler:Default.isl" 29 | 30 | [Files] 31 | Source: "{#MyAppName}_{#MyAppVersion}_windows_i386/vfox.exe"; DestDir: "{app}"; Flags: ignoreversion 32 | 33 | [Code] 34 | procedure CurStepChanged(CurStep: TSetupStep); 35 | begin 36 | if CurStep = ssPostInstall 37 | then EnvAddPath(ExpandConstant('{app}')); 38 | end; 39 | 40 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 41 | begin 42 | if CurUninstallStep = usPostUninstall 43 | then EnvRemovePath(ExpandConstant('{app}')); 44 | end; -------------------------------------------------------------------------------- /internal/shell/clink.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package shell 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/version-fox/vfox/internal/env" 23 | ) 24 | 25 | const clinkHook = ` 26 | {{.EnvContent}} 27 | "{{.SelfPath}}" env --cleanup > nul 2> nul 28 | ` 29 | 30 | type clink struct{} 31 | 32 | var Clink = clink{} 33 | 34 | func (b clink) Activate(config ActivateConfig) (string, error) { 35 | return clinkHook, nil 36 | } 37 | 38 | func (b clink) Export(envs env.Vars) (out string) { 39 | for key, value := range envs { 40 | if value == nil { 41 | out += b.set(key, "") 42 | } else { 43 | out += b.set(key, *value) 44 | } 45 | } 46 | return 47 | } 48 | 49 | func (b clink) set(key, value string) string { 50 | return fmt.Sprintf("set \"%s=%s\"\n", key, value) 51 | } 52 | -------------------------------------------------------------------------------- /cmd/commands/cd.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/urfave/cli/v3" 9 | "github.com/version-fox/vfox/internal" 10 | "github.com/version-fox/vfox/internal/env" 11 | "github.com/version-fox/vfox/internal/shell" 12 | ) 13 | 14 | var Cd = &cli.Command{ 15 | Name: "cd", 16 | Usage: "Launch a shell in the VFOX_HOME or SDK directory", 17 | Flags: []cli.Flag{ 18 | &cli.BoolFlag{ 19 | Name: "plugin", 20 | Aliases: []string{"p"}, 21 | Usage: "Launch a shell in the plugin directory", 22 | }, 23 | }, 24 | Action: cdCmd, 25 | } 26 | 27 | func cdCmd(ctx context.Context, cmd *cli.Command) error { 28 | var dir string 29 | 30 | manager := internal.NewSdkManager() 31 | if cmd.Args().Len() == 0 { 32 | dir = manager.PathMeta.HomePath 33 | } else { 34 | sdkName := cmd.Args().First() 35 | sdk, err := manager.LookupSdk(sdkName) 36 | if err != nil { 37 | return err 38 | } 39 | if cmd.Bool("plugin") { 40 | dir = sdk.Plugin.Path 41 | } else { 42 | current := sdk.Current() 43 | if current == "" { 44 | return fmt.Errorf("no current version of %s", sdkName) 45 | } 46 | sdkPackage, err := sdk.GetLocalSdkPackage(current) 47 | if err != nil { 48 | return err 49 | } 50 | dir = sdkPackage.Main.Path 51 | } 52 | } 53 | manager.Close() 54 | 55 | err := os.Chdir(dir) 56 | if err != nil { 57 | return err 58 | } 59 | return shell.Open(env.GetPid()) 60 | } 61 | -------------------------------------------------------------------------------- /docs/zh-hans/usage/all-commands.md: -------------------------------------------------------------------------------- 1 | # 所有命令 2 | 3 | ```shell 4 | vfox - vfox is a tool for runtime version management. 5 | vfox available List all available plugins 6 | vfox add [--alias --source ] Add a plugin or plugins from official repository or custom source, `--alias` and `--source` are not supported when adding multiple plugins. 7 | vfox remove Remove a plugin 8 | vfox update [ | --all] Update a specified or all plugin(s) 9 | vfox info [@] [options] 显示插件信息或 SDK 路径,支持格式化选项 10 | vfox search Search available versions of a SDK 11 | vfox install @ Install the specified version of SDK 12 | vfox uninstall @ Uninstall the specified version of SDK 13 | vfox use [--global --project --session] [@] Use the specified version of SDK for different scope 14 | vfox unuse [--global --project --session] Unset the SDK version from the specified scope 15 | vfox list [] List all installed versions of SDK 16 | vfox current [] Show the current version of SDK 17 | vfox config [] [] Setup, view config 18 | vfox cd [--plugin] [] Launch a shell in the VFOX_HOME, SDK directory, or plugin directory 19 | vfox upgrade Upgrade vfox to the latest version 20 | vfox help Show this help message 21 | ``` 22 | -------------------------------------------------------------------------------- /internal/config/storage.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | ) 24 | 25 | type Storage struct { 26 | SdkPath string `yaml:"sdkPath"` 27 | } 28 | 29 | var EmptyStorage = &Storage{ 30 | SdkPath: "", 31 | } 32 | 33 | func (s *Storage) Validate() error { 34 | if s.SdkPath == "" { 35 | return nil 36 | } 37 | stat, err := os.Stat(s.SdkPath) 38 | if err != nil { 39 | return err 40 | } 41 | if !stat.IsDir() { 42 | return fmt.Errorf("%s is not a directory", s.SdkPath) 43 | } 44 | tmpfn := filepath.Join(s.SdkPath, ".tmpfile") 45 | f, err := os.OpenFile(tmpfn, os.O_WRONLY|os.O_CREATE, os.ModePerm) 46 | if err != nil { 47 | return err 48 | } 49 | defer f.Close() 50 | defer os.Remove(tmpfn) 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /inno_setup/vfox_windows_x86_64.iss: -------------------------------------------------------------------------------- 1 | #include "environment.iss" 2 | 3 | #define MyAppName "vfox" 4 | #define MyAppVersion GetEnv("VFOX_VERSION") 5 | #define MyAppPublisher "Han Li" 6 | #define MyAppURL "https://github.com/version-fox/vfox" 7 | 8 | [Setup] 9 | AppId={#MyAppName}-fc742fc3-7013-49b7-adcb-96f2d6ddbda0 10 | AppName={#MyAppName} 11 | AppVersion={#MyAppVersion} 12 | AppVerName={#MyAppName} {#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={autopf}\{#MyAppName} 18 | DisableDirPage=yes 19 | DefaultGroupName={#MyAppName} 20 | DisableProgramGroupPage=yes 21 | OutputBaseFilename=vfox_{#MyAppVersion}_windows_setup_x86_64 22 | Compression=zip 23 | SolidCompression=yes 24 | WizardStyle=modern 25 | ChangesEnvironment=true 26 | ArchitecturesAllowed=x64 27 | ArchitecturesInstallIn64BitMode=x64 28 | 29 | [Languages] 30 | Name: "english"; MessagesFile: "compiler:Default.isl" 31 | 32 | [Files] 33 | Source: "{#MyAppName}_{#MyAppVersion}_windows_x86_64/vfox.exe"; DestDir: "{app}"; Flags: ignoreversion 34 | 35 | [Code] 36 | procedure CurStepChanged(CurStep: TSetupStep); 37 | begin 38 | if CurStep = ssPostInstall 39 | then EnvAddPath(ExpandConstant('{app}')); 40 | end; 41 | 42 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 43 | begin 44 | if CurUninstallStep = usPostUninstall 45 | then EnvRemovePath(ExpandConstant('{app}')); 46 | end; -------------------------------------------------------------------------------- /internal/base/scope.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package base 18 | 19 | type UseScope int 20 | 21 | type Location int 22 | 23 | const ( 24 | Global UseScope = iota 25 | Project 26 | Session 27 | ) 28 | 29 | const ( 30 | OriginalLocation Location = iota 31 | GlobalLocation 32 | ShellLocation 33 | ) 34 | 35 | func (s UseScope) String() string { 36 | switch s { 37 | case Global: 38 | return "global" 39 | case Project: 40 | return "project" 41 | case Session: 42 | return "session" 43 | default: 44 | return "unknown" 45 | } 46 | } 47 | 48 | func (s Location) String() string { 49 | switch s { 50 | case GlobalLocation: 51 | return "global" 52 | case ShellLocation: 53 | return "shell" 54 | case OriginalLocation: 55 | return "original" 56 | default: 57 | return "unknown" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /inno_setup/vfox_windows_aarch64.iss: -------------------------------------------------------------------------------- 1 | #include "environment.iss" 2 | 3 | #define MyAppName "vfox" 4 | #define MyAppVersion GetEnv("VFOX_VERSION") 5 | #define MyAppPublisher "Han Li" 6 | #define MyAppURL "https://github.com/version-fox/vfox" 7 | 8 | [Setup] 9 | AppId={#MyAppName}-fc742fc3-7013-49b7-adcb-96f2d6ddbda0 10 | AppName={#MyAppName} 11 | AppVersion={#MyAppVersion} 12 | AppVerName={#MyAppName} {#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={autopf}\{#MyAppName} 18 | DisableDirPage=yes 19 | DefaultGroupName={#MyAppName} 20 | DisableProgramGroupPage=yes 21 | OutputBaseFilename=vfox_{#MyAppVersion}_windows_setup_aarch64 22 | Compression=zip 23 | SolidCompression=yes 24 | WizardStyle=modern 25 | ChangesEnvironment=true 26 | ArchitecturesAllowed=arm64 27 | ArchitecturesInstallIn64BitMode=arm64 28 | 29 | [Languages] 30 | Name: "english"; MessagesFile: "compiler:Default.isl" 31 | 32 | [Files] 33 | Source: "{#MyAppName}_{#MyAppVersion}_windows_aarch64/vfox.exe"; DestDir: "{app}"; Flags: ignoreversion 34 | 35 | [Code] 36 | procedure CurStepChanged(CurStep: TSetupStep); 37 | begin 38 | if CurStep = ssPostInstall 39 | then EnvAddPath(ExpandConstant('{app}')); 40 | end; 41 | 42 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 43 | begin 44 | if CurUninstallStep = usPostUninstall 45 | then EnvRemovePath(ExpandConstant('{app}')); 46 | end; -------------------------------------------------------------------------------- /docs/usage/all-commands.md: -------------------------------------------------------------------------------- 1 | # All Commands 2 | 3 | ```shell 4 | vfox - vfox is a tool for runtime version management. 5 | vfox available List all available plugins 6 | vfox add [--alias --source ] Add a plugin or plugins from official repository or custom source, --alias` and `--source` are not supported when adding multiple plugins. 7 | vfox remove Remove a plugin 8 | vfox update [ | --all] Update a specified or all plugin(s) 9 | vfox info [@] [options] Show plugin info or SDK path with optional formatting 10 | vfox search Search available versions of a SDK 11 | vfox install @ Install the specified version of SDK 12 | vfox uninstall @ Uninstall the specified version of SDK 13 | vfox use [--global --project --session] [@] Use the specified version of SDK for different scope 14 | vfox unuse [--global --project --session] Unset the version of SDK from specified scope 15 | vfox list [] List all installed versions of SDK 16 | vfox current [] Show the current version of SDK 17 | vfox config [] [] Setup, view config 18 | vfox cd [--plugin] [] Launch a shell in the VFOX_HOME, SDK directory, or plugin directory 19 | vfox upgrade Upgrade vfox to the latest version 20 | vfox help Show this help message 21 | ``` 22 | -------------------------------------------------------------------------------- /internal/plugin/luai/module/module.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package module 18 | 19 | import ( 20 | "github.com/version-fox/vfox/internal/config" 21 | "github.com/version-fox/vfox/internal/plugin/luai/module/archiver" 22 | "github.com/version-fox/vfox/internal/plugin/luai/module/html" 23 | "github.com/version-fox/vfox/internal/plugin/luai/module/http" 24 | "github.com/version-fox/vfox/internal/plugin/luai/module/json" 25 | "github.com/version-fox/vfox/internal/plugin/luai/module/string" 26 | lua "github.com/yuin/gopher-lua" 27 | ) 28 | 29 | type PreloadOptions struct { 30 | Config *config.Config 31 | } 32 | 33 | func Preload(L *lua.LState, options *PreloadOptions) { 34 | http.Preload(L, options.Config.Proxy) 35 | json.Preload(L) 36 | html.Preload(L) 37 | string.Preload(L) 38 | archiver.Preload(L) 39 | } 40 | -------------------------------------------------------------------------------- /internal/util/ci.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | ciTruthyEnvVars = []string{ 10 | "CI", 11 | "CONTINUOUS_INTEGRATION", 12 | } 13 | 14 | ciPresenceEnvVars = []string{ 15 | "GITHUB_ACTIONS", 16 | "GITLAB_CI", 17 | "BUILDKITE", 18 | "TF_BUILD", 19 | "TEAMCITY_VERSION", 20 | "TRAVIS", 21 | "CIRCLECI", 22 | "APPVEYOR", 23 | "BITBUCKET_BUILD_NUMBER", 24 | "JENKINS_URL", 25 | "DRONE", 26 | "HUDSON_URL", 27 | "GO_SERVER_URL", 28 | "CODEBUILD_BUILD_ID", 29 | // https://docs.gitlab.com/ci/variables/predefined_variables/ 30 | "CI_PIPELINE_ID", 31 | } 32 | ) 33 | 34 | // isCI checks if the current environment is CI. 35 | func isCI() bool { 36 | for _, key := range ciTruthyEnvVars { 37 | if isTruthyEnv(os.Getenv(key)) { 38 | return true 39 | } 40 | } 41 | 42 | for _, key := range ciPresenceEnvVars { 43 | if os.Getenv(key) != "" { 44 | return true 45 | } 46 | } 47 | 48 | return false 49 | } 50 | 51 | // IsNonInteractiveTerminal checks if the current environment is non-interactive. 52 | // Returns true if running in CI or if stdout is not a terminal (e.g., piped output). 53 | func IsNonInteractiveTerminal() bool { 54 | if isCI() { 55 | return true 56 | } 57 | return !IsTTY() 58 | } 59 | 60 | func isTruthyEnv(value string) bool { 61 | normalized := strings.TrimSpace(strings.ToLower(value)) 62 | switch normalized { 63 | case "", "0", "false", "no", "off": 64 | return false 65 | } 66 | return true 67 | } 68 | -------------------------------------------------------------------------------- /internal/config/legacy_version_file.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | const ( 20 | LatestInstalledStrategy = "latest_installed" 21 | LatestAvailableStrategy = "latest_available" 22 | SpecifiedStrategy = "specified" 23 | DefaultStrategy = SpecifiedStrategy 24 | ) 25 | 26 | // LegacyVersionFile represents whether to enable the ability to parse legacy version files, 27 | type LegacyVersionFile struct { 28 | Enable bool `yaml:"enable"` 29 | // Support three strategies: 30 | // 1. latest_installed: use the latest installed version 31 | // 2. latest_available: use the latest available version 32 | // 3. specified: use the specified version in the legacy file 33 | // default: specified 34 | Strategy string `yaml:"strategy"` 35 | } 36 | 37 | var EmptyLegacyVersionFile = &LegacyVersionFile{ 38 | Enable: true, 39 | Strategy: DefaultStrategy, 40 | } 41 | -------------------------------------------------------------------------------- /internal/registry.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package internal 18 | 19 | const ( 20 | pluginRegistryAddress = "https://version-fox.github.io/vfox-plugins" 21 | ) 22 | 23 | // RegistryIndex is the index of the registry 24 | type RegistryIndex []*RegistryIndexItem 25 | 26 | // RegistryIndexItem is the item in the registry index 27 | type RegistryIndexItem struct { 28 | Name string `json:"name"` 29 | Desc string `json:"desc"` 30 | Homepage string `json:"homepage"` 31 | } 32 | 33 | // RegistryPluginManifest is the manifest of a remote plugin 34 | type RegistryPluginManifest struct { 35 | Name string `json:"name"` 36 | Version string `json:"version"` 37 | License string `json:"license"` 38 | Author string `json:"author"` 39 | DownloadUrl string `json:"downloadUrl"` 40 | MinRuntimeVersion string `json:"minRuntimeVersion"` 41 | } 42 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { en } from './en' 3 | import { zh } from './zh' 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: "vfox", 8 | head: [ 9 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], 10 | ['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }], 11 | ['meta', { property: 'og:type', content: 'website' }], 12 | ['meta', { property: 'og:title', content: 'vfox | The Multiple SDK Version Manager' }], 13 | ['meta', { property: 'og:site_name', content: 'vfox' }], 14 | [ 15 | 'script', 16 | { type: 'text/javascript' }, 17 | `(function(c,l,a,r,i,t,y){ 18 | c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)}; 19 | t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; 20 | y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); 21 | })(window, document, "clarity", "script", "lh37utcspd");` 22 | ] 23 | ], 24 | locales: { 25 | root: { 26 | label: 'English', 27 | ...en 28 | }, 29 | 'zh-hans': { 30 | label: '简体中文', 31 | ...zh 32 | } 33 | }, 34 | themeConfig: { 35 | // https://vitepress.dev/reference/default-theme-config 36 | search: { 37 | provider: "local", 38 | }, 39 | logo: "/logo.png", 40 | socialLinks: [ 41 | { icon: 'github', link: 'https://github.com/version-fox/vfox' }, 42 | { icon: 'discord', link: 'https://discord.com/invite/85c8ptYgb7' } 43 | ], 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /internal/env/path_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "testing" 7 | ) 8 | 9 | func TestPathFormat(t *testing.T) { 10 | if runtime.GOOS == "windows" { 11 | testdata := []struct { 12 | path string 13 | want string 14 | }{ 15 | { 16 | path: "C:\\Program Files\\Git\\bin", 17 | want: "/c/Program Files/Git/bin", 18 | }, 19 | { 20 | path: "D:\\b\\c", 21 | want: "/d/b/c", 22 | }, 23 | } 24 | 25 | paths := NewPaths(EmptyPaths) 26 | for _, v := range testdata { 27 | paths.Add(v.path) 28 | } 29 | result := paths.Slice() 30 | for i, v := range testdata { 31 | if result[i] != v.path { 32 | t.Errorf("want: %s, got: %s", v.want, result[i]) 33 | } 34 | } 35 | 36 | os.Setenv(HookFlag, "bash") 37 | paths = NewPaths(EmptyPaths) 38 | for _, v := range testdata { 39 | paths.Add(v.path) 40 | } 41 | pathStr := paths.String() 42 | if pathStr != "/c/Program Files/Git/bin:/d/b/c" { 43 | t.Errorf("want: /c/Program Files/Git/bin:/d/b/c, got: %s", pathStr) 44 | } 45 | 46 | } else { 47 | testdata := []struct { 48 | path string 49 | want string 50 | }{ 51 | { 52 | path: "/bin/bash", 53 | want: "/bin/bash", 54 | }, 55 | { 56 | path: "/usr/bin", 57 | want: "/usr/bin", 58 | }, 59 | } 60 | 61 | paths := NewPaths(EmptyPaths) 62 | for _, v := range testdata { 63 | paths.Add(v.path) 64 | } 65 | result := paths.Slice() 66 | for i, v := range testdata { 67 | if result[i] != v.want { 68 | t.Errorf("want: %s, got: %s", v.want, result[i]) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/zh-hans/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | title: vfox 6 | titleTemplate: 跨平台、可拓展的版本管理器 7 | 8 | hero: 9 | name: vfox 10 | text: 跨平台、可拓展的版本管理器 11 | tagline: 😉轻松管理你的工具和运行环境~ 12 | image: 13 | src: /logo.png 14 | alt: vfox 15 | actions: 16 | - theme: brand 17 | text: 👋快速上手 18 | link: /zh-hans/guides/quick-start 19 | - theme: alt 20 | text: 为什么选择vfox? 21 | link: /zh-hans/guides/intro 22 | - theme: alt 23 | text: 查看GitHub 24 | link: https://github.com/version-fox/vfox 25 | 26 | features: 27 | - title: 跨平台 28 | details: "支持Windows(非WSL)、Linux、macOS!" 29 | icon: 💻 30 | - title: 插件 31 | details: "简单的API, 添加新工具的支持变得轻而易举!" 32 | icon: 🔌 33 | - title: "Shells" 34 | details: "支持 Powershell、Bash、ZSH、Fish、Clink和Nushell,并提供补全功能。" 35 | icon: 🐚 36 | - title: 向后兼容 37 | details: "支持从现有配置文件.nvmrc、.node-version、.sdkmanrc平滑迁移!" 38 | icon: ⏮ 39 | - title: "一个配置文件" 40 | details: "一个可共享的 .tool-versions 配置文件管理所有工具、运行环境及其版本。" 41 | icon: 📄 42 | --- 43 | 44 | 45 | 65 | -------------------------------------------------------------------------------- /internal/util/clipboard_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "errors" 21 | "testing" 22 | ) 23 | 24 | func TestCopyToClipboard(t *testing.T) { 25 | testText := "vfox use java@23.0.1+11" 26 | 27 | // CopyToClipboard should not panic and should return nil or an error 28 | err := CopyToClipboard(testText) 29 | 30 | // In CI/CD environment, clipboard utilities might not be available 31 | // So we just verify it doesn't panic and handles errors appropriately 32 | if err != nil { 33 | // Check if it's one of the expected errors 34 | if errors.Is(err, ErrClipboardUtilityNotFound) { 35 | t.Logf("CopyToClipboard returned expected error: clipboard utility not found") 36 | } else if errors.Is(err, ErrClipboardNotSupported) { 37 | t.Logf("CopyToClipboard returned expected error: clipboard not supported") 38 | } else { 39 | t.Logf("CopyToClipboard returned error: %v", err) 40 | } 41 | } else { 42 | t.Log("CopyToClipboard succeeded") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docs/zh-hans/usage/shims-path.md: -------------------------------------------------------------------------------- 1 | # 垫片 & PATH 2 | 3 | `vfox` 是通过直接操作`PATH`来进行版本管理的, 但是有些IDE并不会读取`PATH`环境变量, 4 | 所以我们需要一些额外的操作来让IDE读取到`vfox`的版本。 5 | 6 | ## Shims 目录 7 | 8 | 该目录用于存放所有全局SDK垫片文件 。 9 | 10 | **位置**: `$HOME/.version-fox/shims` 11 | 12 | ```shell 13 | $ vfox use -g nodejs@14.17.0 14 | $ ~/.version-fox/shims/node -v 15 | v14.17.0 16 | ``` 17 | 18 | ::: warning 注意 19 | 20 | `vfox` 只会处理插件指定目录下的所有二进制文件, 如果你通过其他安装工具(`npm`)安装二进制文件, `shims`目录下是不会包含的。 21 | 22 | 以`nodejs`为例: 23 | 24 | ```shell 25 | $ vfox use -g nodejs@14.17.0 26 | $ npm install -g prettier@3.1.0 27 | $ ~/.version-fox/shims/node -v 28 | v14.17.0 29 | $ ~/.version-fox/shims/prettier -v # 文件不存在!!!! 30 | ``` 31 | 32 | > 并不打算提供重建`shim`的能力。 请使用`current`软链。 33 | 34 | ::: 35 | 36 | ::: tip 垫片实现 37 | 38 | - **Windows**: `.exe` 和 `.shim` 文件 39 | - **Unix**: 软链接 40 | 41 | 以`nodejs`为例: 42 | 43 | - **Windows**: `node.exe` 和 `node.shim` 44 | - **Unix**: `.version-fox/shims/node` -> `.version-fox/cache/nodejs/v-14.17.0/nodejs-14.17.0/bin/node` 45 | ::: 46 | 47 | ## `current` 软链接 48 | 49 | `vfox` 除了会将全局SDK垫片放置在`shims`目录下, 还会在`$HOME/.version-fox/cache//`目录下创建一个软链接`current`, 50 | 指向对应的SDK。 51 | 52 | 位置: `$HOME/.version-fox/cache//current` 53 | 54 | 以`Nodejs`为例: 55 | 56 | ```shell 57 | $ vfox use -g nodejs@14.17.0 58 | $ npm install -g prettier@3.1.0 59 | $ ~/.version-fox/cache/nodejs/current/node -v 60 | v14.17.0 61 | $ ~/.version-fox/cache/nodejs/current/prettier -v # 可以了!!! 62 | 3.1.0 63 | ``` 64 | 65 | ::: tip 66 | 67 | `vfox`对于版本管理的核心也是通过`current`软链接来实现的. 68 | 69 | 当你在命令行中进行切换时, 实际上会创建一个软链接指向对应的SDK版本, 并将`current`软链接存放在当前`Shell`的临时目录下, 70 | 以及配置到`PATH`中, 从而实现版本的切换。 71 | 72 | ::: 73 | 74 | -------------------------------------------------------------------------------- /internal/util/ci_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestIsCI(t *testing.T) { 6 | allEnvVars := append([]string{}, ciTruthyEnvVars...) 7 | allEnvVars = append(allEnvVars, ciPresenceEnvVars...) 8 | 9 | testCases := []struct { 10 | name string 11 | env map[string]string 12 | want bool 13 | }{ 14 | { 15 | name: "ci_true", 16 | env: map[string]string{ 17 | "CI": "true", 18 | }, 19 | want: true, 20 | }, 21 | { 22 | name: "ci_false", 23 | env: map[string]string{ 24 | "CI": "false", 25 | }, 26 | want: false, 27 | }, 28 | { 29 | name: "ci_numeric", 30 | env: map[string]string{ 31 | "CI": "1", 32 | }, 33 | want: true, 34 | }, 35 | { 36 | name: "continuous_integration_truthy", 37 | env: map[string]string{ 38 | "CI": "0", 39 | "CONTINUOUS_INTEGRATION": "yes", 40 | }, 41 | want: true, 42 | }, 43 | { 44 | name: "github_actions", 45 | env: map[string]string{ 46 | "CI": "", 47 | "GITHUB_ACTIONS": "true", 48 | }, 49 | want: true, 50 | }, 51 | { 52 | name: "jenkins_url", 53 | env: map[string]string{ 54 | "CI": "0", 55 | "JENKINS_URL": "http://jenkins.example", 56 | }, 57 | want: true, 58 | }, 59 | { 60 | name: "no_indicators", 61 | env: map[string]string{}, 62 | want: false, 63 | }, 64 | } 65 | 66 | for _, tc := range testCases { 67 | t.Run(tc.name, func(t *testing.T) { 68 | for _, key := range allEnvVars { 69 | t.Setenv(key, "") 70 | } 71 | for key, value := range tc.env { 72 | t.Setenv(key, value) 73 | } 74 | 75 | if got := isCI(); got != tc.want { 76 | t.Fatalf("IsCI() = %v, want %v", got, tc.want) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /inno_setup/environment.iss: -------------------------------------------------------------------------------- 1 | [Code] 2 | const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 3 | 4 | procedure EnvAddPath(Path: string); 5 | var 6 | Paths: string; 7 | begin 8 | { Retrieve current path (use empty string if entry not exists) } 9 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 10 | then Paths := ''; 11 | 12 | { Skip if string already found in path } 13 | if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit; 14 | 15 | { Append string to the end of the path variable } 16 | Paths := Paths + ';' + Path; 17 | 18 | { Overwrite (or create if missing) path environment variable } 19 | if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 20 | then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths])) 21 | else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths])); 22 | end; 23 | 24 | procedure EnvRemovePath(Path: string); 25 | var 26 | Paths: string; 27 | P: Integer; 28 | begin 29 | { Skip if registry entry not exists } 30 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then 31 | exit; 32 | 33 | { Skip if string not found in path } 34 | P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); 35 | if P = 0 then exit; 36 | 37 | { Update path variable } 38 | Delete(Paths, P - 1, Length(Path) + 1); 39 | 40 | { Overwrite path environment variable } 41 | if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) 42 | then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths])) 43 | else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths])); 44 | end; -------------------------------------------------------------------------------- /internal/plugin/luai/module/file/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package html 18 | 19 | import ( 20 | lua "github.com/yuin/gopher-lua" 21 | "os" 22 | "path/filepath" 23 | ) 24 | 25 | const luaFileTypeName = "file_operation" 26 | 27 | type FileOperation struct { 28 | rootPath string 29 | } 30 | 31 | func (f *FileOperation) symlink(L *lua.LState) int { 32 | src := L.CheckString(1) 33 | dest := L.CheckString(2) 34 | // TODO check 35 | err := os.Symlink(filepath.Join(f.rootPath, src), filepath.Join(f.rootPath, dest)) 36 | if err != nil { 37 | L.RaiseError(err.Error()) 38 | return 0 39 | } 40 | L.Push(lua.LTrue) 41 | return 1 42 | } 43 | 44 | func (f *FileOperation) luaMap() map[string]lua.LGFunction { 45 | return map[string]lua.LGFunction{ 46 | "symlink": f.symlink, 47 | } 48 | } 49 | 50 | func (f *FileOperation) loader(L *lua.LState) int { 51 | t := L.NewTable() 52 | L.SetFuncs(t, f.luaMap()) 53 | L.Push(t) 54 | return 1 55 | } 56 | 57 | func Preload(L *lua.LState, rootPath string) { 58 | operation := &FileOperation{rootPath: rootPath} 59 | L.PreloadModule("file", operation.loader) 60 | } 61 | -------------------------------------------------------------------------------- /docs/guides/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How do I uninstall vfox? 4 | 5 | Please refer to the [Uninstallation Guide](./uninstallation.md) for detailed instructions on how to completely remove vfox from your system. 6 | 7 | ## Switch xxx not work or the vfox use command does not work ? 8 | 9 | If your shell prompt `Warning: The current shell lacks hook support or configuration. It has switched to global scope 10 | automatically` that means you do not hook `vfox` into your shell, please hook it manually first. 11 | 12 | Please refer to [Quick Start#_2-hook-vfox-to-your-shell](./quick-start.md#_2-hook-vfox-to-your-shell) to manually hook `vfox` into your shell. 13 | 14 | ## Why does the PATH environment variable value repeat on Windows? 15 | 16 | Only one situation will cause this, that is, you have used the SDK globally (`vfox use -g`), at this time, `vfox` will 17 | operate the registry and write the SDK's `PATH` into the user environment variable (for the purpose of, **Shell that does not 18 | support Hook function** can also use SDK, such as `CMD`). 19 | 20 | But because of the existence of the `.tool-versions` mechanism, the `PATH` becomes the sum of `.tool-versions` and the user 21 | environment variable `PATH`. 22 | 23 | ::: warning 24 | The same SDK **can be repeated at most twice**, it will not be repeated indefinitely. If >2 times, please feedback to us. 25 | ::: 26 | 27 | ## Why can't I select when use `use` and `search` commands in GitBash? 28 | 29 | Related ISSUE: [Unable to select in GitBash](https://github.com/version-fox/vfox/issues/98) 30 | 31 | The problem of not being able to select in GitBash only occurs when you directly open the native GitBash to run `vfox`. A temporary solution is: 32 | 1. Run `GitBash` through `Windows Terminal` 33 | 2. Run `GitBash` in `VS Code` 34 | -------------------------------------------------------------------------------- /internal/shim/shim_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | /* 4 | * Copyright 2025 Han Li and contributors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package shim 20 | 21 | import ( 22 | "github.com/version-fox/vfox/internal/logger" 23 | "github.com/version-fox/vfox/internal/util" 24 | "os" 25 | "path/filepath" 26 | ) 27 | 28 | // Clear removes the generated shim. 29 | func (s *Shim) Clear() error { 30 | name := filepath.Base(s.BinaryPath) 31 | targetShim := filepath.Join(s.OutputPath, name) 32 | _, err := os.Readlink(targetShim) 33 | if os.IsNotExist(err) { 34 | return nil 35 | } 36 | return os.Remove(targetShim) 37 | } 38 | 39 | // Generate generates the shim. 40 | func (s *Shim) Generate() error { 41 | if err := s.Clear(); err != nil { 42 | logger.Debugf("Clear shim failed: %s\n", err) 43 | return err 44 | } 45 | name := filepath.Base(s.BinaryPath) 46 | targetShim := filepath.Join(s.OutputPath, name) 47 | logger.Debugf("Create shim from %s to %s\n", s.BinaryPath, targetShim) 48 | if util.FileExists(targetShim) { 49 | _ = os.Remove(targetShim) 50 | } 51 | if err := os.Symlink(s.BinaryPath, targetShim); err != nil { 52 | logger.Debugf("Create symlink failed: %s\n", err) 53 | return err 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/printer/select_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package printer 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func TestSelect_Show(t *testing.T) { 26 | if os.Getenv("CI") != "" { 27 | t.Skip("Skipping TestSelect_Show in CI environment because it requires user input") 28 | } 29 | 30 | source := []*KV{ 31 | { 32 | Key: "1", 33 | Value: "1", 34 | }, 35 | { 36 | Key: "2", 37 | Value: "2", 38 | }, 39 | { 40 | Key: "3", 41 | Value: "3", 42 | }, 43 | { 44 | Key: "4", 45 | Value: "4", 46 | }, 47 | { 48 | Key: "5", 49 | Value: "5", 50 | }, 51 | } 52 | s := &PageKVSelect{ 53 | index: 0, 54 | SourceFunc: func(page, size int, options []*KV) ([]*KV, error) { 55 | // 计算开始和结束索引 56 | start := page * size 57 | end := start + size 58 | 59 | // 检查索引是否超出范围 60 | if start > len(source) { 61 | return nil, fmt.Errorf("page is out of range") 62 | } 63 | if end > len(source) { 64 | end = len(source) 65 | } 66 | 67 | // 返回分页后的元素 68 | return source[start:end], nil 69 | }, 70 | Size: 3, 71 | } 72 | show, err := s.Show() 73 | print(show) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/util/checksum.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "crypto/md5" 21 | "crypto/sha1" 22 | "crypto/sha256" 23 | "crypto/sha512" 24 | "encoding/hex" 25 | "os" 26 | 27 | "github.com/pterm/pterm" 28 | ) 29 | 30 | type Checksum struct { 31 | Value string 32 | Type string 33 | } 34 | 35 | func (c *Checksum) Verify(path string) bool { 36 | fileData, err := os.ReadFile(path) 37 | if err != nil { 38 | return false 39 | } 40 | var hash []byte 41 | if c.Type == "sha256" { 42 | hashValue := sha256.Sum256(fileData) 43 | hash = hashValue[:] 44 | } else if c.Type == "sha512" { 45 | hashValue := sha512.Sum512(fileData) 46 | hash = hashValue[:] 47 | } else if c.Type == "sha1" { 48 | hashValue := sha1.Sum(fileData) 49 | hash = hashValue[:] 50 | } else if c.Type == "md5" { 51 | hashValue := md5.Sum(fileData) 52 | hash = hashValue[:] 53 | } else if c.Type == "none" { 54 | pterm.Printf("%s: Checksum is not provided, skip verify...\n", pterm.LightYellow("WARNING")) 55 | return true 56 | } else { 57 | return false 58 | } 59 | checksum := hex.EncodeToString(hash) 60 | return checksum == c.Value 61 | } 62 | 63 | var NoneChecksum = &Checksum{ 64 | Value: "", 65 | Type: "none", 66 | } 67 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | title: vfox 6 | titleTemplate: The Multiple SDK Version Manager 7 | 8 | hero: 9 | name: vfox 10 | text: The Multiple SDK Version Manager 11 | tagline: 😉Easily manage all your SDK versions~ 12 | image: 13 | src: /logo.png 14 | alt: vfox 15 | actions: 16 | - theme: brand 17 | text: 👋Get Started 18 | link: /guides/quick-start 19 | - theme: alt 20 | text: Why use vfox? 21 | link: /guides/intro 22 | - theme: alt 23 | text: View on GitHub 24 | link: https://github.com/version-fox/vfox 25 | 26 | features: 27 | - title: Cross-platform 28 | details: "Supports Windows (non-WSL), Linux, macOS!" 29 | icon: 💻 30 | - title: Plugins 31 | details: "Simple API, making it easy to add support for new tools!" 32 | icon: 🔌 33 | - title: "Shells" 34 | details: "Supports Powershell, Bash, ZSH, Fish, Clink, and Nushell, with autocomplete feature." 35 | icon: 🐚 36 | - title: Backwards Compatible 37 | details: "Support for existing version files .nvmrc, .node-version, .sdkmanrc for smooth migration!" 38 | icon: ⏮ 39 | - title: "One Config File" 40 | details: ".tool-versions manages all tools, runtime environments and their versions." 41 | icon: 📄 42 | --- 43 | 44 | 45 | 65 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package logger 18 | 19 | import "fmt" 20 | 21 | type LoggerLevel int 22 | 23 | // the smaller the level, the more logs. 24 | const ( 25 | DebugLevel LoggerLevel = iota 26 | InfoLevel 27 | ErrorLevel 28 | ) 29 | 30 | var currentLevel = InfoLevel 31 | 32 | func SetLevel(_level LoggerLevel) { 33 | currentLevel = _level 34 | } 35 | 36 | func Log(level LoggerLevel, args ...interface{}) { 37 | if currentLevel <= level { 38 | fmt.Println(args...) 39 | } 40 | } 41 | 42 | func Logf(level LoggerLevel, message string, args ...interface{}) { 43 | if currentLevel <= level { 44 | fmt.Printf(message, args...) 45 | } 46 | } 47 | 48 | func Error(message ...interface{}) { 49 | Log(ErrorLevel, message...) 50 | } 51 | 52 | func Errorf(message string, args ...interface{}) { 53 | Logf(ErrorLevel, message, args...) 54 | } 55 | 56 | func Info(message ...interface{}) { 57 | Log(InfoLevel, message...) 58 | } 59 | 60 | func Infof(message string, args ...interface{}) { 61 | Logf(InfoLevel, message, args...) 62 | } 63 | 64 | func Debug(args ...interface{}) { 65 | Log(DebugLevel, args...) 66 | } 67 | 68 | func Debugf(message string, args ...interface{}) { 69 | Logf(DebugLevel, message, args...) 70 | } 71 | -------------------------------------------------------------------------------- /internal/util/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | type VersionSort []string 25 | 26 | func (s VersionSort) Len() int { 27 | return len(s) 28 | } 29 | 30 | func (s VersionSort) Swap(i, j int) { 31 | s[i], s[j] = s[j], s[i] 32 | } 33 | 34 | func (s VersionSort) Less(i, j int) bool { 35 | return CompareVersion(s[i], s[j]) > 0 36 | } 37 | 38 | func CompareVersion(v1, v2 string) int { 39 | parts1 := strings.Split(v1, ".") 40 | parts2 := strings.Split(v2, ".") 41 | 42 | len1 := len(parts1) 43 | len2 := len(parts2) 44 | 45 | // Get the maximum length between two versions 46 | maxLen := len1 47 | if len2 > maxLen { 48 | maxLen = len2 49 | } 50 | 51 | for i := 0; i < maxLen; i++ { 52 | // Because the length of v1 or v2 may be less than maxLen 53 | // We assume the missing part as 0 54 | part1 := 0 55 | if i < len1 { 56 | part1, _ = strconv.Atoi(parts1[i]) 57 | } 58 | 59 | part2 := 0 60 | if i < len2 { 61 | part2, _ = strconv.Atoi(parts2[i]) 62 | } 63 | 64 | if part1 != part2 { 65 | if part1 > part2 { 66 | return 1 67 | } else { 68 | return -1 69 | } 70 | } 71 | } 72 | 73 | // If all parts are equal 74 | return 0 75 | } 76 | -------------------------------------------------------------------------------- /docs/misc/vs-asdf.md: -------------------------------------------------------------------------------- 1 | # Comparison with asdf-vm 2 | 3 | `vfox` and `asdf-vm` have the same goal, that is, a tool to manage all runtime versions, and all versions are recorded 4 | in the `.tool-versions` file. 5 | 6 | But `vfox` has the following advantages: 7 | 8 | ## Platform compatibility 9 | 10 | | Tool | Windows (non-WSL) | Linux | macOS | 11 | |---------|-------------------|-------|-------| 12 | | asdf-vm | ❌ | ✅ | ✅ | 13 | | vfox | ✅ | ✅ | ✅ | 14 | 15 | `asdf-vm` is a `Shell`-based tool, so it does not support **native Windows** environments! 16 | 17 | `vfox` is implemented in `Golang` + `Lua`, so it naturally supports `Windows` and other operating systems. 18 | 19 | ## Performance comparison 20 | 21 | ![performance.png](/performance.png) 22 | 23 | The above figure is a benchmark test of the two tools' most core functions. It will be found that `vfox` 24 | is about **5 times** faster than `asdf-vm`! 25 | 26 | The reason why `asdf-vm` is slower is mainly due to its `shim` mechanism. Simply put, when you try to run a command 27 | like `node`, `asdf-vm` will first look for the corresponding `shim`, and then determine which version of `node` to use 28 | based on the `.tool-versions` file or global configuration. This process of finding and determining the version will 29 | consume 30 | a certain amount of time, thereby affecting the speed of command execution. 31 | 32 | In contrast, `vfox` uses the direct operation of environment variables to manage versions, and it will directly set and 33 | switch 34 | environment variables, thereby avoiding the process of finding and determining the version. Therefore, `vfox` is much 35 | faster 36 | than `asdf-vm` using the `shim` mechanism. 37 | 38 | `asdf-vm` ecosystem is very strong, but it is powerless for **Windows native**. Although `vfox` is very new, 39 | it does better in terms of performance and platform compatibility than `asdf-vm`. 40 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | # trigger deployment on push to master branch when changes to docs/** 5 | push: 6 | paths: 7 | - "docs/**" 8 | branches: 9 | - main 10 | # trigger deployment manually 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: write 15 | pages: write 16 | id-token: write 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | working-directory: docs/ 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | with: 28 | # fetch all commits to get last updated time or other git log info 29 | fetch-depth: 0 30 | 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v6 33 | with: 34 | node-version: "20" 35 | 36 | - name: Cache dependencies 37 | uses: actions/cache@v5 38 | id: npm-cache 39 | with: 40 | path: | 41 | **/node_modules 42 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} 43 | restore-keys: | 44 | ${{ runner.os }}-npm- 45 | 46 | - name: Install dependencies 47 | if: steps.npm-cache.outputs.cache-hit != 'true' 48 | run: npm install 49 | 50 | - name: Build VitePress site 51 | run: npm run docs:build 52 | 53 | - name: Bundle CNAME with site dist 54 | run: cp CNAME .vitepress/dist 55 | 56 | 57 | - name: Upload artifact 58 | uses: actions/upload-pages-artifact@v4 59 | with: 60 | path: docs/.vitepress/dist 61 | # Deployment job 62 | deploy: 63 | environment: 64 | name: github-pages 65 | url: ${{ steps.deployment.outputs.page_url }} 66 | needs: build 67 | runs-on: ubuntu-latest 68 | name: Deploy 69 | steps: 70 | - name: Deploy to GitHub Pages 71 | id: deployment 72 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /cmd/commands/upgrade_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | * Copyright 2025 Han Li and contributors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package commands 20 | 21 | import ( 22 | "golang.org/x/sys/windows" 23 | "os" 24 | "syscall" 25 | "unsafe" 26 | ) 27 | 28 | func RequestPermission() error { 29 | isAdmin, err := isAdmin() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | if !isAdmin { 35 | if err = runAsAdmin(); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func isAdmin() (bool, error) { 43 | _, err := os.Open("\\\\.\\PHYSICALDRIVE0") 44 | if err != nil { 45 | if os.IsPermission(err) { 46 | return false, nil 47 | } 48 | return false, err 49 | } 50 | 51 | return true, nil 52 | } 53 | 54 | func runAsAdmin() error { 55 | exePath, err := os.Executable() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | verb := "runas" 61 | cwd, _ := syscall.UTF16PtrFromString(".") 62 | arg, _ := syscall.UTF16PtrFromString(SelfUpgradeName) 63 | run := windows.NewLazySystemDLL("shell32.dll").NewProc("ShellExecuteW") 64 | run.Call( 65 | 0, 66 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(verb))), 67 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(exePath))), 68 | uintptr(unsafe.Pointer(arg)), 69 | uintptr(unsafe.Pointer(cwd)), 70 | 1, 71 | ) 72 | os.Exit(0) 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/util/error_store.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import "fmt" 20 | 21 | type errorItem struct { 22 | Note string 23 | Err error 24 | } 25 | 26 | // ErrorStore is a struct that stores errors 27 | type ErrorStore struct { 28 | errors []errorItem 29 | } 30 | 31 | // NewErrorStore creates a new ErrorStore 32 | func NewErrorStore() *ErrorStore { 33 | return &ErrorStore{ 34 | errors: make([]errorItem, 0), 35 | } 36 | } 37 | 38 | // Add adds an error to the store 39 | func (e *ErrorStore) Add(note string, err error) { 40 | e.errors = append(e.errors, errorItem{Note: note, Err: err}) 41 | } 42 | 43 | // Add and show in the console 44 | func (e *ErrorStore) AddAndShow(note string, err error) { 45 | e.Add(note, err) 46 | fmt.Println(err) 47 | } 48 | 49 | // get all error notes 50 | func (e *ErrorStore) GetNotes() []string { 51 | notes := make([]string, 0, len(e.errors)) 52 | 53 | for _, item := range e.errors { 54 | notes = append(notes, item.Note) 55 | } 56 | 57 | return notes 58 | } 59 | 60 | // get notes set 61 | func (e *ErrorStore) GetNotesSet() Set[string] { 62 | set := NewSet[string]() 63 | for _, item := range e.errors { 64 | set.Add(item.Note) 65 | } 66 | return set 67 | } 68 | 69 | func (e *ErrorStore) HasError() bool { 70 | return len(e.errors) > 0 71 | } 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vfox 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with Github 12 | 13 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 16 | 17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 18 | 19 | 1. Fork the repo and create your branch from `master`. 20 | 2. If you've added code that should be tested, add tests. 21 | 3. Ensure the test suite passes. 22 | 5. Make sure your coding style remains consistent 23 | 6. Issue that pull request! 24 | 25 | ## Any contributions you make will be under the Apache 2.0 Software License 26 | 27 | In short, when you submit code changes, your submissions are understood to be under the same [Apache 2.0 License](./LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. 28 | 29 | ## Report bugs using Github's [issues](https://github.com/briandk/transcriptase-atom/issues) 30 | 31 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/briandk/transcriptase-atom/issues); it's that easy! 32 | 33 | 34 | ## Use a Consistent Coding Style 35 | 36 | * We follow the coding style suggested by the Go community. That's [Effective Go](https://golang.org/doc/effective_go.html) and the [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). 37 | * You can try running `go fmt` and `go vet` for style unification 38 | 39 | ## License 40 | 41 | By contributing, you agree that your contributions will be licensed under its Apache 2.0 License. 42 | -------------------------------------------------------------------------------- /cmd/commands/available.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "strings" 22 | 23 | "github.com/pterm/pterm" 24 | "github.com/urfave/cli/v3" 25 | "github.com/version-fox/vfox/internal" 26 | ) 27 | 28 | var Available = &cli.Command{ 29 | Name: "available", 30 | Usage: "Show all available plugins", 31 | Action: availableCmd, 32 | Category: CategoryPlugin, 33 | } 34 | 35 | func availableCmd(ctx context.Context, cmd *cli.Command) error { 36 | manager := internal.NewSdkManager() 37 | defer manager.Close() 38 | //categoryName := cmd.Args().First() 39 | available, err := manager.Available() 40 | if err != nil { 41 | return err 42 | } 43 | data := pterm.TableData{ 44 | {"NAME", "OFFICIAL", "HOMEPAGE", "DESCRIPTION"}, 45 | } 46 | for _, item := range available { 47 | official := pterm.LightRed("NO") 48 | if strings.HasPrefix(item.Homepage, "https://github.com/version-fox/") { 49 | official = pterm.LightGreen("YES") 50 | } 51 | data = append(data, []string{item.Name, official, item.Homepage, item.Desc}) 52 | } 53 | 54 | _ = pterm.DefaultTable. 55 | WithHasHeader(). 56 | WithSeparator("\t "). 57 | WithData(data).Render() 58 | pterm.Printf("Please use %s to install plugins\n", pterm.LightBlue("vfox add ")) 59 | return nil 60 | 61 | } 62 | -------------------------------------------------------------------------------- /internal/util/version_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import "testing" 20 | 21 | func TestCompareVersion(t *testing.T) { 22 | type args struct { 23 | v1 string 24 | v2 string 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | want int 30 | }{ 31 | { 32 | name: "v1 > v2", 33 | args: args{ 34 | v1: "1.0.0", 35 | v2: "0.0.1", 36 | }, 37 | want: 1, 38 | }, 39 | { 40 | name: "v1 > v2", 41 | args: args{ 42 | v1: "0.2.3", 43 | v2: "0.2.1", 44 | }, 45 | want: 1, 46 | }, 47 | { 48 | name: "v1 > v2", 49 | args: args{ 50 | v1: "3.2.0", 51 | v2: "1.2.1", 52 | }, 53 | want: 1, 54 | }, 55 | { 56 | name: "v1 < v2", 57 | args: args{ 58 | v1: "0.0.1", 59 | v2: "1.0.0", 60 | }, 61 | want: -1, 62 | }, 63 | { 64 | name: "v1 < v2", 65 | args: args{ 66 | v1: "0.1.1", 67 | v2: "0.2.0", 68 | }, 69 | want: -1, 70 | }, 71 | { 72 | name: "v1 = v2", 73 | args: args{ 74 | v1: "1.0.0", 75 | v2: "1.0.0", 76 | }, 77 | want: 0, 78 | }, 79 | } 80 | for _, test := range tests { 81 | t.Run(test.name, func(t *testing.T) { 82 | if got := CompareVersion(test.args.v1, test.args.v2); got != test.want { 83 | t.Errorf("CompareVersion() = %v, want %v", got, test.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/util/downloader.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "errors" 21 | "github.com/schollz/progressbar/v3" 22 | "io" 23 | "net/http" 24 | "net/url" 25 | "os" 26 | "path/filepath" 27 | ) 28 | 29 | type Downloader struct { 30 | // URL is the URL to download the SDK from. 31 | LocalPath string `json:"local_path"` 32 | } 33 | 34 | func (d *Downloader) Download(url *url.URL) (string, error) { 35 | req, err := http.NewRequest("GET", url.String(), nil) 36 | if err != nil { 37 | return "", err 38 | } 39 | resp, err := http.DefaultClient.Do(req) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | defer resp.Body.Close() 45 | 46 | if resp.StatusCode == http.StatusNotFound { 47 | return "", errors.New("source file not found") 48 | } 49 | 50 | path := filepath.Join(d.LocalPath, filepath.Base(url.Path)) 51 | 52 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | defer f.Close() 58 | 59 | bar := progressbar.DefaultBytes( 60 | resp.ContentLength, 61 | "Downloading", 62 | ) 63 | _, err = io.Copy(io.MultiWriter(f, bar), resp.Body) 64 | if err != nil { 65 | return "", err 66 | } 67 | return path, nil 68 | } 69 | 70 | func NewDownloader(localPath string) *Downloader { 71 | return &Downloader{ 72 | LocalPath: localPath, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "96004987", 3 | "configHash": "b25d5b92", 4 | "lockfileHash": "16b0c1ce", 5 | "browserHash": "204f4453", 6 | "optimized": { 7 | "vue": { 8 | "src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", 9 | "file": "vue.js", 10 | "fileHash": "68d2c152", 11 | "needsInterop": false 12 | }, 13 | "vitepress > @vue/devtools-api": { 14 | "src": "../../../node_modules/@vue/devtools-api/dist/index.js", 15 | "file": "vitepress___@vue_devtools-api.js", 16 | "fileHash": "0e500d10", 17 | "needsInterop": false 18 | }, 19 | "vitepress > @vueuse/core": { 20 | "src": "../../../node_modules/@vueuse/core/index.mjs", 21 | "file": "vitepress___@vueuse_core.js", 22 | "fileHash": "340cba74", 23 | "needsInterop": false 24 | }, 25 | "vitepress > @vueuse/integrations/useFocusTrap": { 26 | "src": "../../../node_modules/@vueuse/integrations/useFocusTrap.mjs", 27 | "file": "vitepress___@vueuse_integrations_useFocusTrap.js", 28 | "fileHash": "ed87f9b6", 29 | "needsInterop": false 30 | }, 31 | "vitepress > mark.js/src/vanilla.js": { 32 | "src": "../../../node_modules/mark.js/src/vanilla.js", 33 | "file": "vitepress___mark__js_src_vanilla__js.js", 34 | "fileHash": "664db382", 35 | "needsInterop": false 36 | }, 37 | "vitepress > minisearch": { 38 | "src": "../../../node_modules/minisearch/dist/es/index.js", 39 | "file": "vitepress___minisearch.js", 40 | "fileHash": "c3ca8272", 41 | "needsInterop": false 42 | }, 43 | "@theme/index": { 44 | "src": "../../../node_modules/vitepress/dist/client/theme-default/index.js", 45 | "file": "@theme_index.js", 46 | "fileHash": "e89a1f77", 47 | "needsInterop": false 48 | } 49 | }, 50 | "chunks": { 51 | "chunk-TM5ZW4YT": { 52 | "file": "chunk-TM5ZW4YT.js" 53 | }, 54 | "chunk-Z6B2QTD3": { 55 | "file": "chunk-Z6B2QTD3.js" 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/version-fox/vfox 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | atomicgo.dev/cursor v0.2.0 7 | atomicgo.dev/keyboard v0.2.9 8 | github.com/PuerkitoBio/goquery v1.9.3 9 | github.com/bodgit/sevenzip v1.5.1 10 | github.com/lithammer/fuzzysearch v1.1.8 11 | github.com/pterm/pterm v0.12.79 12 | github.com/schollz/progressbar/v3 v3.14.2 13 | github.com/shirou/gopsutil/v4 v4.25.3 14 | github.com/ulikunitz/xz v0.5.12 15 | github.com/urfave/cli/v3 v3.4.1 16 | github.com/yuin/gopher-lua v1.1.1 17 | golang.org/x/crypto v0.35.0 18 | golang.org/x/sys v0.30.0 19 | golang.org/x/term v0.29.0 20 | gopkg.in/yaml.v3 v3.0.1 21 | ) 22 | 23 | require ( 24 | atomicgo.dev/schedule v0.1.0 // indirect 25 | github.com/andybalholm/brotli v1.1.0 // indirect 26 | github.com/andybalholm/cascadia v1.3.2 // indirect 27 | github.com/bodgit/plumbing v1.3.0 // indirect 28 | github.com/bodgit/windows v1.0.1 // indirect 29 | github.com/containerd/console v1.0.4 // indirect 30 | github.com/ebitengine/purego v0.8.3 // indirect 31 | github.com/go-ole/go-ole v1.2.6 // indirect 32 | github.com/gookit/color v1.5.4 // indirect 33 | github.com/hashicorp/errwrap v1.0.0 // indirect 34 | github.com/hashicorp/go-multierror v1.1.1 // indirect 35 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 36 | github.com/klauspost/compress v1.17.7 // indirect 37 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 38 | github.com/mattn/go-runewidth v0.0.15 // indirect 39 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 40 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 41 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 42 | github.com/rivo/uniseg v0.4.7 // indirect 43 | github.com/tklauser/go-sysconf v0.3.12 // indirect 44 | github.com/tklauser/numcpus v0.6.1 // indirect 45 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 46 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 47 | go4.org v0.0.0-20200411211856-f5505b9728dd // indirect 48 | golang.org/x/net v0.29.0 // indirect 49 | golang.org/x/text v0.22.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /cmd/commands/current.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/pterm/pterm" 24 | "github.com/urfave/cli/v3" 25 | "github.com/version-fox/vfox/internal" 26 | ) 27 | 28 | var Current = &cli.Command{ 29 | Name: "current", 30 | Aliases: []string{"c"}, 31 | Usage: "Show current version of the target SDK", 32 | UsageText: "Show current version of all SDK's if no parameters are passed", 33 | Action: currentCmd, 34 | Category: CategorySDK, 35 | } 36 | 37 | func currentCmd(ctx context.Context, cmd *cli.Command) error { 38 | manager := internal.NewSdkManager() 39 | defer manager.Close() 40 | sdkName := cmd.Args().First() 41 | if sdkName == "" { 42 | allSdk, err := manager.LoadAllSdk() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | for _, s := range allSdk { 48 | name := s.Name 49 | current := s.Current() 50 | if current == "" { 51 | pterm.Printf("%s -> N/A \n", name) 52 | } else { 53 | pterm.Printf("%s -> %s\n", name, pterm.LightGreen("v"+string(current))) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | source, err := manager.LookupSdk(sdkName) 60 | if err != nil { 61 | return fmt.Errorf("%s not supported, error: %w", sdkName, err) 62 | } 63 | current := source.Current() 64 | if current == "" { 65 | return fmt.Errorf("no current version of %s", sdkName) 66 | } 67 | pterm.Println("->", pterm.LightGreen("v"+string(current))) 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/plugin/luai/context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package luai 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | // computeUserAgent constructs a user agent string for the vfox runtime and plugin. 25 | // 26 | // Parameters: 27 | // - runtimeVersion: the version of the vfox runtime (may be empty). 28 | // - pluginName: the name of the plugin (will be prefixed with "vfox-" if not already). 29 | // - pluginVersion: the version of the plugin (may be empty). 30 | // 31 | // Returns: 32 | // A user agent string in the format "vfox/ vfox-/", 33 | // omitting version information if not provided, and trimming extra spaces. 34 | func computeUserAgent(runtimeVersion, pluginName, pluginVersion string) string { 35 | components := make([]string, 0, 2) 36 | if runtimeVersion != "" { 37 | components = append(components, fmt.Sprintf("vfox/%s", runtimeVersion)) 38 | } else { 39 | components = append(components, "vfox") 40 | } 41 | 42 | name := ensurePrefix(pluginName) 43 | if name != "" { 44 | if pluginVersion != "" { 45 | components = append(components, fmt.Sprintf("%s/%s", name, pluginVersion)) 46 | } else { 47 | components = append(components, name) 48 | } 49 | } 50 | 51 | return strings.TrimSpace(strings.Join(components, " ")) 52 | } 53 | 54 | func ensurePrefix(name string) string { 55 | if name == "" { 56 | return "" 57 | } 58 | if strings.HasPrefix(name, "vfox-") { 59 | return name 60 | } 61 | return "vfox-" + name 62 | } 63 | -------------------------------------------------------------------------------- /cmd/commands/update.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/pterm/pterm" 24 | "github.com/urfave/cli/v3" 25 | "github.com/version-fox/vfox/internal" 26 | ) 27 | 28 | const allFlag = "all" 29 | 30 | var Update = &cli.Command{ 31 | Name: "update", 32 | Usage: "Update specified plugin, use --all/-a to update all installed plugins", 33 | Flags: []cli.Flag{ 34 | &cli.BoolFlag{ 35 | Name: allFlag, 36 | Aliases: []string{"a"}, 37 | Usage: "all plugins flag", 38 | }, 39 | }, 40 | Action: updateCmd, 41 | Category: CategoryPlugin, 42 | } 43 | 44 | func updateCmd(ctx context.Context, cmd *cli.Command) error { 45 | manager := internal.NewSdkManager() 46 | defer manager.Close() 47 | if cmd.Bool(allFlag) { 48 | if sdks, err := manager.LoadAllSdk(); err == nil { 49 | var ( 50 | index int 51 | total = len(sdks) 52 | ) 53 | for _, s := range sdks { 54 | sdkName := s.Name 55 | index++ 56 | pterm.Printf("[%s/%d]: Updating %s plugin...\n", pterm.Green(index), total, pterm.Green(sdkName)) 57 | if err = manager.Update(sdkName); err != nil { 58 | pterm.Println(fmt.Sprintf("Update plugin(%s) failed, %s", sdkName, err.Error())) 59 | } 60 | } 61 | } else { 62 | return cli.Exit(err.Error(), 1) 63 | } 64 | } else { 65 | args := cmd.Args() 66 | l := args.Len() 67 | if l < 1 { 68 | return cli.Exit("invalid arguments", 1) 69 | } 70 | 71 | return manager.Update(args.First()) 72 | } 73 | return nil 74 | } 75 | -------------------------------------------------------------------------------- /cmd/commands/remove.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/pterm/pterm" 23 | "github.com/urfave/cli/v3" 24 | "github.com/version-fox/vfox/internal" 25 | "github.com/version-fox/vfox/internal/util" 26 | ) 27 | 28 | var Remove = &cli.Command{ 29 | Name: "remove", 30 | Usage: "Remove a plugin", 31 | Flags: []cli.Flag{ 32 | &cli.BoolFlag{ 33 | Name: "yes", 34 | Aliases: []string{"y"}, 35 | Usage: "Skip confirmation prompt", 36 | }, 37 | }, 38 | Action: removeCmd, 39 | Category: CategoryPlugin, 40 | } 41 | 42 | func removeCmd(ctx context.Context, cmd *cli.Command) error { 43 | args := cmd.Args() 44 | l := args.Len() 45 | if l < 1 { 46 | return cli.Exit("invalid arguments", 1) 47 | } 48 | yes := cmd.Bool("yes") 49 | 50 | manager := internal.NewSdkManager() 51 | defer manager.Close() 52 | pterm.Println("Removing this plugin will remove the installed sdk along with the plugin.") 53 | 54 | if !yes { 55 | if util.IsNonInteractiveTerminal() { 56 | return cli.Exit("Use the -y flag to skip confirmation in non-interactive environments", 1) 57 | } 58 | result, _ := pterm.DefaultInteractiveConfirm. 59 | WithTextStyle(&pterm.ThemeDefault.DefaultText). 60 | WithConfirmStyle(&pterm.ThemeDefault.DefaultText). 61 | WithRejectStyle(&pterm.ThemeDefault.DefaultText). 62 | WithDefaultText("Please confirm"). 63 | Show() 64 | if !result { 65 | return cli.Exit("remove canceled", 1) 66 | } 67 | } 68 | 69 | return manager.Remove(args.First()) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/commands/unuse.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/urfave/cli/v3" 24 | "github.com/version-fox/vfox/internal" 25 | "github.com/version-fox/vfox/internal/base" 26 | ) 27 | 28 | var Unuse = &cli.Command{ 29 | Name: "unuse", 30 | Usage: "Unset a version of the target SDK", 31 | Flags: []cli.Flag{ 32 | &cli.BoolFlag{ 33 | Name: "global", 34 | Aliases: []string{"g"}, 35 | Usage: "Unset from the global environment", 36 | }, 37 | &cli.BoolFlag{ 38 | Name: "project", 39 | Aliases: []string{"p"}, 40 | Usage: "Unset from the current directory", 41 | }, 42 | &cli.BoolFlag{ 43 | Name: "session", 44 | Aliases: []string{"s"}, 45 | Usage: "Unset from the current shell session", 46 | }, 47 | }, 48 | Action: unuseCmd, 49 | Category: CategorySDK, 50 | } 51 | 52 | func unuseCmd(ctx context.Context, cmd *cli.Command) error { 53 | sdkName := cmd.Args().First() 54 | if len(sdkName) == 0 { 55 | return fmt.Errorf("invalid parameter. format: ") 56 | } 57 | 58 | scope := base.Session 59 | if cmd.IsSet("global") { 60 | scope = base.Global 61 | } else if cmd.IsSet("project") { 62 | scope = base.Project 63 | } else { 64 | scope = base.Session 65 | } 66 | 67 | manager := internal.NewSdkManager() 68 | defer manager.Close() 69 | 70 | source, err := manager.LookupSdk(sdkName) 71 | if err != nil { 72 | return fmt.Errorf("%s not supported, error: %w", sdkName, err) 73 | } 74 | 75 | return source.Unuse(scope) 76 | } -------------------------------------------------------------------------------- /internal/shell/zsh.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package shell 18 | 19 | import "github.com/version-fox/vfox/internal/env" 20 | 21 | // Based on https://github.com/direnv/direnv/blob/master/internal/cmd/shell_zsh.go 22 | 23 | type zsh struct{} 24 | 25 | var Zsh = zsh{} 26 | 27 | const zshHook = ` 28 | if [[ -z "$__VFOX_PID" || -z "$__VFOX_SHELL" ]]; then 29 | {{.EnvContent}} 30 | 31 | export __VFOX_PID=$$; 32 | 33 | _vfox_hook() { 34 | trap -- '' SIGINT; 35 | eval "$("{{.SelfPath}}" env -s zsh)"; 36 | trap - SIGINT; 37 | } 38 | typeset -ag precmd_functions; 39 | if [[ -z "${precmd_functions[(r)_vfox_hook]+1}" ]]; then 40 | precmd_functions=( _vfox_hook ${precmd_functions[@]} ) 41 | fi 42 | typeset -ag chpwd_functions; 43 | if [[ -z "${chpwd_functions[(r)_vfox_hook]+1}" ]]; then 44 | chpwd_functions=( _vfox_hook ${chpwd_functions[@]} ) 45 | fi 46 | 47 | trap 'vfox env --cleanup' EXIT 48 | fi 49 | ` 50 | 51 | func (z zsh) Activate(config ActivateConfig) (string, error) { 52 | return zshHook, nil 53 | } 54 | 55 | func (z zsh) Export(envs env.Vars) (out string) { 56 | for key, value := range envs { 57 | if value == nil { 58 | out += z.unset(key) 59 | } else { 60 | out += z.export(key, *value) 61 | } 62 | } 63 | return out 64 | } 65 | 66 | func (z zsh) export(key, value string) string { 67 | return "export " + z.escape(key) + "=" + z.escape(value) + ";" 68 | } 69 | 70 | func (z zsh) unset(key string) string { 71 | return "unset " + z.escape(key) + ";" 72 | } 73 | 74 | func (z zsh) escape(str string) string { 75 | return BashEscape(str) 76 | } 77 | -------------------------------------------------------------------------------- /cmd/commands/add.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/pterm/pterm" 24 | "github.com/urfave/cli/v3" 25 | "github.com/version-fox/vfox/internal" 26 | ) 27 | 28 | var Add = &cli.Command{ 29 | Name: "add", 30 | Usage: "Add a plugin or plugins", 31 | Flags: []cli.Flag{ 32 | &cli.StringFlag{ 33 | Name: "source", 34 | Aliases: []string{"s"}, 35 | Usage: "plugin source", 36 | }, 37 | &cli.StringFlag{ 38 | Name: "alias", 39 | Usage: "plugin alias", 40 | }, 41 | }, 42 | Action: addCmd, 43 | Category: CategoryPlugin, 44 | } 45 | 46 | // addCmd is the command to add a plugin of sdk 47 | func addCmd(ctx context.Context, cmd *cli.Command) error { 48 | args := cmd.Args() 49 | 50 | manager := internal.NewSdkManager() 51 | defer manager.Close() 52 | 53 | // multiple plugins 54 | if args.Len() > 1 { 55 | for index, sdkName := range args.Slice() { 56 | pterm.Printf("[%s/%d]: Adding %s plugin...\n", pterm.Green(index+1), args.Len(), pterm.Green(sdkName)) 57 | if err := manager.Add(sdkName, "", ""); err != nil { 58 | pterm.Println(fmt.Sprintf("Add plugin(%s) failed: %s", pterm.Red(sdkName), err.Error())) 59 | } 60 | } 61 | return nil 62 | } else { 63 | sdkName := args.First() 64 | source := cmd.String("source") 65 | alias := cmd.String("alias") 66 | err := manager.Add(sdkName, source, alias) 67 | if err == nil { 68 | pterm.Printf("Please use `%s` to install the version you need.\n", pterm.LightBlue(fmt.Sprintf("vfox install %s@", sdkName))) 69 | } 70 | return err 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /docs/zh-hans/usage/plugins-commands.md: -------------------------------------------------------------------------------- 1 | # 插件相关命令 2 | 3 | 插件是`vfox`知道如何处理`Node.js`、`Java`、`Elixir`等不同工具的方式。 4 | 5 | 请参阅[创建插件](../plugins/create/howto.md)了解用于的支持更多工具的插件API。 6 | 7 | ## Available 8 | 9 | 列举[索引仓库](https://github.com/version-fox/vfox-plugins)中所有可用的插件。 10 | 11 | **用法** 12 | ```shell 13 | vfox available 14 | ``` 15 | 16 | ## Add 17 | 18 | 添加插件,支持安装[仓库插件](../plugins/available.md)和自定义插件。 19 | 20 | **用法** 21 | 22 | ```shell 23 | vfox add [options] [...] 24 | ``` 25 | `plugin-name`: 插件名称, 如`nodejs`。可以同时安装多个插件, 用空格分隔。 26 | 27 | **选项** 28 | - `-a, --alias`: 设置插件别名。 29 | - `-s, --source`: 安装指定路径下的插件(可以是远程文件也可以是本地文件) 30 | 31 | ::: warning 注意 32 | `--alias` 和 `--source` 不支持在安装多个插件时使用。 33 | ::: 34 | 35 | **例子** 36 | 37 | **安装仓库插件** 38 | ```shell 39 | $ vfox add --alias node nodejs 40 | 41 | $ vfox add golang java nodejs 42 | ``` 43 | 44 | 45 | **安装自定义插件** 46 | ```shell 47 | $ vfox add --source https://github.com/version-fox/vfox-nodejs/releases/download/v0.0.5/vfox-nodejs_0.0.5.zip custom-node 48 | ``` 49 | 50 | ## Info 51 | 52 | 查看指定插件信息 53 | 54 | **用法** 55 | 56 | ```shell 57 | vfox info 58 | vfox info @ 59 | vfox info [options] 60 | ``` 61 | 62 | `plugin-name`: 插件名称, 如`nodejs`。 63 | `version`: 插件的特定版本。 64 | 65 | **选项** 66 | 67 | - `-f, --format`: 使用给定的 Go 模板格式化输出。可用字段: 68 | - 插件信息:`Name`、`Version`、`Homepage`、`InstallPath`、`Description` 69 | - 版本信息:`Name`、`Version`、`Path` 70 | 71 | **例子** 72 | 73 | **查看插件信息** 74 | ```shell 75 | vfox info nodejs 76 | ``` 77 | 78 | **查看特定版本路径** 79 | ```shell 80 | vfox info nodejs@20.0.0 81 | ``` 82 | 83 | **使用模板格式化输出** 84 | ```shell 85 | vfox info --format "{{.Homepage}}" nodejs 86 | vfox info --format "{{.InstallPath}}" nodejs 87 | vfox info --format "{{.Path}}" nodejs@20.0.0 88 | ``` 89 | 90 | ## Remove 91 | 92 | 删除本地安装的插件。 93 | 94 | **用法** 95 | 96 | ```shell 97 | vfox remove 98 | ``` 99 | 100 | ::: danger 注意 101 | 删除插件,`vfox`会同步删除当前插件安装的所有版本运行时。 102 | ::: 103 | 104 | 105 | 106 | ## Update 107 | 108 | 更新指定的或全部已安装插件版本。 109 | 110 | **用法** 111 | 112 | ```shell 113 | vfox update 114 | vfox update --all # 更新所有已安装插件 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /docs/zh-hans/plugins/available.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vfox 3 | titleTemplate: 可用插件列表 4 | layout: doc 5 | editLink: false 6 | --- 7 | 8 | 36 | 37 | # 可用插件列表 38 | 39 | > 当前列举的插件全部来自[索引仓库](https://github.com/version-fox/vfox-plugins) 40 | 41 | ::: tip 提醒 42 | 这些都是来自社区的 vfox 插件 43 | 44 | 你可以通过以下命令来快速安装! 45 | 46 | ```shell 47 | vfox add 48 | ``` 49 | ::: 50 | 51 | 52 |
53 |
54 |
55 |

56 |

57 | {{item.name}} 58 |
59 | 60 |

61 |

{{item.desc}}

62 |
63 |
64 |
65 |
正在加载中, 请耐心等候...
66 | 67 | -------------------------------------------------------------------------------- /docs/usage/shims-path.md: -------------------------------------------------------------------------------- 1 | # Shims & PATH 2 | 3 | `vfox` manage versions by manipulating `PATH` directly, but some IDEs don't read the `PATH` environment variable, so we 4 | need some extra operations to let the IDE used. 5 | 6 | ## Shims Directory 7 | 8 | This directory is used to store all shims of global SDK. 9 | 10 | **Location**: `$HOME/.version-fox/shims` 11 | 12 | ```shell 13 | $ vfox use -g nodejs@14.17.0 14 | $ ~/.version-fox/shims/node -v 15 | v14.17.0 16 | ``` 17 | 18 | ::: warning 19 | 20 | `vfox` only handles all binary files in the installation directory. If you install binary files through other 21 | installation tools (`npm`), the `shims` directory will not contain them. 22 | 23 | Take `nodejs` as an example: 24 | ```shell 25 | $ vfox use -g nodejs@14.17.0 26 | $ npm install -g prettier@3.1.0 27 | $ ~/.version-fox/shims/node -v 28 | v14.17.0 29 | $ ~/.version-fox/shims/prettier -v # File not found!!!! 30 | ``` 31 | 32 | > Do not intend to provide the ability to rebuild `shim`. Please use the `current` soft link. 33 | 34 | ::: 35 | 36 | ::: tip Shim Implementation 37 | 38 | - **Windows**: `.exe` and `.shim` files 39 | - **Unix**: Soft link 40 | 41 | Take `nodejs` as an example: 42 | - **Windows**: `node.exe` and `node.shim` 43 | - **Unix**: `.version-fox/shims/node` -> `.version-fox/cache/nodejs/v-14.17.0/nodejs-14.17.0/bin/node` 44 | ::: 45 | 46 | ## `current` Soft Link 47 | 48 | `vfox` will create a soft link `current` in the `$HOME/.version-fox/cache//` directory, pointing to the corresponding SDK. 49 | 50 | **Location**: `$HOME/.version-fox/cache//current` 51 | 52 | Take `Nodejs` as an example: 53 | 54 | ```shell 55 | $ vfox use -g nodejs@14.17.0 56 | $ npm install -g prettier@3.1.0 57 | $ ~/.version-fox/cache/nodejs/current/node -v 58 | v14.17.0 59 | $ ~/.version-fox/cache/nodejs/current/prettier -v # Ok!!! 60 | 3.1.0 61 | ``` 62 | 63 | ::: tip 64 | 65 | `vfox`'s core for version management is also implemented through the `current` soft link. 66 | 67 | When you switch on Shell, a soft link is actually created to point to the corresponding SDK version, and the `current` 68 | soft link is stored in the temporary directory of the current `Shell`, as well as configured to `PATH`, to achieve version switching. 69 | 70 | ::: 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/compile-inno-setup.yml: -------------------------------------------------------------------------------- 1 | name: compile-inno-setup 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | permissions: 8 | contents: write 9 | id-token: write 10 | packages: write 11 | 12 | jobs: 13 | compile-inno-setup: 14 | name: Compile setup and publish 15 | runs-on: windows-latest 16 | defaults: 17 | run: 18 | working-directory: inno_setup 19 | steps: 20 | - name: Checkout version-fox 21 | uses: actions/checkout@v6 22 | - name: Get version-fox version 23 | id: version-fox-version 24 | uses: actions/github-script@v8 25 | with: 26 | github-token: NO_NEED 27 | result-encoding: string 28 | script: return "${{ github.ref }}".substring(11) 29 | - name: Install Inno Setup 30 | run: | 31 | curl --retry 10 --retry-all-errors -L -o installer.exe https://jrsoftware.org/download.php/is.exe 32 | ./installer.exe /verysilent /allusers /dir=inst 33 | sleep 60 34 | - name: Download version-fox packages 35 | env: 36 | VFOX_VERSION: ${{ steps.version-fox-version.outputs.result }} 37 | DOWNLOAD_URL: https://github.com/${{ github.event.repository.full_name }}/releases/download 38 | run: | 39 | curl -L -o i386.zip ${{ env.DOWNLOAD_URL }}/v${{ env.VFOX_VERSION }}/vfox_${{ env.VFOX_VERSION }}_windows_i386.zip && unzip i386.zip 40 | curl -L -o x86_64.zip ${{ env.DOWNLOAD_URL }}/v${{ env.VFOX_VERSION }}/vfox_${{ env.VFOX_VERSION }}_windows_x86_64.zip && unzip x86_64.zip 41 | curl -L -o aarch64.zip ${{ env.DOWNLOAD_URL }}/v${{ env.VFOX_VERSION }}/vfox_${{ env.VFOX_VERSION }}_windows_aarch64.zip && unzip aarch64.zip 42 | - name: Compile by Inno Setup 43 | env: 44 | VFOX_VERSION: ${{ steps.version-fox-version.outputs.result }} 45 | run: | 46 | ./inst/iscc vfox_windows_i386.iss 47 | ./inst/iscc vfox_windows_x86_64.iss 48 | ./inst/iscc vfox_windows_aarch64.iss 49 | - name: Upload Inno Setup Assets 50 | uses: version-fox/vfox-release-assets@v1 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | with: 54 | release_id: ${{ github.event.release.id }} 55 | assets_path: inno_setup/Output/*.exe -------------------------------------------------------------------------------- /cmd/commands/uninstall.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | "github.com/pterm/pterm" 26 | "github.com/urfave/cli/v3" 27 | "github.com/version-fox/vfox/internal" 28 | "github.com/version-fox/vfox/internal/base" 29 | ) 30 | 31 | var Uninstall = &cli.Command{ 32 | Name: "uninstall", 33 | Aliases: []string{"un"}, 34 | Usage: "Uninstall a version of the target SDK", 35 | Action: uninstallCmd, 36 | Category: CategorySDK, 37 | } 38 | 39 | func uninstallCmd(ctx context.Context, cmd *cli.Command) error { 40 | sdkArg := cmd.Args().First() 41 | if sdkArg == "" { 42 | return cli.Exit("sdk name is required", 1) 43 | } 44 | manager := internal.NewSdkManager() 45 | defer manager.Close() 46 | argArr := strings.Split(sdkArg, "@") 47 | argsLen := len(argArr) 48 | if argsLen != 2 { 49 | return cli.Exit("sdk version is invalid", 1) 50 | } 51 | 52 | name := strings.ToLower(argArr[0]) 53 | version := base.Version(argArr[1]) 54 | resolvedVersion := manager.ResolveVersion(name, version) 55 | 56 | source, err := manager.LookupSdk(name) 57 | if err != nil { 58 | return fmt.Errorf("%s not supported, error: %w", name, err) 59 | } 60 | cv := source.Current() 61 | if err = source.Uninstall(resolvedVersion); err != nil { 62 | return err 63 | } 64 | remainVersion := source.List() 65 | if len(remainVersion) == 0 { 66 | _ = os.RemoveAll(source.InstallPath) 67 | return nil 68 | } 69 | if cv == version { 70 | pterm.Println("Auto switch to the other version.") 71 | firstVersion := remainVersion[0] 72 | return source.Use(firstVersion, base.Global) 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/toolset/file_record.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package toolset 18 | 19 | import ( 20 | "bufio" 21 | "fmt" 22 | "github.com/version-fox/vfox/internal/util" 23 | "os" 24 | "strings" 25 | ) 26 | 27 | // FileRecord is a file that contains a map of string to string 28 | type FileRecord struct { 29 | Record map[string]string 30 | Path string 31 | isInitEmpty bool 32 | } 33 | 34 | func (m *FileRecord) Save() error { 35 | if m.isInitEmpty && len(m.Record) == 0 { 36 | return nil 37 | } 38 | file, err := os.Create(m.Path) 39 | if err != nil { 40 | return fmt.Errorf("failed to create file record %s: %w", m.Path, err) 41 | } 42 | defer file.Close() 43 | 44 | for k, v := range m.Record { 45 | _, err := fmt.Fprintf(file, "%s %s\n", k, v) 46 | if err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | // NewFileRecord creates a new FileRecord from a file 54 | // if the file does not exist, an empty FileRecord is returned 55 | func NewFileRecord(path string) (*FileRecord, error) { 56 | versionsMap := make(map[string]string) 57 | if util.FileExists(path) { 58 | file, err := os.Open(path) 59 | if err != nil { 60 | return nil, err 61 | } 62 | defer file.Close() 63 | scanner := bufio.NewScanner(file) 64 | for scanner.Scan() { 65 | line := scanner.Text() 66 | parts := strings.Split(line, " ") 67 | if len(parts) == 2 { 68 | versionsMap[parts[0]] = parts[1] 69 | } 70 | } 71 | 72 | if err := scanner.Err(); err != nil { 73 | return nil, err 74 | } 75 | } 76 | return &FileRecord{ 77 | Record: versionsMap, 78 | Path: path, 79 | isInitEmpty: len(versionsMap) == 0, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config_test 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/version-fox/vfox/internal/config" 24 | ) 25 | 26 | func TestNewConfig(t *testing.T) { 27 | c, err := config.NewConfig("") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | if c.Proxy.Url != "http://test" { 32 | t.Fatal("proxy url is invalid") 33 | } 34 | if !c.Proxy.Enable == false { 35 | t.Fatal("proxy enable is invalid") 36 | } 37 | if c.Storage.SdkPath != "/tmp" { 38 | t.Fatal("storage sdk path is invalid") 39 | } 40 | if !c.LegacyVersionFile.Enable { 41 | t.Fatal("legacy version file enable is invalid") 42 | } 43 | if c.Cache.AvailableHookDuration != config.CacheDuration(-1) { 44 | t.Fatal("cache available hook duration is invalid") 45 | } 46 | } 47 | 48 | func TestConfigWithEmpty(t *testing.T) { 49 | c, err := config.NewConfigWithPath("empty_test.yaml") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if c.Proxy.Url != "" { 54 | t.Fatal("proxy url must be empty") 55 | } 56 | if !c.Proxy.Enable == false { 57 | t.Fatal("proxy enable must be false") 58 | } 59 | if c.Storage.SdkPath != "" { 60 | t.Fatal("proxy url must be empty") 61 | } 62 | if c.LegacyVersionFile.Enable != true { 63 | t.Fatal("legacy version file enable must be true") 64 | } 65 | if c.Registry.Address != "" { 66 | t.Fatal("registry address must be empty") 67 | } 68 | } 69 | 70 | func TestStorageWithWritePermission(t *testing.T) { 71 | dir, err := os.UserHomeDir() 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | s := &config.Storage{ 76 | SdkPath: dir, 77 | } 78 | if err = s.Validate(); err != nil { 79 | t.Fatal(err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bump.sh - Version bumping script for vfox 4 | # Usage: ./scripts/bump.sh 5 | # Example: ./scripts/bump.sh 0.7.1 6 | 7 | set -e 8 | 9 | # Check if version argument is provided 10 | if [ $# -eq 0 ]; then 11 | echo "Usage: $0 " 12 | echo "Example: $0 0.7.1" 13 | exit 1 14 | fi 15 | 16 | NEW_VERSION="$1" 17 | VERSION_FILE="internal/version.go" 18 | 19 | # Validate version format (basic semver check) 20 | if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$'; then 21 | echo "Error: Invalid version format. Please use semantic versioning (e.g., 1.2.3)" 22 | exit 1 23 | fi 24 | 25 | # Check if version file exists 26 | if [ ! -f "$VERSION_FILE" ]; then 27 | echo "Error: Version file $VERSION_FILE not found" 28 | exit 1 29 | fi 30 | 31 | # Get current version 32 | CURRENT_VERSION=$(grep 'const RuntimeVersion' "$VERSION_FILE" | sed 's/.*"\(.*\)".*/\1/') 33 | echo "Current version: $CURRENT_VERSION" 34 | echo "New version: $NEW_VERSION" 35 | 36 | # Confirm the change 37 | read -p "Do you want to proceed with version bump? (y/N): " -n 1 -r 38 | echo 39 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then 40 | echo "Version bump cancelled" 41 | exit 0 42 | fi 43 | 44 | # Update version in the file 45 | echo "Updating version in $VERSION_FILE..." 46 | sed -i.bak "s/const RuntimeVersion = \".*\"/const RuntimeVersion = \"$NEW_VERSION\"/" "$VERSION_FILE" 47 | 48 | # Remove backup file 49 | rm "${VERSION_FILE}.bak" 50 | 51 | # Verify the change 52 | NEW_VERSION_CHECK=$(grep 'const RuntimeVersion' "$VERSION_FILE" | sed 's/.*"\(.*\)".*/\1/') 53 | if [ "$NEW_VERSION_CHECK" != "$NEW_VERSION" ]; then 54 | echo "Error: Version update failed. Expected $NEW_VERSION, got $NEW_VERSION_CHECK" 55 | exit 1 56 | fi 57 | 58 | echo "Version updated successfully to $NEW_VERSION" 59 | 60 | # Stage the version file 61 | echo "Staging version file..." 62 | git add "$VERSION_FILE" 63 | 64 | # Commit the change 65 | echo "Creating commit..." 66 | git commit -m "bump version to $NEW_VERSION" 67 | 68 | # Create and push tag 69 | echo "Creating git tag v$NEW_VERSION..." 70 | git tag "v$NEW_VERSION" 71 | 72 | echo "Version bump completed successfully!" 73 | echo "Don't forget to push the changes and tag:" 74 | echo " git push origin main" 75 | echo " git push origin v$NEW_VERSION" -------------------------------------------------------------------------------- /internal/util/clipboard.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "errors" 21 | "os/exec" 22 | "runtime" 23 | ) 24 | 25 | var ( 26 | // ErrClipboardNotSupported is returned when the OS doesn't support clipboard operations 27 | ErrClipboardNotSupported = errors.New("clipboard not supported on this OS") 28 | // ErrClipboardUtilityNotFound is returned when clipboard utility is not available 29 | ErrClipboardUtilityNotFound = errors.New("clipboard utility not found") 30 | ) 31 | 32 | // CopyToClipboard copies the given text to the system clipboard 33 | // Returns nil if successful, error otherwise 34 | func CopyToClipboard(text string) error { 35 | var cmd *exec.Cmd 36 | 37 | switch runtime.GOOS { 38 | case "darwin": 39 | cmd = exec.Command("pbcopy") 40 | case "linux": 41 | // Try xclip first, then xsel as fallback 42 | if _, err := exec.LookPath("xclip"); err == nil { 43 | cmd = exec.Command("xclip", "-selection", "clipboard") 44 | } else if _, err := exec.LookPath("xsel"); err == nil { 45 | cmd = exec.Command("xsel", "--clipboard", "--input") 46 | } else { 47 | // No clipboard utility available 48 | return ErrClipboardUtilityNotFound 49 | } 50 | case "windows": 51 | cmd = exec.Command("clip") 52 | default: 53 | // Unsupported OS 54 | return ErrClipboardNotSupported 55 | } 56 | 57 | if cmd == nil { 58 | return ErrClipboardNotSupported 59 | } 60 | 61 | in, err := cmd.StdinPipe() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if err := cmd.Start(); err != nil { 67 | return err 68 | } 69 | 70 | if _, err := in.Write([]byte(text)); err != nil { 71 | return err 72 | } 73 | 74 | if err := in.Close(); err != nil { 75 | return err 76 | } 77 | 78 | return cmd.Wait() 79 | } 80 | -------------------------------------------------------------------------------- /docs/plugins/available.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: vfox 3 | titleTemplate: Available Plugins 4 | layout: doc 5 | editLink: false 6 | --- 7 | 8 | 36 | 37 | # Available Plugins 38 | 39 | > Current listed plugins are all from the [Registry](https://github.com/version-fox/vfox-plugins) 40 | 41 | ::: tip 42 | These are all vfox plugins from the community 43 | 44 | You can quickly install them with the following command! 45 | 46 | ```shell 47 | vfox add 48 | ``` 49 | ::: 50 | 51 | 52 |
53 |
54 |
55 |

56 |

57 | {{item.name}} 58 |
59 | 60 |

61 |

{{item.desc}}

62 |
63 |
64 |
65 |
Loading, please be patient...
66 | 67 | -------------------------------------------------------------------------------- /internal/config/cache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "gopkg.in/yaml.v3" 24 | ) 25 | 26 | var ( 27 | EmptyCache = &Cache{ 28 | AvailableHookDuration: CacheDuration(12 * time.Hour), 29 | } 30 | ) 31 | 32 | // Cache is the cache configuration 33 | type Cache struct { 34 | AvailableHookDuration CacheDuration `yaml:"availableHookDuration"` // Available hook result cache time 35 | } 36 | 37 | // CacheDuration is a duration that represents the cache duration and some special values 38 | // -1: never expire 39 | // 0: never cache 40 | type CacheDuration time.Duration 41 | 42 | func (d CacheDuration) MarshalYAML() (interface{}, error) { 43 | switch d { 44 | case -1: 45 | return -1, nil 46 | case 0: 47 | return 0, nil 48 | default: 49 | return d.String(), nil 50 | } 51 | } 52 | 53 | func (d *CacheDuration) UnmarshalYAML(node *yaml.Node) error { 54 | var data any 55 | err := node.Decode(&data) 56 | if err != nil { 57 | return err 58 | } 59 | switch va := data.(type) { 60 | case int: 61 | *d = CacheDuration(va) 62 | case string: 63 | pd, err := time.ParseDuration(va) 64 | if err != nil { 65 | return err 66 | } 67 | *d = CacheDuration(pd) 68 | } 69 | return nil 70 | } 71 | 72 | func (d CacheDuration) String() string { 73 | switch d { 74 | case -1: 75 | return "-1" 76 | case 0: 77 | return "0" 78 | } 79 | 80 | var str string 81 | duration := time.Duration(d) 82 | if h := int(duration.Hours()); h > 0 { 83 | str = fmt.Sprintf("%dh", h) 84 | } 85 | if m := int(duration.Minutes()) % 60; m > 0 { 86 | str = fmt.Sprintf("%s%dm", str, m) 87 | } 88 | if s := int(duration.Seconds()) % 60; s > 0 { 89 | str = fmt.Sprintf("%s%ds", str, s) 90 | } 91 | 92 | return str 93 | } 94 | -------------------------------------------------------------------------------- /internal/toolset/tool_version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package toolset 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | ) 23 | 24 | const filename = ".tool-versions" 25 | 26 | type MultiToolVersions []*ToolVersion 27 | 28 | // FilterTools filters tools by the given filter function 29 | // and return the first one you find. 30 | func (m MultiToolVersions) FilterTools(filter func(name, version string) bool) map[string]string { 31 | tools := make(map[string]string) 32 | for _, t := range m { 33 | for name, version := range t.Record { 34 | _, ok := tools[name] 35 | if !ok && filter(name, version) { 36 | tools[name] = version 37 | } 38 | } 39 | } 40 | return tools 41 | } 42 | 43 | func (m MultiToolVersions) Add(name, version string) { 44 | for _, t := range m { 45 | t.Record[name] = version 46 | } 47 | } 48 | 49 | func (m MultiToolVersions) Save() error { 50 | for _, t := range m { 51 | if err := t.Save(); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | 58 | // ToolVersion represents a .tool-versions file 59 | type ToolVersion struct { 60 | *FileRecord 61 | } 62 | 63 | func NewToolVersion(dirPath string) (*ToolVersion, error) { 64 | file := filepath.Join(dirPath, filename) 65 | mapFile, err := NewFileRecord(file) 66 | if err != nil { 67 | return nil, fmt.Errorf("failed to read tool versions file %s: %w", file, err) 68 | } 69 | return &ToolVersion{ 70 | FileRecord: mapFile, 71 | }, nil 72 | } 73 | 74 | func NewMultiToolVersions(paths []string) (MultiToolVersions, error) { 75 | var tools MultiToolVersions 76 | for _, p := range paths { 77 | tool, err := NewToolVersion(p) 78 | if err != nil { 79 | return nil, err 80 | } 81 | tools = append(tools, tool) 82 | } 83 | return tools, nil 84 | } 85 | -------------------------------------------------------------------------------- /docs/zh-hans/guides/intro.md: -------------------------------------------------------------------------------- 1 | # 项目简介 2 | 3 | 如果你经常需要在各种开发项目之间切换,而这些项目又各自需要不同的运行环境,尤其是不同的运行时版本或环境库, 或者 4 | 厌倦了各种环境繁琐的配置,那么 `vfox` 就是你的不二选择。 5 | 6 | `vfox` 是一款跨平台、可拓展的通用版本管理器。支持**原生Windows**以及**Unix-like**! 通过它,您可以**快速安装和切换**开发环境。 7 | 8 | 它将所有的工具版本信息保存在一个名为 `.tool-versions` 的文件中,这样您就可以在项目中共享这些信息,确保团队中的每个人都使用相同的工具版本。 9 | 10 | 传统工作方式需要多个命令行版本管理器(如`nvm`、`fvm`、`sdkman`、`asdf-vm`等),而且每个管理器都有其不同的 11 | API、配置文件和实现方式(比如,`$PATH` 12 | 操作、垫片、环境变量等等)。`vfox` 提供单个交互方式和配置文件来简化开发工作流程,并可通过简单的插件接口扩展到所有工具和运行环境。 13 | 14 | ## 为什么选择 vfox? 15 | 16 | 17 | - 支持**Windows(非WSL)**、Linux、macOS! 18 | - 支持**不同项目不同版本**、**不同Shell不同版本**以及**全局版本** 19 | - 简单的 **插件系统** 来添加对你选择的语言的支持 20 | - 在您切换项目时, 帮您**自动切换**运行时版本 21 | - 支持现有配置文件 `.node-version`、`.nvmrc`、`.sdkmanrc`,以方便迁移 22 | - 支持常用Shell(Powershell、Bash、ZSH),并提供补全功能 23 | - **比 `asdf-vm` 更快**,并提供更简单的命令和真正的跨平台统一。参见 [与asdf-vm对比](../misc/vs-asdf.md)。 24 | 25 | ## 已支持 Shell 26 | 27 | | Shell | Support | Note | 28 | |------------|---------|---------------------------------------------------------------------------------| 29 | | Powershell | ✅ | | 30 | | GitBash | ✅ | [相关问题](./faq.md#why-can-t-i-select-when-use-use-and-search-commands-in-gitbash) | 31 | | Bash | ✅ | | 32 | | ZSH | ✅ | | 33 | | Fish | ✅ | | 34 | | CMD | ✅ | 仅支持`Global`作用域,不推荐使用!!! | 35 | | Clink | ✅ | | 36 | | Cmder | ✅ | | 37 | | Nushell | ✅ | | 38 | 39 | 40 | 41 | ## 贡献者 42 | 43 | > [!TIP] 44 | > 感谢以下贡献者对本项目的贡献。🎉🎉🙏🙏 45 | 46 | #### [核心仓库](https://github.com/version-fox/vfox) 47 | 48 | ![plugins](https://contrib.rocks/image?repo=version-fox/vfox) 49 | 50 | #### [插件仓库](https://github.com/version-fox/vfox-plugins) 51 | 52 | ![plugins](https://contrib.rocks/image?repo=version-fox/vfox-plugins)) 53 | -------------------------------------------------------------------------------- /internal/config/cache_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "gopkg.in/yaml.v3" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | func TestCacheDuration_MarshalYAML(t *testing.T) { 26 | tests := []struct { 27 | name string 28 | cd CacheDuration 29 | want interface{} 30 | wantErr bool 31 | }{ 32 | {"Negative", CacheDuration(-1), -1, false}, 33 | {"Zero", CacheDuration(0), 0, false}, 34 | {"Positive", CacheDuration(time.Hour), "1h", false}, 35 | } 36 | 37 | for _, tt := range tests { 38 | t.Run(tt.name, func(t *testing.T) { 39 | got, err := tt.cd.MarshalYAML() 40 | if (err != nil) != tt.wantErr { 41 | t.Errorf("MarshalYAML() error = %v, wantErr %v", err, tt.wantErr) 42 | return 43 | } 44 | if got != tt.want { 45 | t.Errorf("MarshalYAML() got = %v, want %v", got, tt.want) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestCacheDuration_UnmarshalYAML(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | node *yaml.Node 55 | want CacheDuration 56 | wantErr bool 57 | }{ 58 | {"Negative", &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: "-1"}, CacheDuration(-1), false}, 59 | {"Zero", &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!int", Value: "0"}, CacheDuration(0), false}, 60 | {"Positive", &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "1h"}, CacheDuration(time.Hour), false}, 61 | } 62 | 63 | for _, tt := range tests { 64 | t.Run(tt.name, func(t *testing.T) { 65 | cd := CacheDuration(0) 66 | if err := cd.UnmarshalYAML(tt.node); (err != nil) != tt.wantErr { 67 | t.Errorf("UnmarshalYAML() error = %v, wantErr %v", err, tt.wantErr) 68 | } 69 | if cd != tt.want { 70 | t.Errorf("UnmarshalYAML() got = %v, want %v", cd, tt.want) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/env/path.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package env 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/version-fox/vfox/internal/logger" 26 | "github.com/version-fox/vfox/internal/util" 27 | ) 28 | 29 | type PathFrom int 30 | 31 | const ( 32 | EmptyPaths PathFrom = iota 33 | OsPaths 34 | ) 35 | 36 | // Paths is a slice of PATH. 37 | type Paths struct { 38 | *util.SortedSet[string] 39 | } 40 | 41 | func (p *Paths) Merge(other *Paths) *Paths { 42 | if other == nil { 43 | return p 44 | } 45 | for _, path := range other.Slice() { 46 | p.Add(path) 47 | } 48 | return p 49 | } 50 | 51 | // ToBinPaths returns a BinPaths from Paths which contains only executable files. 52 | func (p *Paths) ToBinPaths() (*Paths, error) { 53 | bins := NewPaths(EmptyPaths) 54 | for _, path := range p.Slice() { 55 | dir, err := os.ReadDir(path) 56 | if err != nil { 57 | logger.Debugf("Failed to read bin paths:%s: %v", path, err) 58 | return nil, fmt.Errorf("failed to read bin paths:%s: %w", path, err) 59 | } 60 | for _, d := range dir { 61 | if d.IsDir() { 62 | continue 63 | } 64 | file := filepath.Join(path, d.Name()) 65 | if util.IsExecutable(file) { 66 | bins.Add(file) 67 | } 68 | } 69 | } 70 | return bins, nil 71 | } 72 | 73 | // NewPaths returns a new Paths. 74 | // from is the source of the paths. 75 | // If from is OsPaths, it returns the paths from the environment variable PATH. 76 | func NewPaths(from PathFrom) *Paths { 77 | var paths []string 78 | switch from { 79 | case OsPaths: 80 | paths = strings.Split(os.Getenv("PATH"), string(os.PathListSeparator)) 81 | default: 82 | 83 | } 84 | p := &Paths{ 85 | util.NewSortedSet[string](), 86 | } 87 | for _, v := range paths { 88 | p.Add(v) 89 | } 90 | return p 91 | } 92 | -------------------------------------------------------------------------------- /docs/zh-hans/plugins/create/howto_registry.md: -------------------------------------------------------------------------------- 1 | # 如何将插件提交到索引仓库? 2 | 3 | :::tip 提醒 4 | 请注意,这个文档是关于如何将插件提交到[索引仓库](https://github.com/version-fox/vfox-plugins)的。 5 | 6 | 如果你想要了解如何创建一个插件,请查看[这里](./howto.md)。 7 | ::: 8 | 9 | 10 | ## 索引仓库(Registry) 11 | 12 | `vfox` 插件索引仓库是一个用于**收集和分发**各种**vfox插件**的仓库。 方便用户通过`vfox add `命令来快速安装对应插件。 13 | 14 | 索引仓库是一个公共仓库, 任何人都可以提交**vfox插件**到这个仓库中。 15 | 16 | 索引仓库主要分为两个部分: 17 | - `plugins`: 用于存放插件的`manifest.json`文件, 以插件短名为文件名。例如`nodejs.json` 18 | - `sources`: 用于存放插件的数据源信息, 以插件短名为文件名。例如`nodejs.json` 19 | 20 | 仓库将会自动定时(间隔一小时)通过`sources`中的信息检索插件的最新版本信息以及校验插件可用性, 并将获取的`manifest`信息存放在`plugins`目录下。 21 | 22 | 23 | ::: tip 仓库地址 24 | `vfox`默认将会从[插件仓库](https://version-fox.github.io/vfox-plugins)检索插件。 25 | 26 | 如果你想使用**自己的索引仓库或第三方镜像仓库**, 请按照[插件注册表地址](../../guides/configuration.md#插件注册表地址)进行配置。 27 | ::: 28 | 29 | ## 提交插件 30 | 31 | 1. 首先,你需要按照[插件创建指南](./howto.md)创建一个插件。 32 | 2. 维护一个`manifest.json`文件,用于描述插件最新版本信息。(如果基于[vfox-plugin-template](https://github.com/version-fox/vfox-plugin-template)开发, 在发版时会自动生成) 33 | ```json 34 | { 35 | "downloadUrl": "https://github.com/version-fox/vfox-nodejs/releases/download/v0.0.5/vfox-nodejs_0.0.5.zip", 36 | "notes": [], 37 | "version": "0.0.5", 38 | "homepage": "https://github.com/version-fox/vfox-nodejs", 39 | "minRuntimeVersion": "0.2.6", 40 | "license": "Apache 2.0", 41 | "description": "Node.js runtime environment.", 42 | "name": "nodejs" 43 | } 44 | ``` 45 | - `downloadUrl`: 插件的下载地址 46 | - `notes`: 插件的更新日志 47 | - `version`: 插件的版本 48 | - `homepage`: 插件的主页 49 | - `minRuntimeVersion`: 插件所需的最低`vfox`版本 50 | - `license`: 插件的许可证 51 | - `description`: 插件的描述 52 | - `name`: 插件的短名 53 | 3. 在`sources/.json`中创建一个带有您希望`vfox`使用的短名的文件, 例如`sources/nodejs.json` 54 | 4. 在`sources/.json`中添加插件的manifest地址信息,例如: 55 | ```json 56 | { 57 | "name": "nodejs", 58 | "manifestUrl": "https://github.com/version-fox/vfox-nodejs/releases/download/manifest/manifest.json", 59 | "test": { 60 | "version": "21.7.1", 61 | "check": "node -v", 62 | "resultRegx": "v21.7.1" 63 | } 64 | } 65 | ``` 66 | - `name`: 插件的短名 67 | - `manifestUrl`: 插件的manifest地址 68 | - `test`: 插件的测试信息 69 | - `version`: 插件的版本 70 | - `check`: 测试命令 71 | - `resultRegx`: 测试结果正则表达式 72 | 5. 最后, 提交一个PR到[索引仓库](https://github.com/version-fox/vfox-plugins/). 73 | 6. PR被合并后,插件将会被自动添加到索引仓库中, 并每隔一小时检查一次插件的更新情况。 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /cmd/commands/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/pterm/pterm" 24 | "github.com/pterm/pterm/putils" 25 | "github.com/urfave/cli/v3" 26 | "github.com/version-fox/vfox/internal" 27 | ) 28 | 29 | var List = &cli.Command{ 30 | Name: "list", 31 | Aliases: []string{"ls"}, 32 | Usage: "List all versions of the target SDK", 33 | Action: listCmd, 34 | Category: CategorySDK, 35 | } 36 | 37 | func listCmd(ctx context.Context, cmd *cli.Command) error { 38 | manager := internal.NewSdkManager() 39 | defer manager.Close() 40 | sdkName := cmd.Args().First() 41 | if sdkName == "" { 42 | allSdk, err := manager.LoadAllSdk() 43 | if err != nil { 44 | return err 45 | } 46 | if len(allSdk) == 0 { 47 | return fmt.Errorf("you don't have any sdk installed yet") 48 | } 49 | tree := pterm.LeveledList{} 50 | 51 | for _, s := range allSdk { 52 | name := s.Name 53 | tree = append(tree, pterm.LeveledListItem{Level: 0, Text: name}) 54 | for _, version := range s.List() { 55 | tree = append(tree, pterm.LeveledListItem{Level: 1, Text: "v" + string(version)}) 56 | } 57 | } 58 | // Generate tree from LeveledList. 59 | root := putils.TreeFromLeveledList(tree) 60 | root.Text = "All installed sdk versions" 61 | // Render TreePrinter 62 | _ = pterm.DefaultTree.WithRoot(root).Render() 63 | return nil 64 | } 65 | source, err := manager.LookupSdk(sdkName) 66 | if err != nil { 67 | return fmt.Errorf("%s not supported, error: %w", sdkName, err) 68 | } 69 | curVersion := source.Current() 70 | list := source.List() 71 | if len(list) == 0 { 72 | return fmt.Errorf("no available version") 73 | } 74 | for _, version := range list { 75 | if version == curVersion { 76 | pterm.Println("->", fmt.Sprintf("v%s", version), pterm.LightGreen("<— current")) 77 | } else { 78 | pterm.Println("->", fmt.Sprintf("v%s", version)) 79 | } 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/env/flag.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package env 18 | 19 | import ( 20 | "os" 21 | "strconv" 22 | ) 23 | 24 | const ( 25 | HomeFromEnv = "VFOX_HOME" 26 | PluginFromEnv = "VFOX_PLUGIN" 27 | CacheFromEnv = "VFOX_CACHE" 28 | TempFromEnv = "VFOX_TEMP" 29 | 30 | HookFlag = "__VFOX_SHELL" 31 | PidFlag = "__VFOX_PID" 32 | ) 33 | 34 | func IsHookEnv() bool { 35 | return os.Getenv(HookFlag) != "" 36 | } 37 | 38 | // IsIDEEnvironmentResolution detects if the current shell session was launched by an IDE 39 | // for the purpose of environment variable resolution. This is useful to avoid certain 40 | // shell initialization behaviors that might interfere with IDE environment detection. 41 | // 42 | // Supported IDEs: 43 | // - Visual Studio Code: Detects via VSCODE_RESOLVING_ENVIRONMENT environment variable 44 | // Reference: https://code.visualstudio.com/docs/configure/command-line#_how-do-i-detect-when-a-shell-was-launched-by-vs-code 45 | // - JetBrains IDEs (IntelliJ IDEA, PyCharm, etc.): Detects via INTELLIJ_ENVIRONMENT_READER environment variable 46 | // Reference: https://youtrack.jetbrains.com/articles/SUPPORT-A-1727/Shell-Environment-Loading 47 | // 48 | // Returns true if any of the supported IDE environment resolution indicators are present. 49 | func IsIDEEnvironmentResolution() bool { 50 | return os.Getenv("VSCODE_RESOLVING_ENVIRONMENT") != "" || 51 | os.Getenv("INTELLIJ_ENVIRONMENT_READER") != "" 52 | } 53 | 54 | func GetPid() int { 55 | if IsHookEnv() { 56 | if pid := os.Getenv(PidFlag); pid != "" { 57 | p, _ := strconv.Atoi(pid) // Convert pid from string to int 58 | return p 59 | } 60 | } 61 | 62 | return os.Getppid() 63 | } 64 | 65 | func GetVfoxHome() string { 66 | return os.Getenv(HomeFromEnv) 67 | } 68 | 69 | func GetVfoxPlugin() string { 70 | return os.Getenv(PluginFromEnv) 71 | } 72 | 73 | func GetVfoxCache() string { 74 | return os.Getenv(CacheFromEnv) 75 | } 76 | 77 | func GetVfoxTemp() string { 78 | return os.Getenv(TempFromEnv) 79 | } 80 | -------------------------------------------------------------------------------- /internal/manager_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/shirou/gopsutil/v4/process" 11 | ) 12 | 13 | func detectPidNotExists(t *testing.T, pid int32, num int32) int32 { 14 | if num < 1 { 15 | t.Fatalf("num must be greater than 1") 16 | } 17 | 18 | for i := pid + 1; i < pid+num; i++ { 19 | exists, err := process.PidExists(i) 20 | if err != nil { 21 | t.Fatalf("failed to check pid %d existence: %v", i, err) 22 | } 23 | 24 | if !exists { 25 | return i 26 | } 27 | } 28 | 29 | return pid + num 30 | } 31 | 32 | func TestCleanTmp(t *testing.T) { 33 | tmpRoot := filepath.Join(os.TempDir(), "vfox-cleantmp-test") 34 | _ = os.RemoveAll(tmpRoot) 35 | defer os.RemoveAll(tmpRoot) 36 | now := time.Now() 37 | today := strconv.FormatInt(time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix(), 10) 38 | // Create a simulated temporary directory structure 39 | yesterday := strconv.FormatInt(now.Add(-250*time.Hour).Unix(), 10) 40 | pid := os.Getpid() 41 | otherPid := int(detectPidNotExists(t, int32(pid+100), 10000)) 42 | dirs := []string{ 43 | filepath.Join(tmpRoot, today+"-"+strconv.Itoa(pid)), 44 | // filepath.Join(tmpRoot, yesterday+"-"+strconv.Itoa(pid)), 45 | filepath.Join(tmpRoot, yesterday+"-"+strconv.Itoa(otherPid)), 46 | filepath.Join(tmpRoot, yesterday+"-"+strconv.Itoa(pid)), 47 | } 48 | for _, dir := range dirs { 49 | if err := os.MkdirAll(dir, 0o755); err != nil { 50 | t.Fatalf("failed to create dir: %v", err) 51 | } 52 | } 53 | cleanFlagPath := filepath.Join(tmpRoot, cleanupFlagFilename) 54 | // Write yesterday to cleanFlagPath 55 | if err := os.WriteFile(cleanFlagPath, []byte(yesterday), 0o644); err != nil { 56 | t.Fatalf("failed to write cleanFlagPath: %v", err) 57 | } 58 | 59 | // Construct Manager 60 | m := &Manager{PathMeta: &PathMeta{CurTmpPath: tmpRoot, TempPath: tmpRoot}} 61 | 62 | // Execute cleanup 63 | m.CleanTmp() 64 | 65 | // today-pid should be retained 66 | if _, err := os.Stat(filepath.Join(tmpRoot, today+"-"+strconv.Itoa(pid))); os.IsNotExist(err) { 67 | t.Errorf("today-pid dir should exist, but got removed") 68 | } 69 | 70 | // yesterday-otherPid should be deleted 71 | if _, err := os.Stat(filepath.Join(tmpRoot, yesterday+"-"+strconv.Itoa(otherPid))); err == nil { 72 | t.Errorf("yesterday-otherPid dir should be removed, but still exists") 73 | } 74 | 75 | // yesterday-pid should be retained 76 | if _, err := os.Stat(filepath.Join(tmpRoot, yesterday+"-"+strconv.Itoa(pid))); os.IsNotExist(err) { 77 | t.Errorf("yesterday-pid dir should exist, but got removed") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /internal/shell/shell_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestStripLoginShellDash(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | input string 11 | expected string 12 | }{ 13 | { 14 | name: "zsh with dash", 15 | input: "-zsh", 16 | expected: "zsh", 17 | }, 18 | { 19 | name: "bash with dash", 20 | input: "-bash", 21 | expected: "bash", 22 | }, 23 | { 24 | name: "fish with dash", 25 | input: "-fish", 26 | expected: "fish", 27 | }, 28 | { 29 | name: "shell without dash", 30 | input: "zsh", 31 | expected: "zsh", 32 | }, 33 | { 34 | name: "shell with path and dash", 35 | input: "-/bin/zsh", 36 | expected: "/bin/zsh", 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | shellName := tt.input 43 | if len(shellName) > 0 && shellName[0] == '-' { 44 | shellName = shellName[1:] 45 | } 46 | if shellName != tt.expected { 47 | t.Errorf("stripLoginShellDash() = %v, want %v", shellName, tt.expected) 48 | } 49 | }) 50 | } 51 | } 52 | 53 | func TestCommonShellPaths(t *testing.T) { 54 | tests := []struct { 55 | name string 56 | shell string 57 | expected []string 58 | }{ 59 | { 60 | name: "zsh paths", 61 | shell: "zsh", 62 | expected: []string{ 63 | "/bin/zsh", 64 | "/usr/bin/zsh", 65 | "/usr/local/bin/zsh", 66 | "/opt/homebrew/bin/zsh", 67 | "/usr/local/Cellar/zsh", 68 | }, 69 | }, 70 | { 71 | name: "bash paths", 72 | shell: "bash", 73 | expected: []string{ 74 | "/bin/bash", 75 | "/usr/bin/bash", 76 | "/usr/local/bin/bash", 77 | "/opt/homebrew/bin/bash", 78 | "/usr/local/Cellar/bash", 79 | }, 80 | }, 81 | } 82 | 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | // This test verifies the expected paths are generated correctly 86 | // Actual file existence may vary on different systems 87 | commonPaths := []string{ 88 | "/bin/" + tt.shell, 89 | "/usr/bin/" + tt.shell, 90 | "/usr/local/bin/" + tt.shell, 91 | "/opt/homebrew/bin/" + tt.shell, 92 | "/usr/local/Cellar/" + tt.shell, 93 | } 94 | 95 | if len(commonPaths) != len(tt.expected) { 96 | t.Errorf("Expected %d paths, got %d", len(tt.expected), len(commonPaths)) 97 | } 98 | 99 | for i, path := range commonPaths { 100 | if path != tt.expected[i] { 101 | t.Errorf("Expected path %s, got %s", tt.expected[i], path) 102 | } 103 | } 104 | }) 105 | } 106 | } -------------------------------------------------------------------------------- /internal/plugin/luai/vm.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package luai 18 | 19 | import ( 20 | _ "embed" 21 | "strings" 22 | 23 | "github.com/version-fox/vfox/internal/plugin/luai/module" 24 | lua "github.com/yuin/gopher-lua" 25 | ) 26 | 27 | //go:embed fixtures/preload.lua 28 | var preloadScript string 29 | 30 | type LuaVM struct { 31 | Instance *lua.LState 32 | } 33 | 34 | func NewLuaVM() *LuaVM { 35 | instance := lua.NewState() 36 | 37 | return &LuaVM{ 38 | Instance: instance, 39 | } 40 | } 41 | 42 | func (vm *LuaVM) Prepare(options *module.PreloadOptions) error { 43 | if err := vm.Instance.DoString(preloadScript); err != nil { 44 | return err 45 | } 46 | 47 | if options != nil { 48 | module.Preload(vm.Instance, options) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // LimitPackagePath limits the package path of the Lua VM. 55 | func (vm *LuaVM) LimitPackagePath(packagePaths ...string) { 56 | packageModule := vm.Instance.GetGlobal("package").(*lua.LTable) 57 | packageModule.RawSetString("path", lua.LString(strings.Join(packagePaths, ";"))) 58 | } 59 | 60 | func (vm *LuaVM) ReturnedValue() *lua.LTable { 61 | table := vm.Instance.ToTable(-1) // returned value 62 | vm.Instance.Pop(1) // remove received value 63 | return table 64 | } 65 | 66 | func (vm *LuaVM) CallFunction(pluginObj *lua.LTable, funcName string, _args ...lua.LValue) (*lua.LTable, error) { 67 | function := pluginObj.RawGetString(funcName) 68 | 69 | // In Lua, when a function is called with colon syntax (object:method()), 70 | // the object itself is implicitly passed as the first argument. 71 | // Here, pluginObj represents the Lua table instance of the plugin, 72 | // and it's being passed as the first argument to the Lua function to simulate this behavior. 73 | args := append([]lua.LValue{pluginObj}, _args...) 74 | 75 | if err := vm.Instance.CallByParam(lua.P{ 76 | Fn: function.(*lua.LFunction), 77 | NRet: 1, 78 | Protect: true, 79 | }, args...); err != nil { 80 | return nil, err 81 | } 82 | 83 | return vm.ReturnedValue(), nil 84 | } 85 | 86 | func (vm *LuaVM) Close() { 87 | vm.Instance.Close() 88 | } 89 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "github.com/version-fox/vfox/internal/util" 21 | "gopkg.in/yaml.v3" 22 | "os" 23 | "path/filepath" 24 | ) 25 | 26 | type Config struct { 27 | Proxy *Proxy `yaml:"proxy"` 28 | Storage *Storage `yaml:"storage"` 29 | Registry *Registry `yaml:"registry"` 30 | LegacyVersionFile *LegacyVersionFile `yaml:"legacyVersionFile"` 31 | Cache *Cache `yaml:"cache"` 32 | } 33 | 34 | const filename = "config.yaml" 35 | 36 | var ( 37 | DefaultConfig = &Config{ 38 | Proxy: EmptyProxy, 39 | Storage: EmptyStorage, 40 | Registry: EmptyRegistry, 41 | LegacyVersionFile: EmptyLegacyVersionFile, 42 | Cache: EmptyCache, 43 | } 44 | ) 45 | 46 | func NewConfigWithPath(p string) (*Config, error) { 47 | if !util.FileExists(p) { 48 | content, err := yaml.Marshal(DefaultConfig) 49 | if err == nil { 50 | _ = os.WriteFile(p, content, 0666) 51 | return DefaultConfig, nil 52 | } 53 | } 54 | _ = util.ChangeModeIfNot(p, 0666) 55 | content, err := os.ReadFile(p) 56 | if err != nil { 57 | return nil, err 58 | } 59 | config := &Config{} 60 | err = yaml.Unmarshal(content, config) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if config.Proxy == nil { 65 | config.Proxy = EmptyProxy 66 | } 67 | if config.Storage == nil { 68 | config.Storage = EmptyStorage 69 | } 70 | if config.Registry == nil { 71 | config.Registry = EmptyRegistry 72 | } 73 | if config.LegacyVersionFile == nil { 74 | config.LegacyVersionFile = EmptyLegacyVersionFile 75 | } 76 | if config.Cache == nil { 77 | config.Cache = EmptyCache 78 | } 79 | return config, nil 80 | 81 | } 82 | 83 | func NewConfig(path string) (*Config, error) { 84 | p := filepath.Join(path, filename) 85 | return NewConfigWithPath(p) 86 | } 87 | 88 | func (c *Config) SaveConfig(path string) error { 89 | p := filepath.Join(path, filename) 90 | content, err := yaml.Marshal(c) 91 | if err != nil { 92 | return err 93 | } 94 | return os.WriteFile(p, content, os.ModePerm) 95 | } 96 | -------------------------------------------------------------------------------- /docs/zh-hans/guides/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | `vfox` 允许你修改一些配置, 所有配置信息都存放在`$HOME/.version-fox/config.yaml`文件中。 4 | 5 | ::: tip 注意 6 | 如果你是首次运行`vfox`, 则会自动创建一个空的 config.yaml 文件。 7 | ::: 8 | 9 | ## 兼容版本文件 10 | 11 | 插件 **支持** 读取其他版本管理器的配置文件, 例如: Nodejs 的`nvm`的`.nvmrc`文件, Java 的`SDKMAN`的`.sdkmanrc`文件等。 12 | 13 | 此能力**默认是开启的**。相关配置选项如下: 14 | 15 | ```yaml 16 | legacyVersionFile: 17 | enable: true 18 | strategy: "specified" # 解析策略 19 | ``` 20 | 21 | - `enable`: 是否启用 legacy version file 解析功能 22 | - `strategy`: 解析策略,详见下方策略选项说明 23 | 24 | ### 策略选项 25 | 26 | `vfox` 支持以下三种解析策略: 27 | 28 | - `latest_installed`: 使用最新安装的版本 29 | - `latest_available`: 使用最新可用的版本 30 | - `specified`: 使用 legacy file 中指定的版本(默认) 31 | 32 | ::: warning 33 | 34 | 1. 如果目录里同时存在`.tool-versions`和其他版本管理器的配置文件(`.nvmrc`, `.sdkmanrc`等), 35 | `vfox` **优先加载**`.tool-versions`文件. 36 | 2. 开启此功能可能会导致`vfox`刷新环境变量时略微变慢, **请根据自己的需求开启**。 37 | 38 | ::: 39 | 40 | 如果你想禁用此功能,可以使用命令:`vfox config legacyVersionFile.enable false` 41 | 42 | ## 代理设置 43 | 44 | ::: tip 注意 45 | 当前仅支持 http(s)代理协议 46 | ::: 47 | 48 | **格式**: `http[s]://[username:password@]host:port` 49 | 50 | ```yaml 51 | proxy: 52 | enable: false 53 | url: http://localhost:7890 54 | ``` 55 | 56 | ## 存储路径 57 | 58 | `vfox`默认将 SDK 缓存文件存储在`$HOME/.version-fox/cache`目录下。 59 | 60 | ::: danger !!! 61 | 在配置之前, 请确保`vfox`有文件夹的写权限。⚠⚠⚠ 62 | ::: 63 | 64 | ```yaml 65 | storage: 66 | sdkPath: /tmp 67 | ``` 68 | 69 | ## 插件注册表地址 70 | 71 | `vfox`默认从[插件仓库](https://version-fox.github.io/vfox-plugins)检索插件。 72 | 73 | 如果你想要使用自己的索引仓库或第三方镜像仓库,可以按照以下方式配置: 74 | 75 | ```yaml 76 | registry: 77 | address: "https://version-fox.github.io/vfox-plugins" 78 | ``` 79 | 80 | ::: tip 可用镜像 81 | 82 | - https://gitee.com/version-fox/vfox-plugins/raw/main/plugins 83 | - https://cdn.jsdelivr.net/gh/version-fox/vfox-plugins/plugins 84 | - https://rawcdn.githack.com/version-fox/vfox-plugins/plugins 85 | ::: 86 | 87 | ## 缓存 88 | 89 | `vfox` 默认会缓存`search`命令的结果, 以减少网络请求次数。默认缓存时间为`12h`。 90 | 91 | ::: warning 特殊值 92 | 93 | - `-1`: 永不过期 94 | - `0`: 不进行缓存 95 | ::: 96 | 97 | ```yaml 98 | cache: 99 | availableHookDuration: 12h # s 秒, m 分钟, h 小时 100 | ``` 101 | 102 | ::: tip 缓存文件路径 103 | `$HOME/.version-fox/plugins//available.cache` 104 | ::: 105 | 106 | ## Config 命令 107 | 108 | 设置,查看配置 109 | 110 | **用法** 111 | 112 | ```shell 113 | vfox config [] [] 114 | 115 | vfox config proxy.enable true 116 | vfox config proxy.url http://localhost:7890 117 | vfox config storage.sdkPath /tmp 118 | ``` 119 | 120 | `key`:配置项,以`.`分割。 121 | `value`:不传为查看配置项的值。 122 | 123 | **选项** 124 | 125 | - `-l, --list`:列出所有配置。 126 | - `-un, --unset`:删除一个配置。 127 | -------------------------------------------------------------------------------- /docs/usage/plugins-commands.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Plugins are how `vfox` knows to handle different tools like `Node.js`, `Java`, `Elixir` etc. 4 | 5 | See [Creating Plugins](../plugins/create/howto.md) for the plugin API used to support more tools. 6 | 7 | ## Available 8 | 9 | View all available plugins. 10 | 11 | **Usage** 12 | 13 | ```shell 14 | vfox available 15 | ``` 16 | 17 | ## Add 18 | Add a plugin from the official repository or a custom source. 19 | 20 | **Usage** 21 | 22 | ```shell 23 | vfox add [options] [...] 24 | ``` 25 | 26 | `plugin-name`: Plugin name, such as `nodejs`. You can install multiple plugins at once, separated by spaces. 27 | 28 | **Options** 29 | 30 | - `-a, --alias`: Set the plugin alias. 31 | - `-s, --source`: Install the plugin from the specified path (can be a remote file or a local file). 32 | 33 | 34 | ::: warning 35 | `--alias` and `--source` are not supported when adding multiple plugins. 36 | ::: 37 | 38 | **Examples** 39 | 40 | **Install plugin from the official repository** 41 | 42 | ```shell 43 | $ vfox add --alias node nodejs 44 | 45 | $ vfox add golang java nodejs 46 | ``` 47 | 48 | **Install custom plugin** 49 | 50 | ```shell 51 | $ vfox add --source https://github.com/version-fox/vfox-nodejs/releases/download/v0.0.5/vfox-nodejs_0.0.5.zip custom-node 52 | ``` 53 | 54 | ## Info 55 | 56 | View the SDK information installed locally. 57 | 58 | **Usage** 59 | 60 | ```shell 61 | vfox info 62 | vfox info @ 63 | vfox info [options] 64 | ``` 65 | 66 | `plugin-name`: Plugin name, such as `nodejs`. 67 | `version`: Specific version of the plugin. 68 | 69 | **Options** 70 | 71 | - `-f, --format`: Format the output using the given Go template. Available fields: 72 | - For plugin info: `Name`, `Version`, `Homepage`, `InstallPath`, `Description` 73 | - For version info: `Name`, `Version`, `Path` 74 | 75 | **Examples** 76 | 77 | **View plugin information** 78 | ```shell 79 | vfox info nodejs 80 | ``` 81 | 82 | **View specific version path** 83 | ```shell 84 | vfox info nodejs@20.0.0 85 | ``` 86 | 87 | **Format output with template** 88 | ```shell 89 | vfox info --format "{{.Homepage}}" nodejs 90 | vfox info --format "{{.InstallPath}}" nodejs 91 | vfox info --format "{{.Path}}" nodejs@20.0.0 92 | ``` 93 | 94 | ## Remove 95 | 96 | Remove the installed plugin. 97 | 98 | **Usage** 99 | 100 | ```shell 101 | vfox remove 102 | ``` 103 | 104 | ::: danger 105 | `vfox` will remove all versions of the runtime installed by the current plugin. 106 | ::: 107 | 108 | 109 | 110 | ## Update 111 | 112 | Update a specified or all plugin(s) 113 | 114 | **Usage** 115 | 116 | ```shell 117 | vfox update 118 | vfox update --all # update all installed plugins 119 | ``` 120 | 121 | -------------------------------------------------------------------------------- /internal/util/set_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestMapSet(t *testing.T) { 25 | s := NewSet[int]() 26 | 27 | s.Add(1) 28 | if !s.Contains(1) { 29 | t.Errorf("Expected set to contain 1") 30 | } 31 | 32 | s.Remove(1) 33 | if s.Contains(1) { 34 | t.Errorf("Expected set to not contain 1") 35 | } 36 | 37 | if s.Len() != 0 { 38 | t.Errorf("Expected set length to be 0, got %d", s.Len()) 39 | } 40 | } 41 | 42 | func TestOrderedSet(t *testing.T) { 43 | s := NewSortedSet[int]() 44 | 45 | s.Add(1) 46 | if !s.Contains(1) { 47 | t.Errorf("Expected set to contain 1") 48 | } 49 | 50 | s.Remove(1) 51 | if s.Contains(1) { 52 | t.Errorf("Expected set to not contain 1") 53 | } 54 | 55 | if s.Len() != 0 { 56 | t.Errorf("Expected set length to be 0, got %d", s.Len()) 57 | } 58 | 59 | for v := range s.Slice() { 60 | t.Errorf("Expected no iteration over set, but got %v", v) 61 | } 62 | } 63 | 64 | func TestSortedSetSort(t *testing.T) { 65 | s := NewSortedSet[string]() 66 | 67 | elements := []string{"ss23434444444jl2342342424s", "89999809898998bbb", "99234234234234aaa", "1232ssssssssssssssss414141fff"} 68 | 69 | for _, r := range elements { 70 | s.Add(r) 71 | } 72 | 73 | if !reflect.DeepEqual(s.Slice(), elements) { 74 | t.Errorf("Expected set to contain %v, got %v", elements, s.Slice()) 75 | } 76 | 77 | e := elements[0] 78 | elements = elements[1:] 79 | s.Remove(e) 80 | if !reflect.DeepEqual(s.Slice(), elements) { 81 | t.Errorf("Expected set to contain %v, got %v", elements, s.Slice()) 82 | } 83 | } 84 | 85 | func TestLoopRemoveSortedSet(t *testing.T) { 86 | s1 := NewSortedSet[int]() 87 | s2 := NewSortedSet[int]() 88 | s3 := NewSortedSet[int]() 89 | for i := 0; i < 10; i++ { 90 | s1.Add(i) 91 | if i < 5 { 92 | s2.Add(i) 93 | } else { 94 | s3.Add(i) 95 | } 96 | } 97 | for _, s := range s1.Slice() { 98 | if s2.Contains(s) { 99 | s1.Remove(s) 100 | } 101 | } 102 | for _, s := range s1.Slice() { 103 | if !s3.Contains(s) { 104 | t.Errorf("Expected set to equal %v, got %v", s3.Slice(), s1.Slice()) 105 | break 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cmd/commands/activate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package commands 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "strings" 24 | "text/template" 25 | 26 | "github.com/version-fox/vfox/internal" 27 | 28 | "github.com/urfave/cli/v3" 29 | "github.com/version-fox/vfox/internal/env" 30 | "github.com/version-fox/vfox/internal/logger" 31 | "github.com/version-fox/vfox/internal/shell" 32 | ) 33 | 34 | var Activate = &cli.Command{ 35 | Name: "activate", 36 | Hidden: true, 37 | Action: activateCmd, 38 | Category: CategorySDK, 39 | } 40 | 41 | func activateCmd(ctx context.Context, cmd *cli.Command) error { 42 | name := cmd.Args().First() 43 | if name == "" { 44 | return cli.Exit("shell name is required", 1) 45 | } 46 | manager := internal.NewSdkManager() 47 | defer manager.Close() 48 | 49 | sdkEnvs, err := manager.GlobalEnvKeys() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Note: This step must be the first. 55 | // the Paths will handle the path format of GitBash which is different from other shells. 56 | // So we need to set the env.HookFlag first to let the Paths know 57 | // which shell we are using. 58 | _ = os.Setenv(env.HookFlag, name) 59 | 60 | exportEnvs := sdkEnvs.ToExportEnvs() 61 | 62 | // Current shell is not for user use, it will be killed after the activation. 63 | // If we setup hook in this shell, it will cause all terminal launched by IDE could not be hooked properly. 64 | if !env.IsIDEEnvironmentResolution() { 65 | exportEnvs[env.HookFlag] = &name 66 | exportEnvs[internal.HookCurTmpPath] = &manager.PathMeta.CurTmpPath 67 | } 68 | 69 | logger.Debugf("export envs: %+v", exportEnvs) 70 | 71 | path := manager.PathMeta.ExecutablePath 72 | path = strings.Replace(path, "\\", "/", -1) 73 | s := shell.NewShell(name) 74 | if s == nil { 75 | return fmt.Errorf("unknown target shell %s", name) 76 | } 77 | 78 | exportStr := s.Export(exportEnvs) 79 | str, err := s.Activate( 80 | shell.ActivateConfig{ 81 | SelfPath: path, 82 | Args: cmd.Args().Tail(), 83 | }, 84 | ) 85 | if err != nil { 86 | return err 87 | } 88 | hookTemplate, err := template.New("hook").Parse(str) 89 | if err != nil { 90 | return nil 91 | } 92 | tmpCtx := struct { 93 | SelfPath string 94 | EnvContent string 95 | }{ 96 | SelfPath: path, 97 | EnvContent: exportStr, 98 | } 99 | return hookTemplate.Execute(cmd.Writer, tmpCtx) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Han Li and contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cmd 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/urfave/cli/v3" 25 | "github.com/version-fox/vfox/cmd/commands" 26 | "github.com/version-fox/vfox/internal" 27 | "github.com/version-fox/vfox/internal/logger" 28 | ) 29 | 30 | func Execute(args []string) { 31 | newCmd().Execute(args) 32 | } 33 | 34 | type cmd struct { 35 | app *cli.Command 36 | version string 37 | } 38 | 39 | func (c *cmd) Execute(args []string) { 40 | if err := c.app.Run(context.Background(), args); err != nil { 41 | fmt.Println(err) 42 | os.Exit(1) 43 | } 44 | } 45 | 46 | func newCmd() *cmd { 47 | version := internal.RuntimeVersion 48 | cli.VersionFlag = &cli.BoolFlag{ 49 | Name: "version", 50 | Aliases: []string{"v", "V"}, 51 | Usage: "print version", 52 | Action: func(ctx context.Context, cmd *cli.Command, b bool) error { 53 | println(version) 54 | return nil 55 | }, 56 | } 57 | 58 | app := &cli.Command{} 59 | app.EnableShellCompletion = true 60 | app.Name = "vfox" 61 | app.Usage = "vfox is a tool for runtime version management." 62 | app.UsageText = "vfox [command] [command options]" 63 | app.Copyright = "Copyright 2025 Han Li. All rights reserved." 64 | app.Version = version 65 | app.Description = "vfox is a cross-platform version manager, extendable via plugins. It allows you to quickly install and switch between different environment you need via the command line." 66 | app.Suggest = true 67 | app.ShellComplete = func(ctx context.Context, cmd *cli.Command) { 68 | for _, command := range cmd.Commands { 69 | _, _ = fmt.Fprintln(cmd.Writer, command.Name) 70 | } 71 | } 72 | 73 | debugFlags := &cli.BoolFlag{ 74 | Name: "debug", 75 | Usage: "show debug information", 76 | Action: func(ctx context.Context, cmd *cli.Command, b bool) error { 77 | logger.SetLevel(logger.DebugLevel) 78 | return nil 79 | }, 80 | } 81 | 82 | app.Flags = []cli.Flag{ 83 | debugFlags, 84 | } 85 | app.Commands = []*cli.Command{ 86 | commands.Info, 87 | commands.Install, 88 | commands.Current, 89 | commands.Use, 90 | commands.Unuse, 91 | commands.List, 92 | commands.Uninstall, 93 | commands.Available, 94 | commands.Search, 95 | commands.Update, 96 | commands.Upgrade, 97 | commands.Remove, 98 | commands.Add, 99 | commands.Activate, 100 | commands.Env, 101 | commands.Config, 102 | commands.Cd, 103 | } 104 | 105 | return &cmd{app: app, version: version} 106 | } 107 | -------------------------------------------------------------------------------- /internal/shell/nushell_test.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "reflect" 7 | "runtime" 8 | "slices" 9 | "testing" 10 | 11 | "github.com/version-fox/vfox/internal/env" 12 | ) 13 | 14 | func TestExport(t *testing.T) { 15 | sep := string(os.PathListSeparator) 16 | 17 | var pathVarName string 18 | if runtime.GOOS == "windows" { 19 | pathVarName = "Path" 20 | } else { 21 | pathVarName = "PATH" 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | envs env.Vars 27 | want nushellExportData 28 | }{ 29 | { 30 | "Empty", 31 | env.Vars{}, 32 | nushellExportData{ 33 | EnvsToSet: make(map[string]any), 34 | EnvsToUnset: make([]string, 0)}, 35 | }, 36 | { 37 | "SingleEnv", 38 | env.Vars{"FOO": newString("bar")}, 39 | nushellExportData{ 40 | EnvsToSet: map[string]any{"FOO": "bar"}, 41 | EnvsToUnset: make([]string, 0), 42 | }, 43 | }, 44 | { 45 | "MultipleEnvs", 46 | env.Vars{"FOO": newString("bar"), "BAZ": newString("qux")}, 47 | nushellExportData{ 48 | EnvsToSet: map[string]any{"FOO": "bar", "BAZ": "qux"}, 49 | EnvsToUnset: make([]string, 0), 50 | }, 51 | }, 52 | { 53 | "UnsetEnv", 54 | env.Vars{"FOO": nil}, 55 | nushellExportData{ 56 | EnvsToSet: make(map[string]any), 57 | EnvsToUnset: []string{"FOO"}, 58 | }, 59 | }, 60 | { 61 | "MixedEnvs", 62 | env.Vars{"FOO": newString("bar"), "BAZ": nil}, 63 | nushellExportData{ 64 | EnvsToSet: map[string]any{"FOO": "bar"}, 65 | EnvsToUnset: []string{"BAZ"}, 66 | }, 67 | }, 68 | { 69 | "MultipleUnsetEnvs", 70 | env.Vars{"FOO": nil, "BAZ": nil}, 71 | nushellExportData{ 72 | EnvsToSet: make(map[string]any), 73 | EnvsToUnset: []string{"FOO", "BAZ"}, 74 | }, 75 | }, 76 | { 77 | "PathEnv", 78 | env.Vars{"PATH": newString("/path1" + sep + "/path2")}, 79 | nushellExportData{ 80 | EnvsToSet: map[string]any{pathVarName: []any{"/path1", "/path2"}}, 81 | EnvsToUnset: make([]string, 0), 82 | }, 83 | }, 84 | { 85 | "PathAndOtherEnv", 86 | env.Vars{ 87 | "PATH": newString("/path1" + sep + "/path2" + sep + "/path3"), 88 | "FOO": newString("bar"), 89 | "BAZ": nil, 90 | }, 91 | nushellExportData{ 92 | EnvsToSet: map[string]any{pathVarName: []any{"/path1", "/path2", "/path3"}, "FOO": "bar"}, 93 | EnvsToUnset: []string{"BAZ"}, 94 | }, 95 | }, 96 | } 97 | 98 | for _, test := range tests { 99 | t.Run(test.name, func(t *testing.T) { 100 | runExportTest(t, test.envs, test.want) 101 | }) 102 | } 103 | } 104 | 105 | func runExportTest(t *testing.T, envs env.Vars, want nushellExportData) { 106 | n := nushell{} 107 | got := n.Export(envs) 108 | var gotData nushellExportData 109 | err := json.Unmarshal([]byte(got), &gotData) 110 | if err != nil { 111 | t.Errorf("%s: error unmarshaling export data - %v", t.Name(), err) 112 | return 113 | } 114 | 115 | slices.Sort(want.EnvsToUnset) 116 | slices.Sort(gotData.EnvsToUnset) 117 | if !reflect.DeepEqual(gotData, want) { 118 | t.Errorf("%s: export data mismatch - want %v, got %v", t.Name(), want, gotData) 119 | } 120 | } 121 | 122 | func newString(s string) *string { 123 | return &s 124 | } 125 | -------------------------------------------------------------------------------- /internal/plugin/luai/module/string/string_test.go: -------------------------------------------------------------------------------- 1 | package string 2 | 3 | import ( 4 | lua "github.com/yuin/gopher-lua" 5 | "testing" 6 | ) 7 | 8 | func TestSplit(t *testing.T) { 9 | const str = ` 10 | local strings = require("vfox.strings") 11 | local str_parts = strings.split("hello world", " ") 12 | assert(type(str_parts) == 'table') 13 | assert(#str_parts == 2, string.format("%d ~= 2", #str_parts)) 14 | assert(str_parts[1] == "hello", string.format("%s ~= hello", str_parts[1])) 15 | assert(str_parts[2] == "world", string.format("%s ~= world", str_parts[2])) 16 | ` 17 | eval(str, t) 18 | } 19 | func TestHasPrefix(t *testing.T) { 20 | const str = ` 21 | local strings = require("vfox.strings") 22 | assert(strings.has_prefix("hello world", "hello"), [[not strings.has_prefix("hello")]]) 23 | ` 24 | eval(str, t) 25 | } 26 | func TestHasSuffix(t *testing.T) { 27 | const str = ` 28 | local strings = require("vfox.strings") 29 | assert(strings.has_suffix("hello world", "world"), [[not strings.has_suffix("world")]]) 30 | ` 31 | eval(str, t) 32 | } 33 | 34 | func TestTrim(t *testing.T) { 35 | const str = ` 36 | local strings = require("vfox.strings") 37 | assert(strings.trim("hello world", "world") == "hello ", "strings.trim()") 38 | assert(strings.trim("hello world", "hello ") == "world", "strings.trim()") 39 | assert(strings.trim_prefix("hello world", "hello ") == "world", "strings.trim()") 40 | assert(strings.trim_suffix("hello world", "hello ") == "hello world", "strings.trim()") 41 | ` 42 | eval(str, t) 43 | } 44 | 45 | func TestContains(t *testing.T) { 46 | const str = ` 47 | local strings = require("vfox.strings") 48 | assert(strings.contains("hello world", "hello ") == true, "strings.contains()") 49 | ` 50 | eval(str, t) 51 | } 52 | 53 | func TestJoin(t *testing.T) { 54 | const str = ` 55 | local strings = require("vfox.strings") 56 | local str = strings.join({"1",3,"4"},";") 57 | assert(str == "1;3;4", "strings.join()") 58 | ` 59 | eval(str, t) 60 | } 61 | 62 | func TestTrimSpace(t *testing.T) { 63 | const str = ` 64 | local strings = require("vfox.strings") 65 | tests = { 66 | { 67 | name = "string with trailing whitespace", 68 | input = "foo bar ", 69 | expected = "foo bar", 70 | }, 71 | { 72 | name = "string with leading whitespace", 73 | input = " foo bar", 74 | expected = "foo bar", 75 | }, 76 | { 77 | name = "string with leading and trailing whitespace", 78 | input = " foo bar ", 79 | expected = "foo bar", 80 | }, 81 | { 82 | name = "string with no leading or trailing whitespace", 83 | input = "foo bar", 84 | expected = "foo bar", 85 | }, 86 | } 87 | 88 | for _, tt in ipairs(tests) do 89 | got = strings.trim_space(tt.input) 90 | assert(got == tt.expected, string.format([[expected "%s"; got "%s"]], expected, got)) 91 | end 92 | ` 93 | eval(str, t) 94 | } 95 | 96 | func eval(str string, t *testing.T) { 97 | s := lua.NewState() 98 | defer s.Close() 99 | 100 | Preload(s) 101 | if err := s.DoString(str); err != nil { 102 | t.Error(err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /internal/shim/shim_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | /* 4 | * Copyright 2025 Han Li and contributors 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package shim 20 | 21 | import ( 22 | _ "embed" 23 | "fmt" 24 | "os" 25 | "path/filepath" 26 | 27 | "github.com/version-fox/vfox/internal/logger" 28 | "github.com/version-fox/vfox/internal/util" 29 | ) 30 | 31 | const shimFileContent = ` 32 | path = "%s" 33 | ` 34 | 35 | const cmdShimContent = ` 36 | @echo off 37 | call "%s" %%* 38 | ` 39 | const ps1ShimContent = ` 40 | param ( 41 | [Parameter(Position=0, ValueFromRemainingArguments=$true)] 42 | $params 43 | ) 44 | 45 | & '%s' @params 46 | ` 47 | 48 | //go:embed binary/shim.exe 49 | var shim []byte 50 | 51 | // Clear removes the generated shim. 52 | func (s *Shim) Clear() (err error) { 53 | filename := filepath.Base(s.BinaryPath) 54 | ext := filepath.Ext(filename) 55 | shimName := filename[:len(filename)-len(ext)] + ".shim" 56 | shimBinary := filepath.Join(s.OutputPath, filename) 57 | 58 | if util.FileExists(shimBinary) { 59 | if err = os.Remove(shimBinary); err != nil { 60 | return 61 | } 62 | } 63 | shimFile := filepath.Join(s.OutputPath, shimName) 64 | if util.FileExists(shimFile) { 65 | if err = os.Remove(shimFile); err != nil { 66 | return 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | // Generate generates the shim. 73 | func (s *Shim) Generate() error { 74 | if err := s.Clear(); err != nil { 75 | logger.Debugf("Clear shim failed: %s", err) 76 | return err 77 | } 78 | filename := filepath.Base(s.BinaryPath) 79 | stat, err := os.Stat(s.BinaryPath) 80 | if err != nil { 81 | return err 82 | } 83 | targetPath := filepath.Join(s.OutputPath, filename) 84 | ext := filepath.Ext(filename) 85 | if ext == ".cmd" { 86 | if err = os.WriteFile(targetPath, []byte(fmt.Sprintf(cmdShimContent, s.BinaryPath)), stat.Mode()); err != nil { 87 | return fmt.Errorf("failed to generate shim: %w", err) 88 | } 89 | return nil 90 | } else if ext == ".ps1" { 91 | if err = os.WriteFile(targetPath, []byte(fmt.Sprintf(ps1ShimContent, s.BinaryPath)), stat.Mode()); err != nil { 92 | return fmt.Errorf("failed to generate shim: %w", err) 93 | } 94 | return nil 95 | } 96 | logger.Debugf("Write shim binary to %s", targetPath) 97 | if err = os.WriteFile(targetPath, shim, stat.Mode()); err != nil { 98 | return fmt.Errorf("failed to generate shim: %w", err) 99 | } 100 | shimName := filename[:len(filename)-len(ext)] + ".shim" 101 | shimFile := filepath.Join(s.OutputPath, shimName) 102 | logger.Debugf("Write shim file to %s", shimFile) 103 | if err = os.WriteFile(shimFile, []byte(fmt.Sprintf(shimFileContent, s.BinaryPath)), stat.Mode()); err != nil { 104 | return fmt.Errorf("failed to generate shim: %w", err) 105 | } 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /docs/guides/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | If you switch between development projects which expect different environments, specifically different runtime versions or ambient libraries, 4 | or you are tired of all kinds of cumbersome environment configurations, `vfox` is the ideal choice for you. 5 | 6 | `vfox` is a cross-platform, extensible version manager. It supports **native Windows**, and of course **Unix-like**! 7 | With it, you can **quickly install and switch** different environment. 8 | 9 | It saves all tool version information in a file named `.tool-versions`, so you can share this information in your 10 | project to ensure that everyone on your team is using the same tool versions. 11 | 12 | Traditional work requires multiple cli version managers, each with its own API, configuration files, and 13 | implementation (e.g., `$PATH` operations, shims, environment variables, etc.). `vfox` provides a single interactive way 14 | and configuration file to simplify the development workflow and can be extended to all tools and runtime environments 15 | through a simple plugin interface. 16 | 17 | ## Why use vfox? 18 | 19 | - **cross-platform support** (**Windows**, Linux, macOS) 20 | - **consistent commands** to manage all your languages 21 | - supports **different versions for different projects, different shells, and globally**. 22 | - simple **plugin system** to add support for your runtime of choice 23 | - **automatically switches** runtime versions as you traverse your project 24 | - support for existing config files `.node-version`, `.nvmrc`, `.sdkmanrc` for easy migration 25 | - shell completion available for common shells (Bash, ZSH, Powershell, Clink) 26 | - **Faster than `asdf-vm`**, and provides simpler commands and true cross-platform unification. 27 | See [Comparison to asdf](../misc/vs-asdf.md)。 28 | 29 | ## Supported Shell 30 | 31 | | Shell | Support | Note | 32 | |------------|---------|----------------------------------------------------------------------------------| 33 | | Powershell | ✅ | | 34 | | GitBash | ✅ | [Issue](./faq.md#why-can-t-i-select-when-use-use-and-search-commands-in-gitbash) | 35 | | Bash | ✅ | | 36 | | ZSH | ✅ | | 37 | | Fish | ✅ | | 38 | | CMD | ✅ | Only Support `Global` Scope. Not Recommend!!! | 39 | | Clink | ✅ | | 40 | | Cmder | ✅ | | 41 | | Nushell | ✅ | | 42 | 43 | 44 | 45 | ## Contributors 46 | 47 | 48 | > [!TIP] 49 | > Thanks to the following contributors for their contributions.🎉🎉🙏🙏 50 | 51 | #### [vfox](https://github.com/version-fox/vfox) 52 | 53 | ![plugins](https://contrib.rocks/image?repo=version-fox/vfox) 54 | 55 | #### [Public Registry](https://github.com/version-fox/vfox-plugins) 56 | 57 | ![plugins](https://contrib.rocks/image?repo=version-fox/vfox-plugins)) 58 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | main() { 4 | # Detect if running in Termux 5 | IS_TERMUX=false 6 | case "${HOME:-}" in 7 | *com.termux*) 8 | IS_TERMUX=true 9 | echo "Detected Termux environment" 10 | ;; 11 | esac 12 | 13 | # Set installation directory and sudo command based on environment 14 | if [ "$IS_TERMUX" = true ]; then 15 | INSTALL_DIR="${PREFIX}/bin" 16 | SUDO_CMD="" 17 | else 18 | INSTALL_DIR="/usr/local/bin" 19 | # Check if sudo is available 20 | if command -v sudo &> /dev/null; then 21 | SUDO_CMD="sudo" 22 | else 23 | SUDO_CMD="" 24 | fi 25 | fi 26 | 27 | # Check if curl or wget is installed 28 | if command -v curl &> /dev/null 29 | then 30 | DOWNLOAD_CMD="curl -LO" 31 | elif command -v wget &> /dev/null 32 | then 33 | DOWNLOAD_CMD="wget" 34 | else 35 | echo "Neither curl nor wget was found. Please install one of them and try again." 36 | exit 1 37 | fi 38 | 39 | # Get the latest version 40 | if [ -n "${GITHUB_TOKEN}" ]; then 41 | API_RESPONSE=$(curl --silent --header "Authorization: Bearer ${GITHUB_TOKEN}" "https://api.github.com/repos/version-fox/vfox/releases/latest") 42 | else 43 | API_RESPONSE=$(curl --silent "https://api.github.com/repos/version-fox/vfox/releases/latest") 44 | fi 45 | 46 | # Check if the response contains an error message 47 | if echo "$API_RESPONSE" | grep -q '"message":'; then 48 | echo "GitHub API Error:" 49 | echo "$API_RESPONSE" 50 | exit 1 51 | fi 52 | 53 | VERSION=$(echo "$API_RESPONSE" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) 54 | if [ -z "$VERSION" ]; then 55 | echo "Failed to get the latest version. Please check your network connection and try again." 56 | exit 1 57 | fi 58 | echo "Installing vfox v$VERSION ..." 59 | 60 | # Check if the OS is supported 61 | OS_TYPE=$(uname -s | tr '[:upper:]' '[:lower:]') 62 | if [ "$OS_TYPE" = "darwin" ]; then 63 | OS_TYPE="macos" 64 | fi 65 | 66 | ARCH_TYPE=$(uname -m) 67 | 68 | if [ "$ARCH_TYPE" = "arm64" ]; then 69 | ARCH_TYPE="aarch64" 70 | elif [ "$ARCH_TYPE" = "loongarch64" ]; then 71 | ARCH_TYPE="loong64" 72 | fi 73 | 74 | FILENAME="vfox_${VERSION}_${OS_TYPE}_${ARCH_TYPE}" 75 | TAR_FILE="${FILENAME}.tar.gz" 76 | 77 | echo https://github.com/version-fox/vfox/releases/download/v$VERSION/$TAR_FILE 78 | $DOWNLOAD_CMD https://github.com/version-fox/vfox/releases/download/v$VERSION/$TAR_FILE 79 | 80 | 81 | tar -zxvf $TAR_FILE 82 | if [ $? -ne 0 ]; then 83 | echo "Failed to extract vfox binary. Please check if the downloaded file is a valid tar.gz file." 84 | exit 1 85 | fi 86 | 87 | # Create installation directory 88 | $SUDO_CMD mkdir -p "$INSTALL_DIR" 89 | if [ $? -ne 0 ]; then 90 | echo "Failed to create $INSTALL_DIR directory. Please check your permissions and try again." 91 | exit 1 92 | fi 93 | 94 | if [ -d "$INSTALL_DIR" ]; then 95 | $SUDO_CMD mv "${FILENAME}/vfox" "$INSTALL_DIR" 96 | else 97 | echo "$INSTALL_DIR is not a directory. Please make sure it is a valid directory path." 98 | exit 1 99 | fi 100 | 101 | if [ $? -ne 0 ]; then 102 | echo "Failed to move vfox to $INSTALL_DIR. Please check your permissions and try again." 103 | exit 1 104 | fi 105 | rm $TAR_FILE 106 | rm -rf $FILENAME 107 | echo "vfox installed successfully!" 108 | } 109 | 110 | main "$@" 111 | -------------------------------------------------------------------------------- /docs/zh-hans/usage/core-commands.md: -------------------------------------------------------------------------------- 1 | # 核心命令 2 | 3 | ## Search 4 | 5 | 获取指定 SDK 所有可用的运行时版本。 6 | 7 | **用法** 8 | 9 | ```shell 10 | vfox search [...optionArgs] 11 | ``` 12 | 13 | `sdk-name`: 运行时名称, 如`nodejs`、`custom-node`。 14 | `optionArgs`: 搜索命令的附加参数。注意:是否支持取决于插件。 15 | 16 | ::: warning 缓存 17 | 18 | `vfox`会缓存`search`的结果, 默认缓存时间为`12h`。 19 | 20 | 如果你想禁用,可以通过`vfox config`命令进行配置。 21 | ```shell 22 | vfox config cache.availableHookDuration 0 23 | ``` 24 | 25 | 其余操作, 请查看[配置#缓存](../guides/configuration.md#%E7%BC%93%E5%AD%98)。 26 | 27 | ::: 28 | 29 | ::: tip 快捷安装 30 | 选择目标版本, 回车即可快速安装。 31 | ::: 32 | 33 | ::: tip 自动安装 34 | 如果本地没有安装 SDK,`search`命令会从远端仓库检索插件并安装到本地。 35 | ::: 36 | 37 | ## Install 38 | 39 | 将指定的 SDK 版本安装到您的计算机并缓存以供将来使用。 40 | 41 | **用法** 42 | 43 | ```shell 44 | vfox install @ 45 | 46 | vfox i @ 47 | ``` 48 | 49 | `sdk-name`: SDK 名称 50 | 51 | `version`: 需要安装的版本号 52 | 53 | **选项** 54 | 55 | - `-a, --all`: 安装 .tool-versions 中记录的所有 SDK 版本 56 | - `-y, --yes`: 直接安装,跳过确认提示 57 | 58 | ::: tip 自动安装 59 | 你可以一次性安装多个 SDK,通过空格分隔。 60 | 61 | ```shell 62 | vfox install nodejs@20 golang ... 63 | ``` 64 | 65 | ::: 66 | 67 | ::: tip 68 | 直接安装,跳过确认提示 69 | 70 | ```shell 71 | vfox install --yes nodejs@20 72 | vfox install --yes --all 73 | ``` 74 | 75 | ::: 76 | 77 | ## Use 78 | 79 | 设置运行时版本 80 | 81 | **用法** 82 | 83 | ```shell 84 | vfox use [options] [@] 85 | 86 | vfox u [options] [@] 87 | ``` 88 | 89 | `sdk-name`: SDK 名称 90 | 91 | `version`[可选]: 使用 指定版本运行时。如不传, 则下拉选择。 92 | 93 | **选项** 94 | 95 | - `-g, --global`: 全局生效 96 | - `-p, --project`: 当前目录下生效 97 | - `-s, --session`: 当前 Shell 会话内生效 98 | 99 | ::: tip 默认作用域 100 | `Windows`: 默认`Global`作用域 101 | 102 | `Unix-like`: 默认`Session`作用域 103 | 104 | ::: 105 | 106 | ## Unuse 107 | 108 | 从指定作用域取消设置运行时版本 109 | 110 | **用法** 111 | 112 | ```shell 113 | vfox unuse [options] 114 | ``` 115 | 116 | `sdk-name`: SDK 名称 117 | 118 | **选项** 119 | 120 | - `-g, --global`: 从全局作用域移除 121 | - `-p, --project`: 从项目作用域移除(当前目录) 122 | - `-s, --session`: 从会话作用域移除(当前 Shell 会话) 123 | 124 | ::: tip 默认作用域 125 | `Windows`: 默认`Global`作用域 126 | 127 | `Unix-like`: 默认`Session`作用域 128 | ::: 129 | 130 | ::: warning 效果 131 | 使用 `unuse` 后,SDK 将不再在指定作用域中处于活动状态。如果 SDK 在其他作用域中配置,那些将根据 vfox 的作用域层次结构优先生效(Session > Project > Global)。 132 | ::: 133 | 134 | ## Uninstall 135 | 136 | 卸载指定版本的 SDK。 137 | 138 | **用法** 139 | 140 | ```shell 141 | vfox uninstall @ 142 | vfox un @ 143 | ``` 144 | 145 | `sdk-name`: SDK 名 146 | 147 | `version`: 具体版本号 148 | 149 | ## List 150 | 151 | 查看当前已安装的所有 SDK 版本。 152 | 153 | **用法** 154 | 155 | ```shell 156 | vfox list [] 157 | 158 | vfox ls[] 159 | ``` 160 | 161 | `sdk-name`: SDK 名称, 不传展示所有。 162 | 163 | ## Current 164 | 165 | 查看当前 SDK 的版本。 166 | 167 | **用法** 168 | 169 | ```shell 170 | vfox current [] 171 | vfox c 172 | ``` 173 | 174 | ## Cd 175 | 176 | 在 `VFOX_HOME` 或 SDK 目录下启动 shell。 177 | 178 | **用法** 179 | 180 | ```shell 181 | vfox cd [options] [] 182 | ``` 183 | 184 | `sdk-name`: SDK 名称, 不传默认为 `VFOX_HOME`。 185 | 186 | **选项** 187 | 188 | - `-p, --plugin`: 在插件目录下启动 shell。 189 | 190 | 191 | ## Upgrade 192 | 193 | 升级 `vfox` 到最新版本。 194 | 195 | **用法** 196 | 197 | ```shell 198 | vfox upgrade 199 | ``` 200 | --------------------------------------------------------------------------------