Skip to content

Commit 8dc0001

Browse files
Copilotaooohan
andcommitted
Fix: allow vfox use --global without shell hook support (Docker compatibility)
When running in Docker RUN commands, `.bashrc` is not sourced by non-interactive bash, so __VFOX_SHELL is never set and vfox use --global fails with the hook error. Global scope only writes a config file and creates symlinks, so it does not need shell hook support. - In UseWithConfig: on non-Windows, only return the hook error when scope is NOT global (mirrors the Windows behaviour) - In useInHook: when hook env is absent and scope is global, skip spawning a new shell and print a helpful hint instead - Add TestUseWithConfig_NoHookEnv to verify session/project scopes still require hook support while global scope does not Co-authored-by: aooohan <40265686+aooohan@users.noreply.github.com>
1 parent 5cd303e commit 8dc0001

File tree

3 files changed

+82
-2
lines changed

3 files changed

+82
-2
lines changed

internal/sdk/sdk.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,11 @@ func (b *impl) UseWithConfig(version Version, scope env.UseScope, unlink bool) e
543543
pterm.Printf("Warning: The current shell lacks hook support. Switching to global scope automatically.\n")
544544
scope = env.Global
545545
}
546-
} else {
546+
} else if scope != env.Global {
547+
// On non-Windows, only global scope is allowed without hook support.
548+
// Session and project scopes require shell hooks to propagate environment variables.
549+
// Global scope only writes a config file and creates symlinks, so it works fine
550+
// in non-interactive environments such as Docker RUN commands.
547551
logger.Debugf("Hook environment not available\n")
548552
return fmt.Errorf("vfox requires hook support. Please ensure vfox is properly initialized with 'vfox activate'")
549553
}
@@ -668,8 +672,14 @@ func (b *impl) useInHook(version Version, scope env.UseScope, unlink bool, hookE
668672
pterm.Printf("Now using %s.\n", pterm.LightGreen(b.Label(version)))
669673
logger.Debugf("Successfully using SDK: %s\n", b.Label(version))
670674

671-
// In non-hook environment, spawn a new shell so the user can use the new version immediately
675+
// In non-hook environment, spawn a new shell so the user can use the new version immediately.
676+
// For global scope, spawning a shell is unnecessary because the version is persisted via
677+
// config file and symlinks — no shell-session state needs to be updated.
672678
if !hookEnv {
679+
if scope == env.Global {
680+
pterm.Printf("Please start a new shell session or run 'eval \"$(vfox activate <shell>)\"' to use the new version.\n")
681+
return nil
682+
}
673683
return shell.Open(os.Getppid())
674684
}
675685

internal/sdk/sdk_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,76 @@ func TestEnsureVfoxInGitignore(t *testing.T) {
510510
}
511511
}
512512

513+
// TestUseWithConfig_NoHookEnv verifies that UseWithConfig behaves correctly
514+
// when the shell hook environment is not active (e.g., in Docker RUN commands).
515+
// Global scope should be allowed; session and project scopes should return an error.
516+
func TestUseWithConfig_NoHookEnv(t *testing.T) {
517+
// Ensure __VFOX_SHELL is unset for this test
518+
orig := os.Getenv(env.HookFlag)
519+
os.Unsetenv(env.HookFlag)
520+
defer func() {
521+
if orig != "" {
522+
os.Setenv(env.HookFlag, orig)
523+
}
524+
}()
525+
526+
tempDir := t.TempDir()
527+
pathMeta := &pathmeta.PathMeta{
528+
User: pathmeta.UserPaths{
529+
Home: filepath.Join(tempDir, "user"),
530+
},
531+
Shared: pathmeta.SharedPaths{
532+
Installs: filepath.Join(tempDir, "shared", "installs"),
533+
},
534+
Working: pathmeta.WorkingPaths{
535+
Directory: filepath.Join(tempDir, "project"),
536+
ProjectSdkDir: filepath.Join(tempDir, "project", ".vfox", "sdk"),
537+
SessionSdkDir: filepath.Join(tempDir, "session", "sdk"),
538+
GlobalSdkDir: filepath.Join(tempDir, "user", "sdk"),
539+
},
540+
}
541+
542+
runtimeEnvContext := &env.RuntimeEnvContext{
543+
PathMeta: pathMeta,
544+
}
545+
546+
sdkImpl := &impl{
547+
Name: "test-sdk",
548+
envContext: runtimeEnvContext,
549+
InstallPath: filepath.Join(pathMeta.Shared.Installs, "test-sdk"),
550+
}
551+
552+
// Session and project scopes must return a hook error when __VFOX_SHELL is not set.
553+
for _, scope := range []env.UseScope{env.Session, env.Project} {
554+
scope := scope
555+
t.Run(scope.String()+" scope returns hook error without hook env", func(t *testing.T) {
556+
err := sdkImpl.UseWithConfig("1.0.0", scope, false)
557+
if err == nil || !strings.Contains(err.Error(), "vfox requires hook support") {
558+
t.Errorf("scope=%v: expected 'vfox requires hook support' error, got: %v", scope, err)
559+
}
560+
})
561+
}
562+
563+
// Global scope must NOT return a hook error when __VFOX_SHELL is not set.
564+
// It should proceed past the hook check (it will fail later with a missing plugin/version
565+
// error since this is a minimal test fixture, but NOT with the hook-support error).
566+
t.Run("global scope does not return hook error without hook env", func(t *testing.T) {
567+
var gotHookErr bool
568+
func() {
569+
// The call will panic or return an error due to the missing plugin in the test
570+
// fixture — that is acceptable. We only care that it is NOT a hook-support error.
571+
defer func() { recover() }() //nolint:errcheck
572+
err := sdkImpl.UseWithConfig("1.0.0", env.Global, false)
573+
if err != nil && strings.Contains(err.Error(), "vfox requires hook support") {
574+
gotHookErr = true
575+
}
576+
}()
577+
if gotHookErr {
578+
t.Error("global scope must not return 'vfox requires hook support' error when hook env is not set")
579+
}
580+
})
581+
}
582+
513583
// Helper function to check if a line exists in content
514584
func containsLine(content, line string) bool {
515585
lines := splitLines(content)

vfox

18.7 MB
Binary file not shown.

0 commit comments

Comments
 (0)