packages feed

hwm-0.0.1: src/HWM/CLI/Command/Run.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.CLI.Command.Run
  ( runScript,
    ScriptOptions (..),
  )
where

import Control.Concurrent.Async
import Control.Monad.Error.Class (MonadError (..))
import Data.List (intersect)
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Traversable (for)
import HWM.Core.Common (Name)
import HWM.Core.Formatting (Color (..), Format (..), Status (Checked, Invalid), chalk, genMaxLen, padDots, statusIcon)
import HWM.Core.Pkg (Pkg (..))
import HWM.Core.Result (Issue (..), IssueDetails (..), Severity (..))
import HWM.Domain.Config (Config (..))
import HWM.Domain.ConfigT (ConfigT, askWorkspaceGroups, config)
import HWM.Domain.Matrix (BuildEnvironment (..), getBuildEnvironment, getBuildEnvroments)
import HWM.Domain.Workspace (resolveTargets)
import HWM.Integrations.Toolchain.Stack (createEnvYaml, stackPath)
import HWM.Runtime.Cache (prepareDir)
import HWM.Runtime.Logging (logError, logRoot)
import HWM.Runtime.Process (inheritRun, silentRun)
import HWM.Runtime.UI (putLine, runSpinner, sectionEnvironments, sectionWorkspace, statusIndicator)
import Relude

data ScriptOptions = ScriptOptions
  { scriptName :: Name,
    scriptTargets :: [Name],
    scriptEnvs :: [Name],
    scriptOptions :: [Text]
  }
  deriving (Show)

getEnvs :: [Name] -> ConfigT [BuildEnvironment]
getEnvs ["all"] = getBuildEnvroments
getEnvs names = for names (getBuildEnvironment . Just)

runScript :: ScriptOptions -> ConfigT ()
runScript ScriptOptions {..} = do
  prepareDir logRoot
  cfg <- asks config
  case M.lookup scriptName (scripts cfg) of
    Just script -> do
      envs <- getEnvs scriptEnvs
      ws <- askWorkspaceGroups
      targets <- fmap (S.toList . S.fromList) (resolveTargets ws scriptTargets)
      for_ envs (createEnvYaml . buildName)
      let multi = length envs > 1
      let cmdTemplate = if null scriptOptions then script else T.unwords (script : scriptOptions)
      let padding = genMaxLen (map format envs)
      let run = runCommand padding multi cmdTemplate targets

      if multi
        then do
          when multi $ do
            sectionWorkspace $ do
              putLine $ padDots 16 "targets" <> if null scriptTargets then chalk Yellow "None (Global Scope)" else chalk Cyan (T.unwords scriptTargets)
            sectionEnvironments (for_ (map buildName envs) (run . Just))
        else run Nothing
    Nothing -> throwError $ fromString $ toString $ "Script not found: " <> scriptName

runCommand :: Int -> Bool -> Text -> [Pkg] -> Maybe Name -> ConfigT ()
runCommand padding multi scripts targets envName = do
  benv@BuildEnvironment {..} <- getBuildEnvironment envName
  let supported = targets `intersect` buildPkgs
  cmd <- resolveCommand scripts supported
  yamlPath <- stackPath envName
  if multi
    then do
      let env = format benv
      (success, content) <- silentRun yamlPath cmd (async (runSpinner padding env))
      statusIndicator padding env (statusIcon (if success then Checked else Invalid))
      unless success $ do
        path <- logError buildName [("ENVIRONMENT", format benv), ("COMMAND", format cmd)] content
        throwError
          Issue
            { issueTopic = buildName,
              issueMessage = "Command failed",
              issueSeverity = SeverityError,
              issueDetails = Just CommandIssue {issueCommand = format cmd, issueLogFile = path}
            }
      putLine ""
    else do
      sectionWorkspace $ do
        putLine $ padDots 16 "targets" <> if null supported then chalk Yellow "None (Global Scope)" else chalk Cyan (T.unwords $ format . pkgName <$> supported)
      sectionEnvironments $ putLine $ format benv
      putLine ""
      putLine ("❯ " <> cmd)
      inheritRun yamlPath cmd
      putLine ""

resolveCommand :: Text -> [Pkg] -> ConfigT Text
resolveCommand cmd targets = do
  let hasPlaceholder = "{TARGET}" `T.isInfixOf` cmd
      hasTargets = not (null targets)
      targetsStr = T.unwords (map (format . pkgName) targets)
  let result = case (hasPlaceholder, hasTargets) of
        (True, True) -> Right $ T.replace "{TARGET}" targetsStr cmd
        (True, False) -> Left "Missing Target! This command requires specific targets (e.g. --target app1)."
        (False, True) -> Left "Target Not Allowed! This command is Global-only and does not support specific targets."
        (False, False) -> Right cmd
  case result of
    Left err -> throwError $ fromString err
    Right c -> pure c