@@ -16,6 +16,7 @@ const KEY_PUBLIC: &str = "SSH_PUBLIC_KEY";
1616const KEY_FINGERPRINT : & str = "SSH_FINGERPRINT" ;
1717
1818pub ( crate ) const DEFAULT_KEY_NAME : & str = "default" ;
19+ const DEFAULT_SSH_MODE : & str = "force" ;
1920
2021pub fn run ( action : Option < SshAction > ) -> Result < ( ) > {
2122 match action {
@@ -31,7 +32,8 @@ pub(crate) fn ensure_default_identity(ttl_hours: u64) -> Result<()> {
3132 return Ok ( ( ) ) ;
3233 }
3334
34- unlock ( DEFAULT_KEY_NAME , ttl_hours)
35+ let key_name = configured_key_name ( ) ;
36+ unlock ( & key_name, ttl_hours)
3537}
3638
3739fn setup ( name : & str , unlock_after : bool ) -> Result < ( ) > {
@@ -82,6 +84,7 @@ fn setup(name: &str, unlock_after: bool) -> Result<()> {
8284 println ! ( "Stored SSH key in 1focus as '{}'." , key_name) ;
8385 println ! ( "Public key:\n {}" , public_key. trim( ) ) ;
8486 println ! ( "Add it to GitHub: https://github.com/settings/keys" ) ;
87+ ensure_global_ssh_config ( & key_name) ?;
8588
8689 if unlock_after {
8790 unlock ( & key_name, DEFAULT_TTL_HOURS ) ?;
@@ -170,6 +173,121 @@ fn status(name: &str) -> Result<()> {
170173 Ok ( ( ) )
171174}
172175
176+ fn ensure_global_ssh_config ( key_name : & str ) -> Result < ( ) > {
177+ let config_path = config:: default_config_path ( ) ;
178+ if let Some ( parent) = config_path. parent ( ) {
179+ fs:: create_dir_all ( parent)
180+ . with_context ( || format ! ( "failed to create {}" , parent. display( ) ) ) ?;
181+ }
182+
183+ let contents = if config_path. exists ( ) {
184+ fs:: read_to_string ( & config_path)
185+ . with_context ( || format ! ( "failed to read {}" , config_path. display( ) ) ) ?
186+ } else {
187+ String :: new ( )
188+ } ;
189+
190+ let updated = upsert_ssh_block ( & contents, DEFAULT_SSH_MODE , key_name) ;
191+ if updated != contents {
192+ fs:: write ( & config_path, updated)
193+ . with_context ( || format ! ( "failed to write {}" , config_path. display( ) ) ) ?;
194+ println ! (
195+ "Configured Flow to use SSH keys from 1focus (mode={}, key={})." ,
196+ DEFAULT_SSH_MODE , key_name
197+ ) ;
198+ }
199+
200+ Ok ( ( ) )
201+ }
202+
203+ fn upsert_ssh_block ( input : & str , mode : & str , key_name : & str ) -> String {
204+ let mut out: Vec < String > = Vec :: new ( ) ;
205+ let mut in_ssh = false ;
206+ let mut saw_ssh = false ;
207+ let mut saw_mode = false ;
208+ let mut saw_key = false ;
209+ let ends_with_newline = input. ends_with ( '\n' ) ;
210+
211+ for line in input. lines ( ) {
212+ let trimmed = line. trim ( ) ;
213+ if trimmed. starts_with ( '[' ) && trimmed. ends_with ( ']' ) {
214+ if in_ssh {
215+ if !saw_mode {
216+ out. push ( format ! ( "mode = \" {}\" " , mode) ) ;
217+ saw_mode = true ;
218+ }
219+ if !saw_key {
220+ out. push ( format ! ( "key_name = \" {}\" " , key_name) ) ;
221+ saw_key = true ;
222+ }
223+ }
224+
225+ in_ssh = trimmed == "[ssh]" ;
226+ if in_ssh {
227+ saw_ssh = true ;
228+ }
229+ out. push ( line. to_string ( ) ) ;
230+ continue ;
231+ }
232+
233+ if in_ssh {
234+ if trimmed. starts_with ( "mode" ) && trimmed. contains ( '=' ) {
235+ out. push ( format ! ( "mode = \" {}\" " , mode) ) ;
236+ saw_mode = true ;
237+ continue ;
238+ }
239+ if trimmed. starts_with ( "key_name" ) && trimmed. contains ( '=' ) {
240+ out. push ( format ! ( "key_name = \" {}\" " , key_name) ) ;
241+ saw_key = true ;
242+ continue ;
243+ }
244+ }
245+
246+ out. push ( line. to_string ( ) ) ;
247+ }
248+
249+ if in_ssh {
250+ if !saw_mode {
251+ out. push ( format ! ( "mode = \" {}\" " , mode) ) ;
252+ }
253+ if !saw_key {
254+ out. push ( format ! ( "key_name = \" {}\" " , key_name) ) ;
255+ }
256+ }
257+
258+ if !saw_ssh {
259+ if !out. is_empty ( ) {
260+ out. push ( String :: new ( ) ) ;
261+ }
262+ out. push ( "[ssh]" . to_string ( ) ) ;
263+ out. push ( format ! ( "mode = \" {}\" " , mode) ) ;
264+ out. push ( format ! ( "key_name = \" {}\" " , key_name) ) ;
265+ }
266+
267+ let mut rendered = out. join ( "\n " ) ;
268+ if ends_with_newline || rendered. is_empty ( ) {
269+ rendered. push ( '\n' ) ;
270+ }
271+ rendered
272+ }
273+
274+ fn configured_key_name ( ) -> String {
275+ let config_path = config:: default_config_path ( ) ;
276+ if config_path. exists ( ) {
277+ if let Ok ( cfg) = config:: load ( & config_path) {
278+ if let Some ( ssh_cfg) = cfg. ssh {
279+ if let Some ( name) = ssh_cfg. key_name {
280+ if !name. trim ( ) . is_empty ( ) {
281+ return name;
282+ }
283+ }
284+ }
285+ }
286+ }
287+
288+ DEFAULT_KEY_NAME . to_string ( )
289+ }
290+
173291fn key_env_keys ( name : & str ) -> ( String , String , String ) {
174292 if name == "default" {
175293 (
@@ -284,4 +402,22 @@ mod tests {
284402 assert_eq ! ( mode, 0o600 ) ;
285403 }
286404 }
405+
406+ #[ test]
407+ fn upsert_ssh_block_adds_when_missing ( ) {
408+ let updated = upsert_ssh_block ( "" , "force" , "default" ) ;
409+ assert ! ( updated. contains( "[ssh]" ) ) ;
410+ assert ! ( updated. contains( "mode = \" force\" " ) ) ;
411+ assert ! ( updated. contains( "key_name = \" default\" " ) ) ;
412+ }
413+
414+ #[ test]
415+ fn upsert_ssh_block_updates_existing_values ( ) {
416+ let input = "[ssh]\n mode = \" auto\" \n key_name = \" work\" \n " ;
417+ let updated = upsert_ssh_block ( input, "force" , "default" ) ;
418+ assert ! ( updated. contains( "mode = \" force\" " ) ) ;
419+ assert ! ( updated. contains( "key_name = \" default\" " ) ) ;
420+ assert ! ( !updated. contains( "mode = \" auto\" " ) ) ;
421+ assert ! ( !updated. contains( "key_name = \" work\" " ) ) ;
422+ }
287423}
0 commit comments