├── 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 | 
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 | 
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 |
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 |
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 | 
49 |
50 | #### [插件仓库](https://github.com/version-fox/vfox-plugins)
51 |
52 | )
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 | 
54 |
55 | #### [Public Registry](https://github.com/version-fox/vfox-plugins)
56 |
57 | )
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 |
--------------------------------------------------------------------------------