packages feed

seihou-cli-0.6.0.0: src-exe/Seihou/CLI/Status.hs

module Seihou.CLI.Status
  ( handleStatus,
  )
where

import Control.Exception (SomeException, try)
import Data.Generics.Labels ()
import Data.Text.IO qualified as TIO
import Seihou.CLI.Commands (StatusOpts (..))
import Seihou.CLI.ManifestGuard (ArtifactCheck, checkAppliedArtifacts)
import Seihou.CLI.Outdated (checkInstalledModulesForUpdates)
import Seihou.CLI.PendingMigrations (detectPendingMigrations)
import Seihou.CLI.Shared (logIO)
import Seihou.CLI.StatusRender (formatArtifactChecks, formatStatus)
import Seihou.CLI.Style (useColor)
import Seihou.CLI.VersionCompare (OutdatedEntry (..))
import Seihou.Core.Module (defaultSearchPaths, discoverAllModules)
import Seihou.Core.Status (computeTrackedFileStatuses)
import Seihou.Core.Types
import Seihou.Effect.FilesystemInterp (runFilesystem)
import Seihou.Effect.Logger (logError)
import Seihou.Effect.ManifestStore (readManifest)
import Seihou.Effect.ManifestStoreInterp (runManifestStore)
import Seihou.Prelude
import System.Directory (getCurrentDirectory)
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

handleStatus :: StatusOpts -> IO ()
handleStatus opts = do
  let manifestPath = ".seihou" </> "manifest.json"

  -- Run both manifest read and file status computation in the same effect block.
  result <- runEff $ runFilesystem $ runManifestStore manifestPath $ do
    mResult <- readManifest
    case mResult of
      Left err -> pure (Left err)
      Right Nothing -> pure (Right Nothing)
      Right (Just manifest) -> do
        tracked <- computeTrackedFileStatuses manifest
        pure (Right (Just (manifest, tracked)))

  colorEnabled <- useColor

  case result of
    Left err -> do
      logIO LogNormal (logError $ "Error reading manifest: " <> err)
      exitFailure
    Right Nothing ->
      TIO.putStrLn "No Seihou manifest found. Run 'seihou run <module>' to generate a project."
    Right (Just (manifest, tracked)) -> do
      mEntries <-
        if opts ^. #statusCheckUpdates && not (null (manifest ^. #modules))
          then fetchUpdateEntries
          else pure Nothing
      pendings <- detectPendingMigrations manifest Nothing
      TIO.putStr (formatStatus colorEnabled manifest tracked mEntries pendings)
      -- Report, never fail: a stale or mismatched module makes 'seihou run'
      -- refuse, and this is where a developer finds out before that happens.
      -- Any IO failure while checking is swallowed for the same reason.
      guardChecks <- fetchArtifactChecks manifest
      TIO.putStr (formatArtifactChecks colorEnabled guardChecks)

-- | Compare every recorded artifact against this machine, catching any IO
-- failure so status still renders. An empty list means "nothing to report",
-- which is also what a failed check yields — @seihou status@ must not turn a
-- reporting problem into an exit code.
fetchArtifactChecks :: Manifest -> IO [ArtifactCheck]
fetchArtifactChecks manifest = do
  outcome <- try $ do
    projectRoot <- getCurrentDirectory
    searchPaths <- defaultSearchPaths
    checkAppliedArtifacts projectRoot searchPaths manifest
  case outcome of
    Left (e :: SomeException) -> do
      hPutStrLn stderr ("warning: artifact check failed: " <> show e)
      pure []
    Right checks -> pure checks

-- | Run the update check, catching any IO failure so status still renders.
fetchUpdateEntries :: IO (Maybe [OutdatedEntry])
fetchUpdateEntries = do
  outcome <- try $ do
    searchPaths <- defaultSearchPaths
    modules <- discoverAllModules searchPaths
    checkInstalledModulesForUpdates modules
  case outcome of
    Left (e :: SomeException) -> do
      hPutStrLn stderr $
        "warning: update check failed: " <> show e
      pure Nothing
    Right (entries, _stats) -> pure (Just entries)