packages feed

clod-0.2.1: app/Main.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Module      : Main
-- Description : Main entry point for the Clod application
-- Copyright   : (c) Fuzz Leonard, 2025
-- License     : MIT
-- Maintainer  : ink@fuzz.ink
-- Stability   : experimental
--
-- This module provides the main CLI interface for the Clod application.

module Main where

import Options.Applicative
import System.Exit (exitFailure)
import System.Directory (createDirectoryIfMissing, getCurrentDirectory, getTemporaryDirectory, 
                         doesFileExist, doesDirectoryExist)
import System.FilePath ((</>))
import System.IO (stderr, hPutStrLn)
import Data.Time (getCurrentTime)
import Data.Time.Format (formatTime, defaultTimeLocale)
import Data.Hashable (hash)
import Control.Exception (try, IOException)
import Data.List (isInfixOf)
import Data.Version (showVersion)
import qualified Paths_clod as Meta

import Clod.Core (runClodApp)
import Clod.Types (ClodConfig(..))
import qualified Clod.Types as T

-- | Command line options for Clod
data Options = Options
  { optStagingDir  :: String   -- ^ Directory where files will be staged (test mode only)
  , optAllFiles    :: Bool     -- ^ Import all files (otherwise imports only modified files)
  , optTestMode    :: Bool     -- ^ Run in test mode
  , optVerbose     :: Bool     -- ^ Enable verbose output
  , optFlush       :: Bool     -- ^ Flush stale entries from the database
  , optLast        :: Bool     -- ^ Use previous staging directory
  } deriving (Show)

-- | Parser for command line options
optionsParser :: Parser Options
optionsParser = Options
  <$> strOption
      ( long "staging-dir"
     <> short 'd'
     <> metavar "DIR"
     <> help "Directory where files will be staged for Claude (only used in test mode)"
     <> value ""
     <> showDefault )
  <*> switch
      ( long "all"
     <> short 'a'
     <> help "Import all files (respecting .gitignore)" )
  <*> switch
      ( long "test"
     <> short 't'
     <> help "Run in test mode" )
  <*> switch
      ( long "verbose"
     <> short 'v'
     <> help "Enable verbose output" )
  <*> switch
      ( long "flush"
     <> short 'f'
     <> help "Flush missing entries from the database" )
  <*> switch
      ( long "last"
     <> short 'l'
     <> help "Use previous staging directory" )

-- | Main entry point
main :: IO ()
main = do
  options <- execParser opts
  
  -- Create a minimal configuration for the effects system
  currentDir <- getCurrentDirectory
  
  -- For config dir: use local .clod in project directory
  let configDirPath = currentDir </> ".clod"
  
  -- For staging directory: use system temp directory
  tempDir <- getTemporaryDirectory
  let stagingDirBase = tempDir </> "clod-staging"
  
  -- Create a unique staging directory for this run
  timestamp <- formatTime defaultTimeLocale "%Y%m%d%H%M%S" <$> getCurrentTime
  let uniqueId = take 8 $ show $ hash $ currentDir ++ timestamp
  let stagingDirPath = if null (optStagingDir options) 
                    then stagingDirBase </> uniqueId
                    else optStagingDir options
  
  -- Load previous staging directory if in "last" mode
  let dbPath = configDirPath </> "db.dhall"
  previousDir <- if optLast options
                 then do
                   dbExists <- doesFileExist dbPath
                   if dbExists
                     then do
                       -- Try to read the database to get the previous staging dir
                       edb <- try $ do
                         content <- readFile dbPath
                         -- Look for lastStagingDir pattern in the Dhall file
                         let contentLines = filter (\l -> "lastStagingDir" `isInfixOf` l) $ lines content
                         case contentLines of
                           (line:_) -> 
                             if "None" `isInfixOf` line
                               then return Nothing 
                               else do
                                 -- Extract path from "Some ./path/to/dir" format
                                 let parts = words line
                                 let path = if length parts > 2 
                                            then read (last parts) :: String
                                            else ""
                                 return $ if null path then Nothing else Just path
                           _ -> return Nothing
                         :: IO (Either IOException (Maybe FilePath))
                       case edb of
                         Left _ -> return Nothing
                         Right mbDir -> return mbDir
                     else return Nothing
                 else return Nothing
  
  -- Use previous staging dir if requested and available
  finalStagingPath <- case (optLast options, previousDir) of
                        (True, Just prevDir) -> do
                          -- Check if the previous directory still exists
                          dirExists <- doesDirectoryExist prevDir
                          return $ if dirExists 
                                   then prevDir
                                   else stagingDirPath
                        _ -> return stagingDirPath
  
  do
      -- Create staging directory if it doesn't exist
      createDirectoryIfMissing True finalStagingPath
      createDirectoryIfMissing True configDirPath
      
      -- Create a basic config
      let config = ClodConfig "" "" "" "" "" "" Nothing False False False False []
            T.& T.projectPath T..~ currentDir
            T.& T.stagingDir T..~ finalStagingPath
            T.& T.configDir T..~ configDirPath
            T.& T.databaseFile T..~ dbPath
            T.& T.timestamp T..~ ""  -- Will be set internally
            T.& T.currentStaging T..~ finalStagingPath
            T.& T.previousStaging T..~ previousDir
            T.& T.testMode T..~ optTestMode options
            T.& T.verbose T..~ optVerbose options
            T.& T.flushMode T..~ optFlush options
            T.& T.lastMode T..~ optLast options
            T.& T.ignorePatterns T..~ []  -- Will be populated
      
      -- Run with effects system
      result <- runClodApp config 
                  (optStagingDir options)
                  (optVerbose options)
                  (optAllFiles options)
      
      case result of
        Left err -> do
          hPutStrLn stderr $ "Error: " ++ show err
          exitFailure
        Right _ -> return ()
  where
    opts = info (optionsParser <**> helper <**> version)
      ( fullDesc
     <> progDesc "Prepare files from a git repository for upload to Claude's Project Knowledge"
     <> header "clod - Claude Git Project File Uploader" )
    version = infoOption (showVersion Meta.version)
      ( long "version"
     <> short 'V'
     <> help "Show version information" )