@@ -8,9 +8,11 @@ import (
88 "fmt"
99 "io"
1010 "net/http"
11+ "net/url"
1112 "os"
1213 "os/exec"
1314 "path/filepath"
15+ "strings"
1416 "time"
1517
1618 "github.com/stainless-api/stainless-api-cli/pkg/jsonflag"
@@ -436,9 +438,7 @@ func pullBuildOutputs(ctx context.Context, client stainless.Client, res stainles
436438
437439 // Pull each target
438440 for i , target := range targets {
439- targetDir := fmt .Sprintf ("%s-%s" , res .Project , target )
440-
441- targetGroup := Progress ("[%d/%d] Pulling %s → %s" , i + 1 , len (targets ), target , targetDir )
441+ targetGroup := Progress ("[%d/%d] Pulling %s" , i + 1 , len (targets ), target )
442442
443443 // Get the output details
444444 outputRes , err := client .Builds .TargetOutputs .Get (
@@ -456,13 +456,27 @@ func pullBuildOutputs(ctx context.Context, client stainless.Client, res stainles
456456 }
457457
458458 // Handle based on output type
459- err = pullOutput (outputRes .Output , outputRes .URL , outputRes .Ref , targetDir )
459+ err = pullOutput (outputRes .Output , outputRes .URL , outputRes .Ref )
460460 if err != nil {
461461 targetGroup .Error ("Failed to pull %s: %v" , target , err )
462462 continue
463463 }
464464
465- targetGroup .Success ("Successfully pulled to %s" , targetDir )
465+ // Get the appropriate success message based on output type
466+ if outputRes .Output == "git" {
467+ // Extract repository name from git URL for success message
468+ repoName := filepath .Base (outputRes .URL )
469+ if strings .HasSuffix (repoName , ".git" ) {
470+ repoName = strings .TrimSuffix (repoName , ".git" )
471+ }
472+ if repoName == "" || repoName == "." || repoName == "/" {
473+ repoName = "repository"
474+ }
475+ targetGroup .Success ("Successfully pulled to %s" , repoName )
476+ } else {
477+ filename := extractFilenameFromURL (outputRes .URL )
478+ targetGroup .Success ("Successfully downloaded %s" , filename )
479+ }
466480
467481 if i < len (targets )- 1 {
468482 fmt .Fprintf (os .Stderr , "\n " )
@@ -472,23 +486,89 @@ func pullBuildOutputs(ctx context.Context, client stainless.Client, res stainles
472486 return nil
473487}
474488
475- // pullOutput handles downloading or cloning a build target output
476- func pullOutput (output , url , ref , targetDir string ) error {
477- // Remove existing directory if it exists
478- if _ , err := os .Stat (targetDir ); err == nil {
479- Info ("Removing existing directory %s" , targetDir )
480- if err := os .RemoveAll (targetDir ); err != nil {
481- return fmt .Errorf ("failed to remove existing directory %s: %v" , targetDir , err )
489+ // extractFilenameFromURL extracts the filename from just the URL path (without query parameters)
490+ func extractFilenameFromURL (urlStr string ) string {
491+ // Parse URL to remove query parameters
492+ parsedURL , err := url .Parse (urlStr )
493+ if err != nil {
494+ // If URL parsing fails, use the original approach
495+ filename := filepath .Base (urlStr )
496+ if filename == "." || filename == "/" || filename == "" {
497+ return "download"
482498 }
499+ return filename
483500 }
484501
485- // Create a fresh directory for the output
486- if err := os .MkdirAll (targetDir , 0755 ); err != nil {
487- return fmt .Errorf ("failed to create directory %s: %v" , targetDir , err )
502+ // Extract filename from URL path (without query parameters)
503+ filename := filepath .Base (parsedURL .Path )
504+ if filename == "." || filename == "/" || filename == "" {
505+ return "download"
488506 }
489507
508+ return filename
509+ }
510+
511+ // extractFilename extracts the filename from a URL and HTTP response headers
512+ func extractFilename (urlStr string , resp * http.Response ) string {
513+ // First, try to get filename from Content-Disposition header
514+ if contentDisp := resp .Header .Get ("Content-Disposition" ); contentDisp != "" {
515+ // Parse Content-Disposition header for filename
516+ // Format: attachment; filename="example.txt" or attachment; filename=example.txt
517+ if strings .Contains (contentDisp , "filename=" ) {
518+ parts := strings .Split (contentDisp , "filename=" )
519+ if len (parts ) > 1 {
520+ filename := strings .TrimSpace (parts [1 ])
521+ // Remove quotes if present
522+ filename = strings .Trim (filename , `"` )
523+ // Remove any additional parameters after semicolon
524+ if idx := strings .Index (filename , ";" ); idx != - 1 {
525+ filename = filename [:idx ]
526+ }
527+ filename = strings .TrimSpace (filename )
528+ if filename != "" {
529+ return filename
530+ }
531+ }
532+ }
533+ }
534+
535+ // Fallback to URL path parsing
536+ return extractFilenameFromURL (urlStr )
537+ }
538+
539+ // pullOutput handles downloading or cloning a build target output
540+ func pullOutput (output , url , ref string ) error {
490541 switch output {
491542 case "git" :
543+ // Extract repository name from git URL for directory name
544+ // Handle formats like:
545+ // - https://github.com/owner/repo.git
546+ // - https://github.com/owner/repo
547+ // - [email protected] :owner/repo.git 548+ targetDir := filepath .Base (url )
549+
550+ // Remove .git suffix if present
551+ if strings .HasSuffix (targetDir , ".git" ) {
552+ targetDir = strings .TrimSuffix (targetDir , ".git" )
553+ }
554+
555+ // Handle empty or invalid names
556+ if targetDir == "" || targetDir == "." || targetDir == "/" {
557+ targetDir = "repository"
558+ }
559+
560+ // Remove existing directory if it exists
561+ if _ , err := os .Stat (targetDir ); err == nil {
562+ Info ("Removing existing directory %s" , targetDir )
563+ if err := os .RemoveAll (targetDir ); err != nil {
564+ return fmt .Errorf ("failed to remove existing directory %s: %v" , targetDir , err )
565+ }
566+ }
567+
568+ // Create a fresh directory for the output
569+ if err := os .MkdirAll (targetDir , 0755 ); err != nil {
570+ return fmt .Errorf ("failed to create directory %s: %v" , targetDir , err )
571+ }
492572 // Clone the repository
493573 gitGroup := Info ("Cloning repository" )
494574 gitGroup .Property ("ref" , ref )
@@ -511,18 +591,9 @@ func pullOutput(output, url, ref, targetDir string) error {
511591 }
512592
513593 case "url" :
514- // Download the tar file
515- downloadGroup := Info ("Downloading archive " )
594+ // Download the file directly to current directory
595+ downloadGroup := Info ("Downloading file " )
516596 downloadGroup .Property ("url" , url )
517- downloadGroup .Property ("target" , targetDir )
518-
519- // Create a temporary file for the tar download
520- tmpFile , err := os .CreateTemp ("" , "stainless-*.tar.gz" )
521- if err != nil {
522- return fmt .Errorf ("failed to create temporary file: %v" , err )
523- }
524- defer os .Remove (tmpFile .Name ())
525- defer tmpFile .Close ()
526597
527598 // Download the file
528599 resp , err := http .Get (url )
@@ -535,19 +606,21 @@ func pullOutput(output, url, ref, targetDir string) error {
535606 return fmt .Errorf ("download failed with status: %s" , resp .Status )
536607 }
537608
538- // Copy the response body to the temporary file
539- _ , err = io .Copy (tmpFile , resp .Body )
609+ // Extract filename from URL and Content-Disposition header
610+ filename := extractFilename (url , resp )
611+ downloadGroup .Property ("filename" , filename )
612+
613+ // Create the output file in current directory
614+ outFile , err := os .Create (filename )
540615 if err != nil {
541- return fmt .Errorf ("failed to save downloaded file: %v" , err )
616+ return fmt .Errorf ("failed to create output file: %v" , err )
542617 }
543- tmpFile .Close ()
618+ defer outFile .Close ()
544619
545- // Extract the tar file
546- cmd := exec .Command ("tar" , "-xzf" , tmpFile .Name (), "-C" , targetDir )
547- cmd .Stdout = nil // Suppress tar output
548- cmd .Stderr = nil
549- if err := cmd .Run (); err != nil {
550- return fmt .Errorf ("tar extraction failed: %v" , err )
620+ // Copy the response body to the output file
621+ _ , err = io .Copy (outFile , resp .Body )
622+ if err != nil {
623+ return fmt .Errorf ("failed to save downloaded file: %v" , err )
551624 }
552625
553626 default :
0 commit comments