packages feed

baikai-kit-0.2.0.0: src/Baikai/Kit/Manifest.hs

module Baikai.Kit.Manifest
  ( AgentEntry (..),
    ItemSources (..),
    KitItem (..),
    KitItemKind (..),
    KitManifest (..),
    SkillEntry (..),
    itemKind,
    itemName,
    itemSources,
    itemVersion,
    kitItemKind,
    kindLabel,
    supportedManifestVersions,
  )
where

import Baikai.Kit.Error (KitError (..))
import Baikai.Kit.Path (safeItemName, safeRelativePath)
import Baikai.Prelude
import Data.Bifunctor (first)
import System.FilePath (takeDirectory, takeFileName)

data KitManifest = KitManifest
  { version :: !Int,
    skills :: ![SkillEntry],
    agents :: ![AgentEntry]
  }
  deriving stock (Generic, Show)
  deriving anyclass (FromJSON)

data SkillEntry = SkillEntry
  { name :: !Text,
    description :: !Text,
    version :: !(Maybe Text),
    path :: !Text,
    files :: ![Text]
  }
  deriving stock (Generic, Show)
  deriving anyclass (FromJSON)

data AgentEntry = AgentEntry
  { name :: !Text,
    description :: !Text,
    version :: !(Maybe Text),
    path :: !Text,
    files :: !(Maybe [Text])
  }
  deriving stock (Generic, Show)
  deriving anyclass (FromJSON)

data KitItem
  = KitSkillItem !SkillEntry
  | KitAgentItem !AgentEntry
  deriving stock (Generic, Show)

data KitItemKind
  = SkillKind
  | AgentKind
  deriving stock (Eq, Ord, Show)

-- | The manifest versions this installer understands. Both decode
--   identically today; a future version with different semantics must be
--   refused rather than misinstalled.
supportedManifestVersions :: [Int]
supportedManifestVersions = [1, 2]

-- | Where an item's files live inside the kit checkout: a directory
--   relative to the checkout and file names relative to that directory
--   (the first is the agent body for agents).
--
--   Lexically validated; physical checks are
--   'Baikai.Kit.Path.safeSourcePath'.
data ItemSources = ItemSources
  { base :: !FilePath,
    files :: ![FilePath]
  }
  deriving stock (Eq, Generic, Show)

-- | Derive an item's source list. Fails with 'KitUnsafeName',
--   'KitUnsafePath' or 'KitItemHasNoFiles'. The item name is validated
--   here too, so every consumer of the result has already seen it pass.
itemSources :: KitItem -> Either KitError ItemSources
itemSources item = do
  _ <- first (KitUnsafeName (itemName item)) (safeItemName (itemName item))
  case item of
    KitSkillItem entry -> do
      base <- lexPath (entry ^. #path)
      files <- traverse lexPath (entry ^. #files)
      nonEmpty ItemSources {base, files}
    KitAgentItem entry ->
      case entry ^. #files of
        Just declared -> do
          base <- lexPath (entry ^. #path)
          files <- traverse lexPath declared
          nonEmpty ItemSources {base, files}
        Nothing -> do
          source <- lexPath (entry ^. #path)
          pure ItemSources {base = takeDirectory source, files = [takeFileName source]}
  where
    lexPath raw = first (KitUnsafePath raw) (safeRelativePath raw)
    nonEmpty sources
      | null (sources ^. #files) = Left (KitItemHasNoFiles (itemName item))
      | otherwise = Right sources

itemName :: KitItem -> Text
itemName (KitSkillItem entry) = entry ^. #name
itemName (KitAgentItem entry) = entry ^. #name

itemKind :: KitItem -> Text
itemKind KitSkillItem {} = "skill"
itemKind KitAgentItem {} = "agent"

kitItemKind :: KitItem -> KitItemKind
kitItemKind KitSkillItem {} = SkillKind
kitItemKind KitAgentItem {} = AgentKind

kindLabel :: KitItemKind -> Text
kindLabel SkillKind = "skill"
kindLabel AgentKind = "agent"

itemVersion :: KitItem -> Maybe Text
itemVersion (KitSkillItem entry) = entry ^. #version
itemVersion (KitAgentItem entry) = entry ^. #version