@@ -8,12 +8,15 @@ import (
88
99 "github.com/madLinux7/dssh/internal/db"
1010 "github.com/madLinux7/dssh/internal/model"
11+ "github.com/madLinux7/dssh/internal/sshconfig"
1112 "github.com/spf13/cobra"
1213)
1314
1415func newAddCmd () * cobra.Command {
1516 var port int
1617 var directory string
18+ var addToSQLite bool
19+ var addToSSHConfig bool
1720
1821 cmd := & cobra.Command {
1922 Use : "add [-p PORT] [-d DIR] NAME target [password]" ,
@@ -28,6 +31,7 @@ and the password is encrypted with your master passphrase.`,
2831 return err
2932 }
3033 target := args [1 ]
34+ hasPassword := len (args ) == 3
3135
3236 user , host , parsedPort , err := parseTarget (target )
3337 if err != nil {
@@ -41,12 +45,6 @@ and the password is encrypted with your master passphrase.`,
4145 parsedPort = 22
4246 }
4347
44- d , err := db .Open ()
45- if err != nil {
46- return err
47- }
48- defer d .Close ()
49-
5048 conn := & model.Connection {
5149 Name : name ,
5250 User : user ,
@@ -56,24 +54,52 @@ and the password is encrypted with your master passphrase.`,
5654 AuthType : model .AuthKey ,
5755 }
5856
59- // Optional password argument → password auth.
60- if len (args ) == 3 {
57+ // Determine save target.
58+ saveTarget := resolveAddTarget (addToSQLite , addToSSHConfig , hasPassword )
59+ if saveTarget == "" {
60+ return nil // user aborted
61+ }
62+
63+ // Password handling.
64+ if hasPassword {
65+ if saveTarget == model .SaveTargetSSHConfig {
66+ errMsg ("Passwords can only be saved to SQLite. Use %s or %s mode." ,
67+ magentaText ("--sqlite" ), magentaText ("sqlite_only" ))
68+ return nil
69+ }
6170 password := args [2 ]
6271 conn .AuthType = model .AuthPassword
63-
64- encPass , nonce , err := encryptPassword (d , password )
72+ encPass , nonce , err := encryptPassword (sharedDB , password )
6573 if err != nil {
6674 return err
6775 }
6876 conn .EncryptedPass = encPass
6977 conn .PassNonce = nonce
7078 }
7179
72- if err := db .Insert (d , conn ); err != nil {
73- return err
80+ // Cross-source duplicate check in "both" mode.
81+ if runtimeCfg .ParseMode == model .ParseModeBoth {
82+ if err := checkCrossSourceDuplicate (name , saveTarget ); err != nil {
83+ return err
84+ }
7485 }
7586
76- success ("Added connection %q (%s@%s:%d)" , name , user , host , parsedPort )
87+ // Save to the chosen target.
88+ if saveTarget == model .SaveTargetSSHConfig {
89+ p , err := sshConfigPath ()
90+ if err != nil {
91+ return err
92+ }
93+ if err := sshconfig .Insert (p , conn ); err != nil {
94+ return err
95+ }
96+ success ("Added connection %q to ssh_config (%s@%s:%d)" , name , user , host , parsedPort )
97+ } else {
98+ if err := db .Insert (sharedDB , conn ); err != nil {
99+ return err
100+ }
101+ success ("Added connection %q to SQLite (%s@%s:%d)" , name , user , host , parsedPort )
102+ }
77103 return nil
78104 },
79105 }
@@ -82,9 +108,82 @@ and the password is encrypted with your master passphrase.`,
82108 cmd .Flags ().StringVarP (& directory , "directory" , "d" , "" , "Remote directory to cd into on connect" )
83109 cmd .Flags ().StringVar (& directory , "cd" , "" , "Alias for --directory" )
84110 cmd .Flags ().MarkHidden ("cd" )
111+ cmd .Flags ().BoolVar (& addToSQLite , "sqlite" , false , "Save to SQLite" )
112+ cmd .Flags ().BoolVar (& addToSSHConfig , "sshconfig" , false , "Save to ssh_config" )
85113 return cmd
86114}
87115
116+ // resolveAddTarget determines where to save based on flags and mode.
117+ // Returns empty string if user aborted.
118+ func resolveAddTarget (flagSQL , flagSSH , hasPassword bool ) model.SaveTarget {
119+ mode := runtimeCfg .ParseMode
120+
121+ // Explicit flags take priority.
122+ if flagSQL {
123+ return model .SaveTargetSQLite
124+ }
125+ if flagSSH {
126+ return model .SaveTargetSSHConfig
127+ }
128+
129+ switch mode {
130+ case model .ParseModeSSHConfigOnly :
131+ return model .SaveTargetSSHConfig
132+ case model .ParseModeBoth :
133+ if hasPassword {
134+ fmt .Println ("Passwords can only be saved to SQLite." )
135+ choice := radioPrompt ("Save to SQLite?" , []string {"Yes" , "Abort" })
136+ if choice == 0 {
137+ return model .SaveTargetSQLite
138+ }
139+ return ""
140+ }
141+ // Prompt with radio buttons, pre-select the default.
142+ options := []string {"SQLite" , "ssh_config" }
143+ choice := radioPrompt ("Save to:" , options )
144+ switch choice {
145+ case 0 :
146+ return model .SaveTargetSQLite
147+ case 1 :
148+ return model .SaveTargetSSHConfig
149+ default :
150+ return ""
151+ }
152+ default : // sqlite_only
153+ return model .SaveTargetSQLite
154+ }
155+ }
156+
157+ // checkCrossSourceDuplicate warns if the name exists in the other source.
158+ func checkCrossSourceDuplicate (name string , target model.SaveTarget ) error {
159+ sqlConn , sshConn , err := getConnectionSources (name )
160+ if err != nil {
161+ return err
162+ }
163+ if sqlConn != nil && sshConn != nil {
164+ return fmt .Errorf ("connection %q already exists in both SQLite and ssh_config" , name )
165+ }
166+ if target == model .SaveTargetSQLite && sshConn != nil {
167+ fmt .Printf ("Connection %q already exists in ssh_config.\n " , name )
168+ choice := radioPrompt ("Save to SQLite anyway?" , []string {"Yes" , "Abort" })
169+ if choice != 0 {
170+ return fmt .Errorf ("aborted" )
171+ }
172+ }
173+ if target == model .SaveTargetSSHConfig && sqlConn != nil {
174+ fmt .Printf ("Connection %q already exists in SQLite.\n " , name )
175+ choice := radioPrompt ("Save to ssh_config anyway?" , []string {"Yes" , "Abort" })
176+ if choice != 0 {
177+ return fmt .Errorf ("aborted" )
178+ }
179+ }
180+ return nil
181+ }
182+
183+ func magentaText (s string ) string {
184+ return fmt .Sprintf ("\033 [35m%s\033 [0m" , s )
185+ }
186+
88187// parseTarget parses user@host or ssh://user@host:port into components.
89188func parseTarget (target string ) (user , host string , port int , err error ) {
90189 // Try ssh:// URI first.
0 commit comments