@@ -21,6 +21,7 @@ import (
2121 "fmt"
2222 "os"
2323 "os/exec"
24+ "slices"
2425 "strings"
2526
2627 "github.com/urfave/cli/v3"
@@ -29,109 +30,199 @@ import (
2930 "github.com/version-fox/vfox/internal/sdk"
3031)
3132
33+ type execSDKSpec struct {
34+ Name string
35+ Version sdk.Version
36+ }
37+
38+ const (
39+ execCommandName = "exec"
40+ execCommandAlias = "x"
41+ )
42+
3243var Exec = & cli.Command {
33- Name : "exec" ,
34- Aliases : []string {"x" },
44+ Name : execCommandName ,
45+ Aliases : []string {execCommandAlias },
3546 Usage : "Execute a command in vfox managed environment" ,
3647 Action : execCmd ,
3748}
3849
3950func execCmd (ctx context.Context , cmd * cli.Command ) error {
40- args := cmd .Args ()
41- if args .Len () < 2 {
42- return fmt .Errorf ("usage: vfox exec <sdk>[@<version>] <command> [args...]\n Example: vfox exec node@20 -- node -v" )
51+ sdkSpecs , command , cmdArgs , err := parseExecInvocation (cmd .Args ().Slice (), os .Args )
52+ if err != nil {
53+ return err
54+ }
55+ return executeInVfoxEnv (sdkSpecs , command , cmdArgs )
56+ }
57+
58+ func parseExecInvocation (parsedArgs , rawArgs []string ) ([]execSDKSpec , string , []string , error ) {
59+ if len (parsedArgs ) < 2 {
60+ return nil , "" , nil , execUsageError ()
4361 }
4462
45- // 1. Parse sdk@version (first argument)
46- firstArg := args .First ()
47- parts := strings .Split (firstArg , "@" )
48- sdkName := parts [0 ]
49- var sdkVersion sdk.Version
50- if len (parts ) > 1 {
51- sdkVersion = sdk .Version (strings .TrimPrefix (parts [1 ], "v" ))
63+ rawExecArgs := rawExecArgs (rawArgs )
64+ if separatorIndex := slices .Index (rawExecArgs , "--" ); separatorIndex >= 0 {
65+ sdkArgs := rawExecArgs [:separatorIndex ]
66+ commandArgs := rawExecArgs [separatorIndex + 1 :]
67+ if len (sdkArgs ) == 0 || len (commandArgs ) == 0 {
68+ return nil , "" , nil , execUsageError ()
69+ }
70+
71+ sdkSpecs := make ([]execSDKSpec , 0 , len (sdkArgs ))
72+ for _ , sdkArg := range sdkArgs {
73+ spec , err := parseExecSDKSpec (sdkArg )
74+ if err != nil {
75+ return nil , "" , nil , err
76+ }
77+ sdkSpecs = append (sdkSpecs , spec )
78+ }
79+ return sdkSpecs , commandArgs [0 ], commandArgs [1 :], nil
5280 }
5381
54- // 2. Second argument is the command, rest are command arguments
55- command := args . Get ( 1 )
56- cmdArgs := args . Slice ()[ 2 :]
82+ if len ( parsedArgs ) > 2 && strings . Contains ( parsedArgs [ 1 ], "@" ) {
83+ return nil , "" , nil , fmt . Errorf ( "multiple SDKs require '--' before the command \n %s" , execUsageLine () )
84+ }
5785
58- // 3. Execute the command
59- return executeInVfoxEnv (sdkName , sdkVersion , command , cmdArgs )
86+ spec , err := parseExecSDKSpec (parsedArgs [0 ])
87+ if err != nil {
88+ return nil , "" , nil , err
89+ }
90+ return []execSDKSpec {spec }, parsedArgs [1 ], parsedArgs [2 :], nil
91+ }
92+
93+ func rawExecArgs (rawArgs []string ) []string {
94+ for i := 1 ; i < len (rawArgs ); i ++ {
95+ if rawArgs [i ] == execCommandName || rawArgs [i ] == execCommandAlias {
96+ return rawArgs [i + 1 :]
97+ }
98+ }
99+ return nil
100+ }
101+
102+ func parseExecSDKSpec (arg string ) (execSDKSpec , error ) {
103+ arg = strings .TrimSpace (arg )
104+ if arg == "" {
105+ return execSDKSpec {}, execUsageError ()
106+ }
107+
108+ parts := strings .SplitN (arg , "@" , 2 )
109+ spec := execSDKSpec {Name : parts [0 ]}
110+ if spec .Name == "" {
111+ return execSDKSpec {}, fmt .Errorf ("invalid SDK spec %q" , arg )
112+ }
113+ if len (parts ) == 2 {
114+ spec .Version = sdk .Version (strings .TrimPrefix (parts [1 ], "v" ))
115+ }
116+ return spec , nil
117+ }
118+
119+ func execUsageLine () string {
120+ return "usage: vfox exec <sdk>[@<version>]... -- <command> [args...]\n Example: vfox exec nodejs@24.14.0 golang@1.25.6 -- npm install -g pnpm"
121+ }
122+
123+ func execUsageError () error {
124+ return fmt .Errorf ("%s" , execUsageLine ())
60125}
61126
62127// executeInVfoxEnv executes a command in vfox managed environment
63- func executeInVfoxEnv (sdkName string , sdkVersion sdk. Version , command string , cmdArgs []string ) error {
128+ func executeInVfoxEnv (sdkSpecs [] execSDKSpec , command string , cmdArgs []string ) error {
64129 manager , err := internal .NewSdkManager ()
65130 if err != nil {
66131 return fmt .Errorf ("failed to create sdk manager: %w" , err )
67132 }
68133 defer manager .Close ()
69134
70- // Lookup SDK
71- sdkSource , err := manager .LookupSdk (sdkName )
72- if err != nil {
73- return fmt .Errorf ("%s not supported, error: %w" , sdkName , err )
74- }
75-
76- // If version is not specified, try to get it from current scope
77- var resolvedVersion sdk.Version
78- if sdkVersion == "" {
79- // Get version from scope chain: Global > Session > Project
80- chain , err := manager .RuntimeEnvContext .LoadVfoxTomlChainByScopes (
81- env .Global , env .Session , env .Project ,
82- )
135+ sdkEnvs := make ([]* env.Envs , 0 , len (sdkSpecs ))
136+ for _ , sdkSpec := range sdkSpecs {
137+ specEnvs , err := resolveExecSDKEnv (manager , sdkSpec )
83138 if err != nil {
84- return fmt . Errorf ( "failed to load config: %w" , err )
139+ return err
85140 }
141+ sdkEnvs = append (sdkEnvs , specEnvs )
142+ }
86143
87- version , _ , ok := chain .GetToolVersion (sdkName )
88- if ! ok || version == "" {
89- return fmt .Errorf ("no version configured for %s. Please use 'vfox use' to set a version first" , sdkName )
144+ mergedEnvs := mergeExecEnvsByPriority (sdkEnvs )
145+ applyExecSystemPaths (manager .RuntimeEnvContext , mergedEnvs )
146+
147+ envMap := make (map [string ]string , len (mergedEnvs .Variables )+ 1 )
148+ for key , value := range mergedEnvs .Variables {
149+ if value != nil {
150+ envMap [key ] = * value
90151 }
91- resolvedVersion = sdk .Version (version )
92- } else {
93- // Use the user-specified version
94- resolvedVersion = sdkVersion
95-
96- // Check if installed, auto-install if not
97- if ! sdkSource .CheckRuntimeExist (resolvedVersion ) {
98- fmt .Printf ("SDK %s@%s not found, installing...\n " , sdkName , resolvedVersion )
99- if err := sdkSource .Install (resolvedVersion ); err != nil {
100- return fmt .Errorf ("failed to install %s@%s: %w" , sdkName , resolvedVersion , err )
101- }
152+ }
153+ envMap ["PATH" ] = mergedEnvs .Paths .String ()
154+ return executeCommand (command , cmdArgs , envMap )
155+ }
156+
157+ func resolveExecSDKEnv (manager * internal.Manager , sdkSpec execSDKSpec ) (* env.Envs , error ) {
158+ sdkSource , err := manager .LookupSdk (sdkSpec .Name )
159+ if err != nil {
160+ return nil , fmt .Errorf ("%s not supported, error: %w" , sdkSpec .Name , err )
161+ }
162+
163+ resolvedVersion , err := resolveExecSDKVersion (manager , sdkSpec )
164+ if err != nil {
165+ return nil , err
166+ }
167+ if sdkSpec .Version != "" && ! sdkSource .CheckRuntimeExist (resolvedVersion ) {
168+ fmt .Printf ("SDK %s@%s not found, installing...\n " , sdkSpec .Name , resolvedVersion )
169+ if err := sdkSource .Install (resolvedVersion ); err != nil {
170+ return nil , fmt .Errorf ("failed to install %s@%s: %w" , sdkSpec .Name , resolvedVersion , err )
102171 }
103172 }
104173
105- // Get environment variables
106- // Note: Using EnvKeys to get the runtime package paths
107174 runtimePackage , err := sdkSource .GetRuntimePackage (resolvedVersion )
108175 if err != nil {
109- return fmt .Errorf ("failed to get runtime package for %s@%s: %w" , sdkName , resolvedVersion , err )
176+ return nil , fmt .Errorf ("failed to get runtime package for %s@%s: %w" , sdkSpec . Name , resolvedVersion , err )
110177 }
178+
111179 envKeys , err := sdkSource .EnvKeys (runtimePackage )
112180 if err != nil {
113- return fmt .Errorf ("failed to get env keys for %s@%s: %w" , sdkName , resolvedVersion , err )
181+ return nil , fmt .Errorf ("failed to get env keys for %s@%s: %w" , sdkSpec . Name , resolvedVersion , err )
114182 }
183+ return envKeys , nil
184+ }
115185
116- // Build environment variable map
117- envMap := make (map [string ]string )
186+ func resolveExecSDKVersion (manager * internal.Manager , sdkSpec execSDKSpec ) (sdk.Version , error ) {
187+ if sdkSpec .Version != "" {
188+ return sdkSpec .Version , nil
189+ }
118190
119- // Add PATH from envKeys.Paths
120- if envKeys .Paths != nil && envKeys .Paths .Slice () != nil {
121- paths := envKeys .Paths .Slice ()
122- pathStr := strings .Join (paths , string (os .PathListSeparator ))
123- envMap ["PATH" ] = pathStr
191+ chain , err := manager .RuntimeEnvContext .LoadVfoxTomlChainByScopes (env .Global , env .Session , env .Project )
192+ if err != nil {
193+ return "" , fmt .Errorf ("failed to load config: %w" , err )
124194 }
125195
126- // Add other variables from envKeys.Variables
127- for key , value := range envKeys .Variables {
128- if value != nil {
129- envMap [key ] = * value
196+ version , _ , ok := chain .GetToolVersion (sdkSpec .Name )
197+ if ! ok || version == "" {
198+ return "" , fmt .Errorf ("no version configured for %s. Please use 'vfox use' to set a version first" , sdkSpec .Name )
199+ }
200+ return sdk .Version (version ), nil
201+ }
202+
203+ func mergeExecEnvsByPriority (envsByPriority []* env.Envs ) * env.Envs {
204+ merged := env .NewEnvs ()
205+ for _ , sdkEnvs := range envsByPriority {
206+ if sdkEnvs == nil {
207+ continue
208+ }
209+ merged .Paths .Merge (sdkEnvs .Paths )
210+ }
211+ for i := len (envsByPriority ) - 1 ; i >= 0 ; i -- {
212+ if sdkEnvs := envsByPriority [i ]; sdkEnvs != nil {
213+ merged .Variables .Merge (sdkEnvs .Variables )
130214 }
131215 }
216+ return merged
217+ }
132218
133- // Execute command
134- return executeCommand (command , cmdArgs , envMap )
219+ func applyExecSystemPaths (runtimeEnvContext * env.RuntimeEnvContext , sdkEnvs * env.Envs ) {
220+ if sdkEnvs == nil {
221+ return
222+ }
223+ prefixPaths , cleanSystemPaths := runtimeEnvContext .SplitSystemPaths ()
224+ sdkEnvs .Paths .Merge (prefixPaths )
225+ sdkEnvs .Paths .Merge (cleanSystemPaths )
135226}
136227
137228// executeCommand executes a command in the specified environment
0 commit comments