Skip to content

Commit 2c00f33

Browse files
Copilotbytemain
andauthored
fix: fall back to installed lower-scope SDK versions when higher-priority config is missing (#661)
* Plan fallback behavior for missing project SDK version Agent-Logs-Url: https://github.com/version-fox/vfox/sessions/f9ac9663-b05f-43e1-aa20-4abf8b1e5255 Co-authored-by: bytemain <13938334+bytemain@users.noreply.github.com> * Fallback to installed lower-scope SDK version during activation Agent-Logs-Url: https://github.com/version-fox/vfox/sessions/f9ac9663-b05f-43e1-aa20-4abf8b1e5255 Co-authored-by: bytemain <13938334+bytemain@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bytemain <13938334+bytemain@users.noreply.github.com>
1 parent 5a16b6e commit 2c00f33

5 files changed

Lines changed: 113 additions & 16 deletions

File tree

cmd/commands/activate.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ func activateCmd(ctx context.Context, cmd *cli.Command) error {
8989
// Process SDKs concurrently using errgroup
9090
g, _ := errgroup.WithContext(ctx)
9191

92-
for sdkName, version := range allTools {
92+
for sdkName := range allTools {
9393
sdkName := sdkName // Capture loop variable
94-
version := version // Capture loop variable
9594

9695
g.Go(func() error {
9796
// Lookup SDK
@@ -101,10 +100,8 @@ func activateCmd(ctx context.Context, cmd *cli.Command) error {
101100
return nil // Continue processing other SDKs
102101
}
103102

104-
sdkVersion := sdk.Version(version)
105-
106-
// Get tool config with scope information (searches by priority)
107-
toolConfig, scope, ok := chain.GetToolConfig(sdkName)
103+
// Resolve to the highest-priority installed version across scopes
104+
toolConfig, scope, sdkVersion, ok := resolveInstalledToolConfig(chain, sdkObj, sdkName)
108105
if !ok {
109106
return nil
110107
}
@@ -121,14 +118,14 @@ func activateCmd(ctx context.Context, cmd *cli.Command) error {
121118
// Create symlinks if needed (internal logic checks if symlink already exists)
122119
if err := sdkObj.CreateSymlinksForScope(sdkVersion, actualScope); err != nil {
123120
logger.Debugf("Failed to create symlinks for %s@%s (scope: %s): %v",
124-
sdkName, version, actualScope.String(), err)
121+
sdkName, sdkVersion, actualScope.String(), err)
125122
return nil // Continue processing other SDKs
126123
}
127124

128125
// Get environment variables pointing to symlinks
129126
sdkEnvs, err := sdkObj.EnvKeysForScope(sdkVersion, actualScope)
130127
if err != nil {
131-
logger.Debugf("Failed to get env keys for %s@%s: %v", sdkName, version, err)
128+
logger.Debugf("Failed to get env keys for %s@%s: %v", sdkName, sdkVersion, err)
132129
return nil // Continue processing other SDKs
133130
}
134131

cmd/commands/env.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,8 @@ func envFlag(cmd *cli.Command) error {
212212
// Process SDKs concurrently using errgroup
213213
g, _ := errgroup.WithContext(context.Background())
214214

215-
for sdkName, version := range allTools {
215+
for sdkName := range allTools {
216216
sdkName := sdkName // Capture loop variable
217-
version := version // Capture loop variable
218217

219218
g.Go(func() error {
220219
// Lookup SDK
@@ -224,10 +223,8 @@ func envFlag(cmd *cli.Command) error {
224223
return nil // Continue processing other SDKs
225224
}
226225

227-
sdkVersion := sdk.Version(version)
228-
229-
// Get tool config with scope information (searches by priority)
230-
toolConfig, scope, ok := chain.GetToolConfig(sdkName)
226+
// Resolve to the highest-priority installed version across scopes
227+
toolConfig, scope, sdkVersion, ok := resolveInstalledToolConfig(chain, sdkObj, sdkName)
231228
if !ok {
232229
return nil
233230
}
@@ -244,14 +241,14 @@ func envFlag(cmd *cli.Command) error {
244241
// Create symlinks if needed (internal logic checks if symlink already exists)
245242
if err := sdkObj.CreateSymlinksForScope(sdkVersion, actualScope); err != nil {
246243
logger.Debugf("Failed to create symlinks for %s@%s (scope: %s): %v",
247-
sdkName, version, actualScope.String(), err)
244+
sdkName, sdkVersion, actualScope.String(), err)
248245
return nil // Continue processing other SDKs
249246
}
250247

251248
// Get environment variables pointing to symlinks
252249
sdkEnvs, err := sdkObj.EnvKeysForScope(sdkVersion, actualScope)
253250
if err != nil {
254-
logger.Debugf("Failed to get env keys for %s@%s: %v", sdkName, version, err)
251+
logger.Debugf("Failed to get env keys for %s@%s: %v", sdkName, sdkVersion, err)
255252
return nil // Continue processing other SDKs
256253
}
257254

cmd/commands/tool_resolution.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2026 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+
"github.com/version-fox/vfox/internal/env"
21+
"github.com/version-fox/vfox/internal/pathmeta"
22+
"github.com/version-fox/vfox/internal/sdk"
23+
"github.com/version-fox/vfox/internal/shared/logger"
24+
)
25+
26+
func resolveInstalledToolConfig(chain env.VfoxTomlChain, sdkObj sdk.Sdk, sdkName string) (*pathmeta.ToolConfig, env.UseScope, sdk.Version, bool) {
27+
toolConfigs := chain.GetToolConfigsByPriority(sdkName)
28+
if len(toolConfigs) == 0 {
29+
return nil, env.Global, "", false
30+
}
31+
32+
for _, toolConfig := range toolConfigs {
33+
version := sdk.Version(toolConfig.Config.Version)
34+
if sdkObj.CheckRuntimeExist(version) {
35+
return toolConfig.Config, toolConfig.Scope, version, true
36+
}
37+
logger.Debugf("SDK %s@%s from %s scope not installed, trying lower-priority config",
38+
sdkName, version, toolConfig.Scope.String())
39+
}
40+
41+
logger.Debugf("No installed configured version found for SDK %s", sdkName)
42+
return nil, env.Global, "", false
43+
}

internal/env/vfox_toml_chain.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ type chainItem struct {
2626
scope UseScope
2727
}
2828

29+
// ScopedToolConfig bundles a tool configuration with its scope.
30+
type ScopedToolConfig struct {
31+
Config *pathmeta.ToolConfig
32+
Scope UseScope
33+
}
34+
2935
// VfoxTomlChain is a chain of VfoxToml configs, supporting multi-config merging
3036
type VfoxTomlChain []*chainItem
3137

@@ -103,6 +109,26 @@ func (c *VfoxTomlChain) GetToolVersion(name string) (string, UseScope, bool) {
103109
return "", Global, false
104110
}
105111

112+
// GetToolConfigsByPriority returns all tool configs from high to low priority.
113+
func (c *VfoxTomlChain) GetToolConfigsByPriority(name string) []ScopedToolConfig {
114+
result := make([]ScopedToolConfig, 0, len(*c))
115+
for i := len(*c) - 1; i >= 0; i-- {
116+
item := (*c)[i]
117+
if item == nil || item.config == nil {
118+
continue
119+
}
120+
config, ok := item.config.Tools.Get(name)
121+
if !ok {
122+
continue
123+
}
124+
result = append(result, ScopedToolConfig{
125+
Config: config,
126+
Scope: item.scope,
127+
})
128+
}
129+
return result
130+
}
131+
106132
// GetByIndex returns the config at the specified index
107133
func (c *VfoxTomlChain) GetByIndex(index int) *pathmeta.VfoxToml {
108134
if index < 0 || index >= len(*c) {

internal/env/vfox_toml_chain_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,40 @@ func TestVfoxTomlChain(t *testing.T) {
196196
}
197197
})
198198

199+
t.Run("GetToolConfigsByPriority", func(t *testing.T) {
200+
chain := NewVfoxTomlChain()
201+
202+
globalConfig := pathmeta.NewVfoxToml()
203+
globalConfig.SetTool("golang", "1.26.0")
204+
205+
sessionConfig := pathmeta.NewVfoxToml()
206+
sessionConfig.SetTool("golang", "1.25.0")
207+
208+
projectConfig := pathmeta.NewVfoxToml()
209+
projectConfig.SetTool("golang", "1.24.0")
210+
211+
chain.Add(globalConfig, Global)
212+
chain.Add(sessionConfig, Session)
213+
chain.Add(projectConfig, Project)
214+
215+
configs := chain.GetToolConfigsByPriority("golang")
216+
if len(configs) != 3 {
217+
t.Fatalf("Expected 3 configs, got %d", len(configs))
218+
}
219+
220+
if configs[0].Scope != Project || configs[0].Config.Version != "1.24.0" {
221+
t.Fatalf("Expected project config first, got scope=%s version=%s", configs[0].Scope.String(), configs[0].Config.Version)
222+
}
223+
224+
if configs[1].Scope != Session || configs[1].Config.Version != "1.25.0" {
225+
t.Fatalf("Expected session config second, got scope=%s version=%s", configs[1].Scope.String(), configs[1].Config.Version)
226+
}
227+
228+
if configs[2].Scope != Global || configs[2].Config.Version != "1.26.0" {
229+
t.Fatalf("Expected global config third, got scope=%s version=%s", configs[2].Scope.String(), configs[2].Config.Version)
230+
}
231+
})
232+
199233
t.Run("GetTool not found", func(t *testing.T) {
200234
chain := NewVfoxTomlChain()
201235

0 commit comments

Comments
 (0)