@@ -15,7 +15,12 @@ use serde::Deserialize;
1515
1616use crate :: error:: KagiError ;
1717
18- const DEFAULT_CONFIG_PATH : & str = ".kagi.toml" ;
18+ /// Environment variable that overrides the full path to the config file.
19+ pub const CONFIG_PATH_ENV : & str = "KAGI_CONFIG" ;
20+ /// Directory name, under the XDG config home, that holds the config file.
21+ const CONFIG_DIR_NAME : & str = "kagi-cli" ;
22+ /// File name of the config file within the config directory.
23+ const CONFIG_FILE_NAME : & str = "config.toml" ;
1924pub const API_KEY_ENV : & str = "KAGI_API_KEY" ;
2025pub const API_TOKEN_ENV : & str = "KAGI_API_TOKEN" ;
2126pub const SESSION_TOKEN_ENV : & str = "KAGI_SESSION_TOKEN" ;
@@ -262,6 +267,41 @@ pub struct ConfigAuthSnapshot {
262267 pub search_preference : SearchAuthPreference ,
263268}
264269
270+ /// Resolves the path to the kagi config file.
271+ ///
272+ /// Resolution order:
273+ /// 1. The `KAGI_CONFIG` environment variable, used verbatim as the full file
274+ /// path when set to a non-empty value.
275+ /// 2. `$XDG_CONFIG_HOME/kagi-cli/config.toml`.
276+ /// 3. `$HOME/.config/kagi-cli/config.toml` when `XDG_CONFIG_HOME` is unset.
277+ pub fn default_config_path ( ) -> PathBuf {
278+ if let Some ( path) = env:: var_os ( CONFIG_PATH_ENV )
279+ . map ( PathBuf :: from)
280+ . filter ( |value| !value. as_os_str ( ) . is_empty ( ) )
281+ {
282+ return path;
283+ }
284+
285+ xdg_config_home ( )
286+ . join ( CONFIG_DIR_NAME )
287+ . join ( CONFIG_FILE_NAME )
288+ }
289+
290+ fn xdg_config_home ( ) -> PathBuf {
291+ env:: var_os ( "XDG_CONFIG_HOME" )
292+ . map ( PathBuf :: from)
293+ . filter ( |value| !value. as_os_str ( ) . is_empty ( ) )
294+ . unwrap_or_else ( || home_dir ( ) . join ( ".config" ) )
295+ }
296+
297+ fn home_dir ( ) -> PathBuf {
298+ env:: var_os ( "HOME" )
299+ . or_else ( || env:: var_os ( "USERPROFILE" ) )
300+ . map ( PathBuf :: from)
301+ . filter ( |value| !value. as_os_str ( ) . is_empty ( ) )
302+ . unwrap_or_else ( || PathBuf :: from ( "." ) )
303+ }
304+
265305/// Loads the credential inventory from the default config path and environment variables.
266306///
267307/// # Returns
@@ -282,7 +322,7 @@ pub fn load_credential_inventory() -> Result<CredentialInventory, KagiError> {
282322pub fn load_credential_inventory_for_profile (
283323 profile : Option < & str > ,
284324) -> Result < CredentialInventory , KagiError > {
285- load_credential_inventory_from_path ( Path :: new ( DEFAULT_CONFIG_PATH ) , profile)
325+ load_credential_inventory_from_path ( & default_config_path ( ) , profile)
286326}
287327
288328fn load_credential_inventory_from_path (
@@ -388,7 +428,7 @@ pub fn format_status(inventory: &CredentialInventory) -> String {
388428/// # Errors
389429/// Returns `KagiError::Config` if the config file cannot be read or parsed.
390430pub fn load_config_auth_snapshot ( ) -> Result < ConfigAuthSnapshot , KagiError > {
391- load_config_auth_snapshot_from_path ( Path :: new ( DEFAULT_CONFIG_PATH ) )
431+ load_config_auth_snapshot_from_path ( & default_config_path ( ) )
392432}
393433
394434fn load_config_auth_snapshot_from_path (
@@ -464,7 +504,7 @@ fn select_auth_config<'a>(
464504 . and_then ( |profiles| profiles. get ( profile) )
465505 . ok_or_else ( || {
466506 KagiError :: Config ( format ! (
467- "profile `{profile}` was not found in . kagi.toml . Run `kagi auth status --profile {profile}` to confirm the name, or add [profiles.{profile}.auth]"
507+ "profile `{profile}` was not found in the kagi config file . Run `kagi auth status --profile {profile}` to confirm the name, or add [profiles.{profile}.auth]"
468508 ) )
469509 } ) ?;
470510
@@ -593,7 +633,7 @@ fn save_credentials_with_preference_for_profile(
593633 preferred_auth : Option < SearchAuthPreference > ,
594634) -> Result < CredentialInventory , KagiError > {
595635 save_credentials_with_preference_to_path (
596- Path :: new ( DEFAULT_CONFIG_PATH ) ,
636+ & default_config_path ( ) ,
597637 profile,
598638 api_key,
599639 api_token,
@@ -733,6 +773,14 @@ fn read_config_file(path: &Path) -> Result<ConfigFile, KagiError> {
733773
734774fn write_config_file_atomically ( path : & Path , raw : & str ) -> Result < ( ) , KagiError > {
735775 let parent = path. parent ( ) . unwrap_or_else ( || Path :: new ( "." ) ) ;
776+ if !parent. as_os_str ( ) . is_empty ( ) {
777+ fs:: create_dir_all ( parent) . map_err ( |error| {
778+ KagiError :: Config ( format ! (
779+ "failed to create config directory {}: {error}" ,
780+ parent. display( )
781+ ) )
782+ } ) ?;
783+ }
736784 let file_name = path
737785 . file_name ( )
738786 . map ( |value| value. to_string_lossy ( ) . into_owned ( ) )
@@ -921,7 +969,7 @@ mod tests {
921969 api_token : None ,
922970 session_token : None ,
923971 search_preference : SearchAuthPreference :: Session ,
924- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
972+ config_path : default_config_path ( ) ,
925973 profile : None ,
926974 } ;
927975
@@ -943,7 +991,7 @@ mod tests {
943991 api_token : None ,
944992 session_token : None ,
945993 search_preference : SearchAuthPreference :: Session ,
946- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
994+ config_path : default_config_path ( ) ,
947995 profile : None ,
948996 } ;
949997
@@ -969,7 +1017,7 @@ mod tests {
9691017 value : "session" . to_string ( ) ,
9701018 } ) ,
9711019 search_preference : SearchAuthPreference :: Session ,
972- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
1020+ config_path : default_config_path ( ) ,
9731021 profile : None ,
9741022 } ;
9751023
@@ -995,7 +1043,7 @@ mod tests {
9951043 value : "session" . to_string ( ) ,
9961044 } ) ,
9971045 search_preference : SearchAuthPreference :: Session ,
998- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
1046+ config_path : default_config_path ( ) ,
9991047 profile : None ,
10001048 } ;
10011049
@@ -1020,7 +1068,7 @@ mod tests {
10201068 value : "session" . to_string ( ) ,
10211069 } ) ,
10221070 search_preference : SearchAuthPreference :: Api ,
1023- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
1071+ config_path : default_config_path ( ) ,
10241072 profile : None ,
10251073 } ;
10261074
@@ -1041,7 +1089,7 @@ mod tests {
10411089 } ) ,
10421090 session_token : None ,
10431091 search_preference : SearchAuthPreference :: Api ,
1044- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
1092+ config_path : default_config_path ( ) ,
10451093 profile : None ,
10461094 } ;
10471095
@@ -1085,7 +1133,7 @@ mod tests {
10851133 } ) ,
10861134 session_token : None ,
10871135 search_preference : SearchAuthPreference :: Session ,
1088- config_path : PathBuf :: from ( DEFAULT_CONFIG_PATH ) ,
1136+ config_path : default_config_path ( ) ,
10891137 profile : None ,
10901138 } ;
10911139
@@ -1241,4 +1289,41 @@ mod tests {
12411289 assert_eq ! ( mode, 0o600 ) ;
12421290 }
12431291 }
1292+
1293+ #[ test]
1294+ fn config_path_prefers_explicit_override ( ) {
1295+ let _guard = lock_env ( ) ;
1296+ let _override = set_env_var ( CONFIG_PATH_ENV , "/tmp/custom/kagi-config.toml" ) ;
1297+
1298+ assert_eq ! (
1299+ default_config_path( ) ,
1300+ PathBuf :: from( "/tmp/custom/kagi-config.toml" )
1301+ ) ;
1302+ }
1303+
1304+ #[ test]
1305+ fn config_path_uses_xdg_config_home_when_no_override ( ) {
1306+ let _guard = lock_env ( ) ;
1307+ let _override = set_env_var ( CONFIG_PATH_ENV , "" ) ;
1308+ let _xdg = set_env_var ( "XDG_CONFIG_HOME" , "/tmp/xdg-config" ) ;
1309+
1310+ let expected = PathBuf :: from ( "/tmp/xdg-config" )
1311+ . join ( "kagi-cli" )
1312+ . join ( "config.toml" ) ;
1313+ assert_eq ! ( default_config_path( ) , expected) ;
1314+ }
1315+
1316+ #[ test]
1317+ fn config_path_falls_back_to_home_config_dir ( ) {
1318+ let _guard = lock_env ( ) ;
1319+ let _override = set_env_var ( CONFIG_PATH_ENV , "" ) ;
1320+ let _xdg = set_env_var ( "XDG_CONFIG_HOME" , "" ) ;
1321+ let _home = set_env_var ( "HOME" , "/home/tester" ) ;
1322+
1323+ let expected = PathBuf :: from ( "/home/tester" )
1324+ . join ( ".config" )
1325+ . join ( "kagi-cli" )
1326+ . join ( "config.toml" ) ;
1327+ assert_eq ! ( default_config_path( ) , expected) ;
1328+ }
12441329}
0 commit comments