@@ -10,7 +10,9 @@ import (
1010 "strings"
1111
1212 "github.com/agnivade/levenshtein"
13+ gofrogcmd "github.com/jfrog/gofrog/io"
1314 artifactoryCLI "github.com/jfrog/jfrog-cli-artifactory/cli"
15+ "github.com/jfrog/jfrog-cli-core/v2/common/build"
1416 corecommon "github.com/jfrog/jfrog-cli-core/v2/docs/common"
1517 "github.com/jfrog/jfrog-cli-core/v2/plugins/components"
1618 coreconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
@@ -36,6 +38,7 @@ import (
3638 "github.com/jfrog/jfrog-cli/pipelines"
3739 "github.com/jfrog/jfrog-cli/plugins"
3840 "github.com/jfrog/jfrog-cli/plugins/utils"
41+ "github.com/jfrog/jfrog-cli/utils/buildinfo"
3942 "github.com/jfrog/jfrog-cli/utils/cliutils"
4043 "github.com/jfrog/jfrog-client-go/http/httpclient"
4144 clientutils "github.com/jfrog/jfrog-client-go/utils"
@@ -129,13 +132,219 @@ func execMain() error {
129132 if err = setUberTraceIdToken (); err != nil {
130133 clientlog .Warn ("failed generating a trace ID token:" , err .Error ())
131134 }
135+ if os .Getenv ("JFROG_RUN_NATIVE" ) == "true" {
136+ // If the JFROG_RUN_NATIVE environment variable is set to true, we run the new implementation
137+ // but only for package manager commands, not for JFrog CLI commands
138+ args := ctx .Args ()
139+ if args .Present () && len (args ) > 0 {
140+ firstArg := args .Get (0 )
141+ if isPackageManagerCommand (firstArg ) {
142+ if err = runNativeImplementation (ctx ); err != nil {
143+ clientlog .Error ("Failed to run native implementation:" , err )
144+ os .Exit (1 )
145+ }
146+ os .Exit (0 )
147+ }
148+ }
149+ // For non-package-manager commands, continue with normal CLI processing
150+ }
132151 return nil
133152 }
153+
154+ app .CommandNotFound = func (c * cli.Context , command string ) {
155+ // Try to handle as native package manager command only when JFROG_RUN_NATIVE is true
156+ if os .Getenv ("JFROG_RUN_NATIVE" ) == "true" && isPackageManagerCommand (command ) {
157+ clientlog .Debug ("Attempting to handle as native package manager command:" , command )
158+ err := runNativeImplementation (c )
159+ if err != nil {
160+ clientlog .Error ("Failed to run native implementation:" , err )
161+ os .Exit (1 )
162+ }
163+ os .Exit (0 )
164+ }
165+
166+ // Original behavior for unknown commands
167+ _ , err = fmt .Fprintf (c .App .Writer , "'%s %s' is not a jf command. See --help\n " , c .App .Name , command )
168+ if err != nil {
169+ clientlog .Debug (err )
170+ os .Exit (1 )
171+ }
172+ if bestSimilarity := searchSimilarCmds (c .App .Commands , command ); len (bestSimilarity ) > 0 {
173+ text := "The most similar "
174+ if len (bestSimilarity ) == 1 {
175+ text += "command is:\n \t jf " + bestSimilarity [0 ]
176+ } else {
177+ sort .Strings (bestSimilarity )
178+ text += "commands are:\n \t jf " + strings .Join (bestSimilarity , "\n \t jf " )
179+ }
180+ _ , err = fmt .Fprintln (c .App .Writer , text )
181+ if err != nil {
182+ clientlog .Debug (err )
183+ }
184+ }
185+ os .Exit (1 )
186+ }
187+
134188 err = app .Run (args )
135189 logTraceIdOnFailure (err )
136190 return err
137191}
138192
193+ func runNativeImplementation (ctx * cli.Context ) error {
194+ clientlog .Debug ("Starting native implementation..." )
195+
196+ // Extract the build name and number from the command arguments
197+ args , buildArgs , err := build .ExtractBuildDetailsFromArgs (ctx .Args ())
198+ if err != nil {
199+ clientlog .Error ("Failed to extract build details from args: " , err )
200+ return fmt .Errorf ("ExtractBuildDetailsFromArgs failed: %w" , err )
201+ }
202+
203+ if len (args ) < 2 {
204+ return fmt .Errorf ("insufficient arguments: expected at least package-manager and command, got %v" , args )
205+ }
206+
207+ packageManager := args [0 ]
208+ command := args [1 ]
209+ clientlog .Debug ("Executing native command: " + packageManager + " " + command )
210+
211+ buildName , err := buildArgs .GetBuildName ()
212+ if err != nil {
213+ clientlog .Error ("Failed to get build name: " , err )
214+ return fmt .Errorf ("GetBuildName failed: %w" , err )
215+ }
216+
217+ buildNumber , err := buildArgs .GetBuildNumber ()
218+ if err != nil {
219+ clientlog .Error ("Failed to get build number: " , err )
220+ return fmt .Errorf ("GetBuildNumber failed: %w" , err )
221+ }
222+
223+ // Execute the native command
224+ err = RunActions (args )
225+ if err != nil {
226+ clientlog .Error ("Failed to run actions: " , err )
227+ return fmt .Errorf ("RunActions failed: %w" , err )
228+ }
229+
230+ // Collect build info if build name and number are provided
231+ if buildName != "" && buildNumber != "" {
232+ clientlog .Info ("Collecting build info for executed command..." )
233+ workingDir := ctx .GlobalString ("working-dir" )
234+ if workingDir == "" {
235+ workingDir = "."
236+ }
237+
238+ // Use the enhanced build info collection that supports Poetry
239+ err = buildinfo .GetBuildInfoForPackageManager (packageManager , workingDir , buildArgs )
240+ if err != nil {
241+ clientlog .Error ("Failed to collect build info: " , err )
242+ return fmt .Errorf ("GetBuildInfoForPackageManager failed: %w" , err )
243+ }
244+ }
245+
246+ clientlog .Info ("Native implementation completed successfully." )
247+ return nil
248+ }
249+
250+ // isPackageManagerCommand checks if the command is a supported package manager
251+ func isPackageManagerCommand (command string ) bool {
252+ supportedPackageManagers := []string {"poetry" , "pip" , "pipenv" , "gem" , "bundle" , "npm" , "yarn" , "gradle" , "mvn" , "maven" , "nuget" , "go" }
253+ for _ , pm := range supportedPackageManagers {
254+ if command == pm {
255+ return true
256+ }
257+ }
258+ return false
259+ }
260+
261+ // cleanProgressOutput handles carriage returns and progress updates properly
262+ func cleanProgressOutput (output string ) string {
263+ if output == "" {
264+ return ""
265+ }
266+
267+ // First, split by both \n and \r to handle all line breaks
268+ lines := strings .FieldsFunc (output , func (c rune ) bool {
269+ return c == '\n' || c == '\r'
270+ })
271+
272+ var cleanedLines []string
273+ var progressLines = make (map [string ]string ) // Track progress lines by filename
274+
275+ for _ , line := range lines {
276+ line = strings .TrimSpace (line )
277+ if line == "" {
278+ continue
279+ }
280+
281+ // Check if this is a progress line (contains % and "Uploading")
282+ if strings .Contains (line , "Uploading" ) && strings .Contains (line , "%" ) {
283+ // Extract filename for tracking progress
284+ if strings .Contains (line , " - Uploading " ) {
285+ parts := strings .Split (line , " - Uploading " )
286+ if len (parts ) == 2 {
287+ filename := strings .Split (parts [1 ], " " )[0 ]
288+ progressLines [filename ] = line
289+ continue
290+ }
291+ }
292+ }
293+
294+ // Add non-progress lines immediately
295+ cleanedLines = append (cleanedLines , line )
296+ }
297+
298+ // Add final progress states
299+ for _ , progressLine := range progressLines {
300+ cleanedLines = append (cleanedLines , progressLine )
301+ }
302+
303+ if len (cleanedLines ) > 0 {
304+ return strings .Join (cleanedLines , "\n " ) + "\n "
305+ }
306+ return ""
307+ }
308+
309+ func RunActions (args []string ) error {
310+ if len (args ) < 2 {
311+ return fmt .Errorf ("insufficient arguments for RunActions: expected at least 2, got %d" , len (args ))
312+ }
313+
314+ packageManager := args [0 ]
315+ subCommand := args [1 ]
316+ executableCommand := append ([]string {}, args [2 :]... )
317+
318+ clientlog .Debug ("Executing command: " + packageManager + " " + subCommand )
319+ command := gofrogcmd .NewCommand (packageManager , subCommand , executableCommand )
320+
321+ // Use RunCmdWithOutputParser but handle the output better
322+ stdout , stderr , exitOk , err := gofrogcmd .RunCmdWithOutputParser (command , false )
323+ if err != nil {
324+ clientlog .Error ("Command execution failed: " , err )
325+ if stderr != "" {
326+ clientlog .Error ("Command stderr: " , stderr )
327+ }
328+ return fmt .Errorf ("command '%s %s' failed (exitOk=%t): %w" , packageManager , subCommand , exitOk , err )
329+ }
330+
331+ // Print stdout directly without parsing to preserve Poetry's output format
332+ if stdout != "" {
333+ fmt .Print (stdout )
334+ }
335+
336+ // Also print stderr, but clean it up for progress information
337+ if stderr != "" {
338+ cleanStderr := cleanProgressOutput (stderr )
339+ if cleanStderr != "" {
340+ fmt .Print (cleanStderr )
341+ }
342+ }
343+
344+ clientlog .Debug ("Command executed successfully" )
345+ return nil
346+ }
347+
139348// This command generates and sets an Uber Trace ID token which will be attached as a header to every request.
140349// This allows users to easily identify which logs on the server side are related to the command executed by the CLI.
141350func setUberTraceIdToken () error {
0 commit comments