11package cmd
22
33import (
4+ "bufio"
45 "fmt"
56 "os"
7+ "os/exec"
68 "path/filepath"
9+ "strings"
710
811 "github.com/shipbase/anycli/definitions"
912 "github.com/shipbase/anycli/internal/config"
@@ -19,8 +22,10 @@ var installCmd = &cobra.Command{
1922 Args : cobra .ExactArgs (1 ),
2023 RunE : func (cmd * cobra.Command , args []string ) error {
2124 name := args [0 ]
25+ yes , _ := cmd .Flags ().GetBool ("yes" )
26+ mode , _ := cmd .Flags ().GetString ("mode" )
2227
23- // Check if already installed
28+ // Check if already installed in anycli
2429 if _ , err := registry .Load (name ); err == nil {
2530 fmt .Printf ("%s is already installed\n " , name )
2631 return nil
@@ -43,13 +48,46 @@ var installCmd = &cobra.Command{
4348 def = d
4449 }
4550
46- // Download the real binary if source is configured
51+ // Check if the tool already exists in PATH
52+ if mode == "" {
53+ existingPath , err := findExistingBinary (name )
54+ if err == nil && existingPath != "" {
55+ if yes {
56+ // Non-interactive: default to link
57+ mode = "link"
58+ fmt .Printf ("found existing %s at %s, linking\n " , name , existingPath )
59+ } else {
60+ m , err := promptConflict (name , existingPath )
61+ if err != nil {
62+ return err
63+ }
64+ mode = m
65+ }
66+ }
67+ }
68+
69+ if mode == "abort" {
70+ fmt .Println ("installation aborted" )
71+ return nil
72+ }
73+
74+ if mode == "link" {
75+ // Link mode: wrap existing binary, skip download
76+ existingPath , err := findExistingBinary (name )
77+ if err != nil {
78+ return fmt .Errorf ("cannot find existing %s: %w" , name , err )
79+ }
80+ def .Resolve = existingPath
81+ def .Source = nil // don't download
82+ fmt .Printf ("linking to existing %s at %s\n " , name , existingPath )
83+ }
84+
85+ // Download the real binary if source is configured (override mode or no conflict)
4786 if def .Source != nil {
4887 result , err := installer .Install (def )
4988 if err != nil {
5089 return fmt .Errorf ("failed to install %s: %w" , name , err )
5190 }
52- // Set resolve to the installed binary path
5391 def .Resolve = result .BinaryPath
5492 fmt .Printf ("downloaded %s v%s\n " , name , result .Version )
5593 }
@@ -69,6 +107,46 @@ var installCmd = &cobra.Command{
69107 },
70108}
71109
110+ // findExistingBinary searches PATH for an existing binary, skipping the anycli shim dir.
111+ func findExistingBinary (name string ) (string , error ) {
112+ shimDir := config .BinDir ()
113+ pathEnv := os .Getenv ("PATH" )
114+ for _ , dir := range filepath .SplitList (pathEnv ) {
115+ if dir == shimDir {
116+ continue
117+ }
118+ candidate := filepath .Join (dir , name )
119+ if info , err := os .Stat (candidate ); err == nil && ! info .IsDir () {
120+ return candidate , nil
121+ }
122+ }
123+ return "" , exec .ErrNotFound
124+ }
125+
126+ // promptConflict asks the user how to handle an existing binary.
127+ func promptConflict (name , existingPath string ) (string , error ) {
128+ fmt .Printf ("found existing %s at %s\n " , name , existingPath )
129+ fmt .Println (" [o]verride - download new binary, replace existing" )
130+ fmt .Println (" [l]ink - wrap existing binary with anycli middleware" )
131+ fmt .Println (" [a]bort - cancel installation" )
132+ fmt .Print ("choose [o/l/a]: " )
133+
134+ reader := bufio .NewReader (os .Stdin )
135+ input , _ := reader .ReadString ('\n' )
136+ input = strings .TrimSpace (strings .ToLower (input ))
137+
138+ switch input {
139+ case "o" , "override" :
140+ return "override" , nil
141+ case "l" , "link" :
142+ return "link" , nil
143+ case "a" , "abort" , "" :
144+ return "abort" , nil
145+ default :
146+ return "abort" , nil
147+ }
148+ }
149+
72150func loadFromFile (path string ) (* registry.Definition , error ) {
73151 data , err := os .ReadFile (path )
74152 if err != nil {
@@ -108,5 +186,7 @@ func createShim(name string) error {
108186
109187func init () {
110188 installCmd .Flags ().String ("from" , "" , "install from a local JSON definition file" )
189+ installCmd .Flags ().StringP ("mode" , "m" , "" , "conflict resolution: override, link, or abort" )
190+ installCmd .Flags ().BoolP ("yes" , "y" , false , "non-interactive mode (defaults to link on conflict)" )
111191 rootCmd .AddCommand (installCmd )
112192}
0 commit comments