never executed always true always false
    1 module PureClaw.CLI.Config
    2   ( -- * File config
    3     FileConfig (..)
    4   , emptyFileConfig
    5     -- * Loading
    6   , loadFileConfig
    7   , loadConfig
    8     -- * Directory helpers
    9   , getPureclawDir
   10   ) where
   11 
   12 import Control.Exception
   13 import Data.Text (Text)
   14 import Data.Text.IO qualified as TIO
   15 import System.Directory (getHomeDirectory)
   16 import System.FilePath ((</>))
   17 import Toml (TomlCodec, (.=))
   18 import Toml qualified
   19 
   20 -- | Configuration that can be read from a TOML file.
   21 -- All fields are optional — missing fields default to Nothing.
   22 data FileConfig = FileConfig
   23   { _fc_apiKey         :: Maybe Text
   24   , _fc_model          :: Maybe Text
   25   , _fc_provider       :: Maybe Text
   26   , _fc_system         :: Maybe Text
   27   , _fc_memory         :: Maybe Text
   28   , _fc_allow          :: Maybe [Text]
   29   , _fc_vault_path      :: Maybe Text  -- ^ vault file path (default: ~/.pureclaw/vault.age)
   30   , _fc_vault_recipient :: Maybe Text  -- ^ age recipient string (required to enable vault)
   31   , _fc_vault_identity  :: Maybe Text  -- ^ age identity path or plugin string
   32   , _fc_vault_unlock    :: Maybe Text  -- ^ "startup", "on_demand", or "per_access"
   33   } deriving stock (Show, Eq)
   34 
   35 emptyFileConfig :: FileConfig
   36 emptyFileConfig =
   37   FileConfig Nothing Nothing Nothing Nothing Nothing Nothing
   38              Nothing Nothing Nothing Nothing
   39 
   40 fileConfigCodec :: TomlCodec FileConfig
   41 fileConfigCodec = FileConfig
   42   <$> Toml.dioptional (Toml.text "api_key")                   .= _fc_apiKey
   43   <*> Toml.dioptional (Toml.text "model")                     .= _fc_model
   44   <*> Toml.dioptional (Toml.text "provider")                  .= _fc_provider
   45   <*> Toml.dioptional (Toml.text "system")                    .= _fc_system
   46   <*> Toml.dioptional (Toml.text "memory")                    .= _fc_memory
   47   <*> Toml.dioptional (Toml.arrayOf Toml._Text "allow")       .= _fc_allow
   48   <*> Toml.dioptional (Toml.text "vault_path")                .= _fc_vault_path
   49   <*> Toml.dioptional (Toml.text "vault_recipient")           .= _fc_vault_recipient
   50   <*> Toml.dioptional (Toml.text "vault_identity")            .= _fc_vault_identity
   51   <*> Toml.dioptional (Toml.text "vault_unlock")              .= _fc_vault_unlock
   52 
   53 -- | Load config from a single file path.
   54 -- Returns 'emptyFileConfig' if the file does not exist or cannot be parsed.
   55 loadFileConfig :: FilePath -> IO FileConfig
   56 loadFileConfig path = do
   57   text <- try @IOError (TIO.readFile path)
   58   pure $ case text of
   59     Left  _    -> emptyFileConfig
   60     Right toml -> case Toml.decode fileConfigCodec toml of
   61       Left  _ -> emptyFileConfig
   62       Right c -> c
   63 
   64 -- | The PureClaw home directory: @~\/.pureclaw@.
   65 -- This is where config, memory, and vault files are stored by default.
   66 getPureclawDir :: IO FilePath
   67 getPureclawDir = do
   68   home <- getHomeDirectory
   69   pure (home </> ".pureclaw")
   70 
   71 -- | Load config from the default locations, trying each in order:
   72 --
   73 -- 1. @~\/.pureclaw\/config.toml@ (user home)
   74 -- 2. @~\/.config\/pureclaw\/config.toml@ (XDG fallback)
   75 --
   76 -- Returns the first config found, or 'emptyFileConfig' if none exists.
   77 loadConfig :: IO FileConfig
   78 loadConfig = do
   79   home <- try @IOError getHomeDirectory
   80   case home of
   81     Left  _ -> pure emptyFileConfig
   82     Right h -> do
   83       homeCfg <- loadFileConfig (h </> ".pureclaw" </> "config.toml")
   84       if homeCfg /= emptyFileConfig
   85         then pure homeCfg
   86         else loadFileConfig (h </> ".config" </> "pureclaw" </> "config.toml")