packages feed

valiant-cli-0.1.0.0: src/Valiant/CLI/Command/Watch.hs

module Valiant.CLI.Command.Watch
  ( runWatch
  ) where

import Control.Concurrent (threadDelay)
import Control.Monad (forever, unless)
import Data.IORef
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Valiant.CLI.Command.Prepare (runPrepare)
import Valiant.CLI.Config (AppEnv (..))
import Valiant.CLI.Discover (SqlFile (..), discoverSqlFiles)
import Valiant.CLI.Error (ValiantCliError (..), dieWithError)
import Valiant.CLI.Output
import System.Directory (doesDirectoryExist)
import System.Exit (ExitCode (..))

-- | Run prepare in a loop, watching for SQL file changes.
-- Polls every 2 seconds (fsnotify can be added later for efficiency).
-- Automatically re-prepares when changes are detected.
runWatch :: AppEnv -> IO ExitCode
runWatch env = do
  _ <- case appDatabaseUrl env of
    Nothing -> dieWithError ErrNoDatabaseUrl
    Just _ -> pure ()

  exists <- doesDirectoryExist (appSqlDir env)
  unless exists $ dieWithError (ErrSqlDirNotFound (appSqlDir env))

  printHeader $ "Watching " <> T.pack (appSqlDir env) <> " for changes..."
  printLn ""

  -- Initial prepare
  printLn "  Running initial prepare..."
  _ <- runPrepare env []
  printLn ""

  -- Track known file hashes
  files <- discoverSqlFiles (appSqlDir env)
  hashRef <- newIORef (Map.fromList [(sqlRelPath sf, sqlHash sf) | sf <- files])

  -- Poll loop
  _ <- forever $ do
    threadDelay 2000000 -- 2 seconds
    scanAndPrepare env hashRef

  -- unreachable, but needed for type
  pure ExitSuccess

-- | Scan all SQL files, detect changes, report, and re-prepare if needed.
scanAndPrepare :: AppEnv -> IORef (Map FilePath Text) -> IO ()
scanAndPrepare env hashRef = do
  files <- discoverSqlFiles (appSqlDir env)
  oldHashes <- readIORef hashRef

  let newHashes = Map.fromList [(sqlRelPath sf, sqlHash sf) | sf <- files]
      changes = detectChanges oldHashes newHashes

  unless (null changes) $ do
    mapM_ reportChange changes
    printLn "  Re-preparing..."
    exitCode <- runPrepare env []
    case exitCode of
      ExitSuccess -> printLn ""
      ExitFailure _ -> printLn "  (Some queries failed. Fix the SQL and save again.)\n"

  -- Update stored hashes
  writeIORef hashRef newHashes

data FileChange
  = FileAdded FilePath
  | FileModified FilePath
  | FileDeleted FilePath

detectChanges :: Map FilePath Text -> Map FilePath Text -> [FileChange]
detectChanges old new =
  let added = [FileAdded k | k <- Map.keys new, not (Map.member k old)]
      modified = [FileModified k | (k, v) <- Map.toList new, Map.member k old, Map.lookup k old /= Just v]
      deleted = [FileDeleted k | k <- Map.keys old, not (Map.member k new)]
   in added ++ modified ++ deleted

reportChange :: FileChange -> IO ()
reportChange (FileAdded path) =
  printLn $ "  [+] " <> T.pack path <> " (new file)"
reportChange (FileModified path) =
  printLn $ "  [~] " <> T.pack path <> " (modified)"
reportChange (FileDeleted path) =
  printLn $ "  [-] " <> T.pack path <> " (deleted)"