@@ -130,7 +130,23 @@ pub fn load_from(path: &Path) -> Result<Option<ConfigFile>, CliError> {
130130pub fn save_api_key ( path : & Path , api_key : & str ) -> Result < ( ) , CliError > {
131131 let mut cfg = load_from ( path) ?. unwrap_or_default ( ) ;
132132 cfg. api . key = Some ( api_key. to_string ( ) ) ;
133- let text = toml:: to_string_pretty ( & cfg) . map_err ( |e| CliError :: ConfigWrite {
133+ write_config ( path, & cfg)
134+ }
135+
136+ /// Removes the saved API key while preserving the rest of the config (output
137+ /// preferences). Used by `qn auth logout` so logging out doesn't reset
138+ /// `[output]` settings the user deliberately chose.
139+ pub fn clear_api_key ( path : & Path ) -> Result < ( ) , CliError > {
140+ let mut cfg = load_from ( path) ?. unwrap_or_default ( ) ;
141+ cfg. api . key = None ;
142+ write_config ( path, & cfg)
143+ }
144+
145+ /// Atomically writes `cfg` to `path`: serialize, write to a 0600 tempfile in the
146+ /// same directory, fsync, then rename into place. Shared by [`save_api_key`] and
147+ /// [`clear_api_key`].
148+ fn write_config ( path : & Path , cfg : & ConfigFile ) -> Result < ( ) , CliError > {
149+ let text = toml:: to_string_pretty ( cfg) . map_err ( |e| CliError :: ConfigWrite {
134150 path : path. to_path_buf ( ) ,
135151 source : std:: io:: Error :: other ( e) ,
136152 } ) ?;
@@ -196,15 +212,6 @@ pub fn save_api_key(path: &Path, api_key: &str) -> Result<(), CliError> {
196212 Ok ( ( ) )
197213}
198214
199- /// Deletes the saved config file. No error if it didn't exist.
200- pub fn delete_config ( path : & Path ) -> Result < ( ) , CliError > {
201- match fs:: remove_file ( path) {
202- Ok ( ( ) ) => Ok ( ( ) ) ,
203- Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => Ok ( ( ) ) ,
204- Err ( e) => Err ( e. into ( ) ) ,
205- }
206- }
207-
208215/// Resolves an API key per the documented precedence: flag > config file.
209216///
210217/// `allow_prompt` and `prompt` exist only so `qn auth login` can opt into the
@@ -432,14 +439,41 @@ mod tests {
432439 }
433440
434441 #[ test]
435- fn delete_is_idempotent ( ) {
442+ fn clear_api_key_preserves_output_section ( ) {
443+ let dir = tempdir ( ) . unwrap ( ) ;
444+ let path = dir. path ( ) . join ( "config.toml" ) ;
445+ std:: fs:: write (
446+ & path,
447+ "[api]\n key = \" k\" \n \n [output]\n format = \" json\" \n wide = true\n " ,
448+ )
449+ . unwrap ( ) ;
450+ clear_api_key ( & path) . unwrap ( ) ;
451+ let cfg = load_from ( & path) . unwrap ( ) . unwrap ( ) ;
452+ assert_eq ! ( cfg. api. key, None ) ;
453+ assert_eq ! ( cfg. output. format, Some ( Format :: Json ) ) ;
454+ assert ! ( cfg. output. wide) ;
455+ }
456+
457+ #[ test]
458+ fn clear_api_key_with_no_existing_file_writes_defaults ( ) {
459+ let dir = tempdir ( ) . unwrap ( ) ;
460+ let path = dir. path ( ) . join ( "config.toml" ) ;
461+ clear_api_key ( & path) . unwrap ( ) ;
462+ assert ! ( path. exists( ) ) ;
463+ let cfg = load_from ( & path) . unwrap ( ) . unwrap ( ) ;
464+ assert_eq ! ( cfg. api. key, None ) ;
465+ }
466+
467+ #[ cfg( unix) ]
468+ #[ test]
469+ fn clear_api_key_writes_mode_0600 ( ) {
470+ use std:: os:: unix:: fs:: PermissionsExt ;
436471 let dir = tempdir ( ) . unwrap ( ) ;
437472 let path = dir. path ( ) . join ( "config.toml" ) ;
438- delete_config ( & path) . unwrap ( ) ; // no file yet
439- save_api_key ( & path, "k" ) . unwrap ( ) ;
440- delete_config ( & path) . unwrap ( ) ;
441- delete_config ( & path) . unwrap ( ) ; // already gone
442- assert ! ( !path. exists( ) ) ;
473+ save_api_key ( & path, "secret" ) . unwrap ( ) ;
474+ clear_api_key ( & path) . unwrap ( ) ;
475+ let mode = fs:: metadata ( & path) . unwrap ( ) . permissions ( ) . mode ( ) & 0o777 ;
476+ assert_eq ! ( mode, 0o600 , "expected 0600, got {mode:o}" ) ;
443477 }
444478
445479 #[ cfg( unix) ]
0 commit comments