diff --git a/.gitignore b/.gitignore index 53bc389b..1700e1e1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ coverage.out **/available.cache + +vfox \ No newline at end of file diff --git a/README.md b/README.md index 64f18c16..347737aa 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,11 @@ [![GitHub Release](https://img.shields.io/github/v/release/version-fox/vfox?display_name=tag&style=for-the-badge)](https://github.com/version-fox/vfox/releases) [![Discord](https://img.shields.io/discord/1191981003204477019?style=for-the-badge&logo=discord)](https://discord.gg/85c8ptYgb7) -[[English]](./README.md) [[中文文档]](./README_CN.md) +[[English]](./README.md) [[中文文档]](./README_CN.md) If you **switch between development projects which expect different environments**, specifically different runtime versions or ambient libraries, or **you are tired of all kinds of cumbersome environment configurations**, `vfox` is the ideal choice for you. + ## Introduction **`vfox` is a cross-platform version manager(similar to `nvm`, `fvm`, `sdkman`, `asdf-vm`, etc.), extendable via plugins**. It allows you to quickly install @@ -57,13 +58,14 @@ if (-not (Test-Path -Path $PROFILE)) { New-Item -Type File -Path $PROFILE -Force # 3. copy internal/shell/clink_vfox.lua to script path # For Nushell: -vfox activate nushell | save --append $nu.config-path +vfox activate nushell $nu.default-config-dir | save --append $nu.config-path ``` > Remember to restart your shell to apply the changes. #### 3. Add an SDK plugin -```bash + +```bash $ vfox add nodejs ``` @@ -93,8 +95,8 @@ Our future plans and high priority features and enhancements are: - Introducing plugin templates to facilitate multi-file plugin development. - Establishing a global registry (similar to `NPM Registry` or `Scoop Main Bucket`) to provide a unified entry point for plugin distribution. - Decomposing the existing plugin repository into individual repositories, one for each plugin. -- [X] Allowing the switching of registry addresses. -- [X] Plugin capabilities: Parsing legacy configuration files, such as `.nvmrc`, `.node-version`, `.sdkmanrc`, etc. +- [x] Allowing the switching of registry addresses. +- [x] Plugin capabilities: Parsing legacy configuration files, such as `.nvmrc`, `.node-version`, `.sdkmanrc`, etc. - [ ] Plugin capabilities: Allowing plugins to load installed runtimes and provide information about the runtime. ## Available Plugins @@ -126,7 +128,6 @@ Plugin Contributions, please go to [Public Registry](https://github.com/version- ## Thanks - > Thanks JetBrains for the free open source license. :) @@ -139,4 +140,3 @@ Plugin Contributions, please go to [Public Registry](https://github.com/version- [Apache 2.0 license](./LICENSE) - Copyright (C) 2024 Han Li and [contributors](https://github.com/version-fox/vfox/graphs/contributors) - diff --git a/cmd/commands/activate.go b/cmd/commands/activate.go index 7d05e1b4..6a7ef4da 100644 --- a/cmd/commands/activate.go +++ b/cmd/commands/activate.go @@ -22,8 +22,6 @@ import ( "strings" "text/template" - "github.com/version-fox/vfox/internal/toolset" - "github.com/version-fox/vfox/internal" "github.com/urfave/cli/v2" @@ -46,26 +44,7 @@ func activateCmd(ctx *cli.Context) error { manager := internal.NewSdkManager() defer manager.Close() - workToolVersion, err := toolset.NewToolVersion(manager.PathMeta.WorkingDirectory) - if err != nil { - return err - } - - if err = manager.ParseLegacyFile(func(sdkname, version string) { - if _, ok := workToolVersion.Record[sdkname]; !ok { - workToolVersion.Record[sdkname] = version - } - }); err != nil { - return err - } - homeToolVersion, err := toolset.NewToolVersion(manager.PathMeta.HomePath) - if err != nil { - return err - } - sdkEnvs, err := manager.EnvKeys(toolset.MultiToolVersions{ - workToolVersion, - homeToolVersion, - }, internal.ShellLocation) + sdkEnvs, err := manager.FullEnvKeys() if err != nil { return err } @@ -91,7 +70,12 @@ func activateCmd(ctx *cli.Context) error { return fmt.Errorf("unknown target shell %s", name) } exportStr := s.Export(exportEnvs) - str, err := s.Activate() + str, err := s.Activate( + shell.ActivateConfig{ + SelfPath: path, + Args: ctx.Args().Tail(), + }, + ) if err != nil { return err } diff --git a/cmd/commands/env.go b/cmd/commands/env.go index 49e4cafb..b71399da 100644 --- a/cmd/commands/env.go +++ b/cmd/commands/env.go @@ -19,14 +19,12 @@ package commands import ( "encoding/json" "fmt" + "github.com/urfave/cli/v2" "github.com/version-fox/vfox/internal" - "github.com/version-fox/vfox/internal/cache" "github.com/version-fox/vfox/internal/env" - "github.com/version-fox/vfox/internal/logger" "github.com/version-fox/vfox/internal/shell" "github.com/version-fox/vfox/internal/toolset" - "path/filepath" ) var Env = &cli.Command{ @@ -47,6 +45,10 @@ var Env = &cli.Command{ Aliases: []string{"j"}, Usage: "output json format", }, + &cli.BoolFlag{ + Name: "full", + Usage: "output full env", + }, }, Action: envCmd, Category: CategorySDK, @@ -57,8 +59,10 @@ func envCmd(ctx *cli.Context) error { return outputJSON() } else if ctx.IsSet("cleanup") { return cleanTmp() + } else if ctx.IsSet("full") { + return envFlag(ctx, "full") } else { - return envFlag(ctx) + return envFlag(ctx, "cwd") } } @@ -109,7 +113,7 @@ func cleanTmp() error { return nil } -func envFlag(ctx *cli.Context) error { +func envFlag(ctx *cli.Context, mode string) error { shellName := ctx.String("shell") if shellName == "" { return cli.Exit("shell name is required", 1) @@ -120,13 +124,19 @@ func envFlag(ctx *cli.Context) error { } manager := internal.NewSdkManager() defer manager.Close() + var sdkEnvs internal.SdkEnvs + var err error + if mode == "full" { + sdkEnvs, err = manager.FullEnvKeys() + } else { + sdkEnvs, err = manager.AggregateEnvKeys() + } - sdkEnvs, err := aggregateEnvKeys(manager) if err != nil { return err } - if len(sdkEnvs) == 0 && shellName != "nushell" { + if len(sdkEnvs) == 0 { return nil } @@ -145,63 +155,3 @@ func envFlag(ctx *cli.Context) error { fmt.Println(exportStr) return nil } - -func aggregateEnvKeys(manager *internal.Manager) (internal.SdkEnvs, error) { - workToolVersion, err := toolset.NewToolVersion(manager.PathMeta.WorkingDirectory) - if err != nil { - return nil, err - } - - if err = manager.ParseLegacyFile(func(sdkname, version string) { - if _, ok := workToolVersion.Record[sdkname]; !ok { - workToolVersion.Record[sdkname] = version - } - }); err != nil { - return nil, err - } - - curToolVersion, err := toolset.NewToolVersion(manager.PathMeta.CurTmpPath) - if err != nil { - return nil, err - } - defer curToolVersion.Save() - - // Add the working directory to the first - tvs := toolset.MultiToolVersions{workToolVersion, curToolVersion} - - flushCache, err := cache.NewFileCache(filepath.Join(manager.PathMeta.CurTmpPath, "flush_env.cache")) - if err != nil { - return nil, err - } - defer flushCache.Close() - - var sdkEnvs []*internal.SdkEnv - - tvs.FilterTools(func(name, version string) bool { - if lookupSdk, err := manager.LookupSdk(name); err == nil { - vv, ok := flushCache.Get(name) - if ok && string(vv) == version { - logger.Debugf("Hit cache, skip flush environment, %s@%s\n", name, version) - return true - } else { - logger.Debugf("No hit cache, name: %s cache: %s, expected: %s \n", name, string(vv), version) - } - v := internal.Version(version) - if keys, err := lookupSdk.EnvKeys(v, internal.ShellLocation); err == nil { - flushCache.Set(name, cache.Value(version), cache.NeverExpired) - - sdkEnvs = append(sdkEnvs, &internal.SdkEnv{ - Sdk: lookupSdk, Env: keys, - }) - - // If we encounter a .tool-versions file, it is valid for the entire shell session, - // unless we encounter the next .tool-versions file or manually switch to the use command. - curToolVersion.Record[name] = version - return true - } - } - return false - }) - - return sdkEnvs, nil -} diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md index 5dc5baa1..7a27c06f 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -132,7 +132,7 @@ y ::: details Nushell ```shell -vfox activate nushell | save --append $nu.config-path +vfox activate nushell $nu.default-config-dir | save --append $nu.config-path ``` ::: diff --git a/internal/manager.go b/internal/manager.go index 9eb829a7..afc403fa 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -34,6 +34,7 @@ import ( "github.com/mitchellh/go-ps" "github.com/pterm/pterm" "github.com/urfave/cli/v2" + "github.com/version-fox/vfox/internal/cache" "github.com/version-fox/vfox/internal/config" "github.com/version-fox/vfox/internal/env" "github.com/version-fox/vfox/internal/logger" @@ -69,6 +70,89 @@ type Manager struct { Config *config.Config } +func (m *Manager) FullEnvKeys() (SdkEnvs, error) { + workToolVersion, err := toolset.NewToolVersion(m.PathMeta.WorkingDirectory) + if err != nil { + return nil, err + } + + if err = m.ParseLegacyFile(func(sdkname, version string) { + if _, ok := workToolVersion.Record[sdkname]; !ok { + workToolVersion.Record[sdkname] = version + } + }); err != nil { + return nil, err + } + homeToolVersion, err := toolset.NewToolVersion(m.PathMeta.HomePath) + if err != nil { + return nil, err + } + return m.EnvKeys(toolset.MultiToolVersions{ + workToolVersion, + homeToolVersion, + }, ShellLocation) +} + +func (m *Manager) AggregateEnvKeys() (SdkEnvs, error) { + workToolVersion, err := toolset.NewToolVersion(m.PathMeta.WorkingDirectory) + if err != nil { + return nil, err + } + + if err = m.ParseLegacyFile(func(sdkname, version string) { + if _, ok := workToolVersion.Record[sdkname]; !ok { + workToolVersion.Record[sdkname] = version + } + }); err != nil { + return nil, err + } + + curToolVersion, err := toolset.NewToolVersion(m.PathMeta.CurTmpPath) + if err != nil { + return nil, err + } + defer curToolVersion.Save() + + // Add the working directory to the first + tvs := toolset.MultiToolVersions{workToolVersion, curToolVersion} + + flushCache, err := cache.NewFileCache(filepath.Join(m.PathMeta.CurTmpPath, "flush_env.cache")) + if err != nil { + return nil, err + } + defer flushCache.Close() + + var sdkEnvs []*SdkEnv + + tvs.FilterTools(func(name, version string) bool { + if lookupSdk, err := m.LookupSdk(name); err == nil { + vv, ok := flushCache.Get(name) + if ok && string(vv) == version { + logger.Debugf("Hit cache, skip flush environment, %s@%s\n", name, version) + return true + } else { + logger.Debugf("No hit cache, name: %s cache: %s, expected: %s \n", name, string(vv), version) + } + v := Version(version) + if keys, err := lookupSdk.EnvKeys(v, ShellLocation); err == nil { + flushCache.Set(name, cache.Value(version), cache.NeverExpired) + + sdkEnvs = append(sdkEnvs, &SdkEnv{ + Sdk: lookupSdk, Env: keys, + }) + + // If we encounter a .tool-versions file, it is valid for the entire shell session, + // unless we encounter the next .tool-versions file or manually switch to the use command. + curToolVersion.Record[name] = version + return true + } + } + return false + }) + + return sdkEnvs, nil +} + func (m *Manager) EnvKeys(tvs toolset.MultiToolVersions, location Location) (SdkEnvs, error) { var sdkEnvs SdkEnvs tools := make(map[string]struct{}) diff --git a/internal/shell/bash.go b/internal/shell/bash.go index e9232ece..8c6898ab 100644 --- a/internal/shell/bash.go +++ b/internal/shell/bash.go @@ -50,7 +50,7 @@ type bash struct{} var Bash = bash{} -func (b bash) Activate() (string, error) { +func (b bash) Activate(config ActivateConfig) (string, error) { return bashHook, nil } diff --git a/internal/shell/clink.go b/internal/shell/clink.go index ec129b02..f23f85de 100644 --- a/internal/shell/clink.go +++ b/internal/shell/clink.go @@ -31,7 +31,7 @@ type clink struct{} var Clink = clink{} -func (b clink) Activate() (string, error) { +func (b clink) Activate(config ActivateConfig) (string, error) { return clinkHook, nil } diff --git a/internal/shell/fish.go b/internal/shell/fish.go index cce5cf0a..edf0b2b3 100644 --- a/internal/shell/fish.go +++ b/internal/shell/fish.go @@ -62,7 +62,7 @@ function cleanup_on_exit --on-process-exit %self end; ` -func (sh fish) Activate() (string, error) { +func (sh fish) Activate(config ActivateConfig) (string, error) { return fishHook, nil } diff --git a/internal/shell/nushell.go b/internal/shell/nushell.go index aec059c0..fb3a3e9a 100644 --- a/internal/shell/nushell.go +++ b/internal/shell/nushell.go @@ -3,38 +3,46 @@ package shell import ( "encoding/json" "fmt" - "github.com/version-fox/vfox/internal/env" + "os" "path/filepath" + "strings" + + "github.com/version-fox/vfox/internal/env" ) type nushell struct{} var Nushell = nushell{} -const nushellConfig = env.Newline + - "# vfox configuration" + env.Newline + - "export-env {" + env.Newline + - " def --env updateVfoxEnvironment [] {" + env.Newline + - " let envData = (^'{{.SelfPath}}' env -s nushell | from json)" + env.Newline + - " load-env $envData.envsToSet" + env.Newline + - " hide-env ...$envData.envsToUnset" + env.Newline + - " }" + env.Newline + - " $env.config = ($env.config | upsert hooks.pre_prompt {" + env.Newline + - " let currentValue = ($env.config | get -i hooks.pre_prompt)" + env.Newline + - " if $currentValue == null {" + env.Newline + - " [{updateVfoxEnvironment}]" + env.Newline + - " } else {" + env.Newline + - " $currentValue | append {updateVfoxEnvironment}" + env.Newline + - " }" + env.Newline + - " })" + env.Newline + - " $env.__VFOX_SHELL = 'nushell'" + env.Newline + - " $env.__VFOX_PID = $nu.pid" + env.Newline + - " ^'{{.SelfPath}}' env --cleanup | ignore" + env.Newline + - " updateVfoxEnvironment" + env.Newline + - "}" + env.Newline - -// Activate implements shell.Activate by returning a script to be placed in the Nushell configuration file. This script -// does the following: +const nushellConfig = ` +# vfox configuration +# this make sure this configuration is up to date when you open a new shell +^'{{.SelfPath}}' activate nushell $nu.default-config-dir | ignore + +export-env { + def --env updateVfoxEnvironment [] { + let envData = (^'{{.SelfPath}}' env -s nushell --full | from json) + load-env $envData.envsToSet + hide-env ...$envData.envsToUnset + } + $env.config = ($env.config | upsert hooks.pre_prompt { + let currentValue = ($env.config | get -i hooks.pre_prompt) + if $currentValue == null { + [{updateVfoxEnvironment}] + } else { + $currentValue | append {updateVfoxEnvironment} + } + }) + $env.__VFOX_SHELL = 'nushell' + $env.__VFOX_PID = $nu.pid + ^'{{.SelfPath}}' env --cleanup | ignore + updateVfoxEnvironment +} +` + +// We create a `vfox.nu“ in the `$nu.default-config-dir“ +// Activate implements shell.Activate will generate a script to the `vfox.nu` file. +// This script does the following: // // 1. Sets up a [pre_prompt hook] to update the environment variables when needed. // 2. Initializes the __VFOX_SHELL and __VFOX_PID environment variables. @@ -42,8 +50,22 @@ const nushellConfig = env.Newline + // 4. Updates the environment variables. // // [pre_prompt hook]: https://www.nushell.sh/book/hooks.html -func (n nushell) Activate() (string, error) { - return nushellConfig, nil +func (n nushell) Activate(config ActivateConfig) (string, error) { + if len(config.Args) == 0 { + return "", fmt.Errorf("config path is required") + } + + // write file to config + targetPath := filepath.Join(config.Args[0], "vfox.nu") + + nushellConfig := strings.ReplaceAll(nushellConfig, "\n", env.Newline) + nushellConfig = strings.ReplaceAll(nushellConfig, "{{.SelfPath}}", config.SelfPath) + + if err := os.WriteFile(targetPath, []byte(nushellConfig), 0755); err != nil { + return "", fmt.Errorf("failed to write file: %s", err) + } + + return `source ($nu.default-config-dir | path join "vfox.nu")` + env.Newline, nil } // nushellExportData is used to create a JSON representation of the environment variables to be set and unset. diff --git a/internal/shell/nushell_test.go b/internal/shell/nushell_test.go index e5e1a80d..47e7a51a 100644 --- a/internal/shell/nushell_test.go +++ b/internal/shell/nushell_test.go @@ -1,73 +1,14 @@ package shell import ( - "bytes" "encoding/json" - "github.com/version-fox/vfox/internal/env" "os" "reflect" - "runtime" "slices" "testing" - "text/template" -) - -func TestActivate(t *testing.T) { - var newline string - if runtime.GOOS == "windows" { - newline = "\r\n" - } else { - newline = "\n" - } - selfPath := "/path/to/vfox" - want := newline + - "# vfox configuration" + newline + - "export-env {" + newline + - " def --env updateVfoxEnvironment [] {" + newline + - " let envData = (^'" + selfPath + "' env -s nushell | from json)" + newline + - " load-env $envData.envsToSet" + newline + - " hide-env ...$envData.envsToUnset" + newline + - " }" + newline + - " $env.config = ($env.config | upsert hooks.pre_prompt {" + newline + - " let currentValue = ($env.config | get -i hooks.pre_prompt)" + newline + - " if $currentValue == null {" + newline + - " [{updateVfoxEnvironment}]" + newline + - " } else {" + newline + - " $currentValue | append {updateVfoxEnvironment}" + newline + - " }" + newline + - " })" + newline + - " $env.__VFOX_SHELL = 'nushell'" + newline + - " $env.__VFOX_PID = $nu.pid" + newline + - " ^'" + selfPath + "' env --cleanup | ignore" + newline + - " updateVfoxEnvironment" + newline + - "}" + newline - - n := nushell{} - gotTemplate, err := n.Activate() - - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - parsedTemplate, err := template.New("activate").Parse(gotTemplate) - if err != nil { - t.Errorf("Unexpected error parsing template: %v", err) - return - } - var buffer bytes.Buffer - err = parsedTemplate.Execute(&buffer, struct{ SelfPath string }{selfPath}) - if err != nil { - t.Errorf("Unexpected error executing template: %v", err) - return - } - - got := buffer.String() - if got != want { - t.Errorf("Output mismatch:\n\ngot=\n%v\n\nwant=\n%v", got, want) - } -} + "github.com/version-fox/vfox/internal/env" +) func TestExport(t *testing.T) { sep := string(os.PathListSeparator) diff --git a/internal/shell/powershell.go b/internal/shell/powershell.go index 0cfd1e71..da323953 100644 --- a/internal/shell/powershell.go +++ b/internal/shell/powershell.go @@ -59,7 +59,7 @@ Register-EngineEvent -SourceIdentifier PowerShell.Exiting -SupportEvent -Action } ` -func (sh pwsh) Activate() (string, error) { +func (sh pwsh) Activate(config ActivateConfig) (string, error) { return hook, nil } diff --git a/internal/shell/shell.go b/internal/shell/shell.go index 89a2bfb7..07433369 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -22,10 +22,15 @@ import ( "github.com/version-fox/vfox/internal/env" ) +type ActivateConfig struct { + SelfPath string + Args []string +} + type Shell interface { // Activate generates a shell script to be placed in the shell's configuration file, which will set up initial // environment variables and set a hook to update the environment variables when needed. - Activate() (string, error) + Activate(config ActivateConfig) (string, error) // Export generates a string that can be used by the shell to set or unset the given environment variables. (The // input specifies environment variables to be unset by giving them a nil value.) diff --git a/internal/shell/zsh.go b/internal/shell/zsh.go index c0e94650..ec4770e8 100644 --- a/internal/shell/zsh.go +++ b/internal/shell/zsh.go @@ -48,7 +48,7 @@ if [[ -z "$__VFOX_PID" ]]; then fi ` -func (z zsh) Activate() (string, error) { +func (z zsh) Activate(config ActivateConfig) (string, error) { return zshHook, nil }