Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/commands/agent/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Non-interactive paths:
- Pass `--api-key <KEY>` on every invocation, or
- Write the key once: `qn auth login --api-key <KEY>` (saves the config file).

`qn auth logout` removes the saved API key but preserves your `[output]`
preferences in the config file.

Config file location:

- Linux/macOS: `$XDG_CONFIG_HOME/qn/config.toml`, else `~/.config/qn/config.toml`.
Expand Down
2 changes: 1 addition & 1 deletion src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ fn logout(global: GlobalArgs) -> Result<(), CliError> {
.to_string(),
)
})?;
config::delete_config(&path)?;
config::clear_api_key(&path)?;
if !global.quiet {
let _ = writeln!(std::io::stderr(), "✓ Removed saved API key");
}
Expand Down
66 changes: 50 additions & 16 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,23 @@ pub fn load_from(path: &Path) -> Result<Option<ConfigFile>, CliError> {
pub fn save_api_key(path: &Path, api_key: &str) -> Result<(), CliError> {
let mut cfg = load_from(path)?.unwrap_or_default();
cfg.api.key = Some(api_key.to_string());
let text = toml::to_string_pretty(&cfg).map_err(|e| CliError::ConfigWrite {
write_config(path, &cfg)
}

/// Removes the saved API key while preserving the rest of the config (output
/// preferences). Used by `qn auth logout` so logging out doesn't reset
/// `[output]` settings the user deliberately chose.
pub fn clear_api_key(path: &Path) -> Result<(), CliError> {
let mut cfg = load_from(path)?.unwrap_or_default();
cfg.api.key = None;
write_config(path, &cfg)
}

/// Atomically writes `cfg` to `path`: serialize, write to a 0600 tempfile in the
/// same directory, fsync, then rename into place. Shared by [`save_api_key`] and
/// [`clear_api_key`].
fn write_config(path: &Path, cfg: &ConfigFile) -> Result<(), CliError> {
let text = toml::to_string_pretty(cfg).map_err(|e| CliError::ConfigWrite {
path: path.to_path_buf(),
source: std::io::Error::other(e),
})?;
Expand Down Expand Up @@ -196,15 +212,6 @@ pub fn save_api_key(path: &Path, api_key: &str) -> Result<(), CliError> {
Ok(())
}

/// Deletes the saved config file. No error if it didn't exist.
pub fn delete_config(path: &Path) -> Result<(), CliError> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.into()),
}
}

/// Resolves an API key per the documented precedence: flag > config file.
///
/// `allow_prompt` and `prompt` exist only so `qn auth login` can opt into the
Expand Down Expand Up @@ -432,14 +439,41 @@ mod tests {
}

#[test]
fn delete_is_idempotent() {
fn clear_api_key_preserves_output_section() {
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
"[api]\nkey = \"k\"\n\n[output]\nformat = \"json\"\nwide = true\n",
)
.unwrap();
clear_api_key(&path).unwrap();
let cfg = load_from(&path).unwrap().unwrap();
assert_eq!(cfg.api.key, None);
assert_eq!(cfg.output.format, Some(Format::Json));
assert!(cfg.output.wide);
}

#[test]
fn clear_api_key_with_no_existing_file_writes_defaults() {
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
clear_api_key(&path).unwrap();
assert!(path.exists());
let cfg = load_from(&path).unwrap().unwrap();
assert_eq!(cfg.api.key, None);
}

#[cfg(unix)]
#[test]
fn clear_api_key_writes_mode_0600() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
delete_config(&path).unwrap(); // no file yet
save_api_key(&path, "k").unwrap();
delete_config(&path).unwrap();
delete_config(&path).unwrap(); // already gone
assert!(!path.exists());
save_api_key(&path, "secret").unwrap();
clear_api_key(&path).unwrap();
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "expected 0600, got {mode:o}");
}

#[cfg(unix)]
Expand Down
Loading