packages feed

tricorder 0.2.0.1 → 0.2.1.0

raw patch · 12 files changed

+366/−78 lines, 12 filesdep −ansi-terminaldep ~Cabaldep ~Cabal-syntaxdep ~aeson

Dependencies removed: ansi-terminal

Dependency ranges changed: Cabal, Cabal-syntax, aeson, atelier-core, brick, megaparsec, time, vty

Files

CHANGELOG.md view
@@ -7,6 +7,20 @@  ## [Unreleased] +## 0.2.1.0 - 2026-08-17++### Added++- Tricorder now respects all forms of `cabal.project` files, like+  `cabal.project.local` for example. See+  [Cabal's documentation on project description files](https://cabal.readthedocs.io/en/stable/cabal-project-description-file.html). ([#73](https://github.com/tweag/tricorder/issues/73))+- With `hpack` in `PATH`, Tricorder will now run `hpack` in the directory of+  any changed `package.yaml` file in the root of the project or any+  subdirectory. Control this behavior with the `generate_with_hpack`+  configuration.+- Tricorder will now restart the GHCi session when detecting changes to a+  project's `stack.yaml`.+ ## [0.2.0.1] - 2026-08-12  ### Fixed
LICENSE view
@@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Christian Georgii+Copyright (c) 2025 Tweag  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal
src/Tricorder/Daemon/Core.hs view
@@ -9,7 +9,7 @@ import Atelier.Effects.FileWatcher (FileEvent, FileWatcher) import Atelier.Effects.Input (Input) import Atelier.Effects.Log (Log)-import Atelier.Effects.Publishing (runPubSub_)+import Atelier.Effects.Publishing (runPubSub) import Atelier.Effects.Publishing.Pub (Pub) import Atelier.Effects.Publishing.Sub (Sub) import Data.List (isSuffixOf)@@ -18,8 +18,10 @@ import Effectful.Reader.Static (Reader) import Effectful.State.Static.Shared (State) import Relude.Extra.Tuple (dup)+import System.FilePath ((</>))  import Atelier.Effects.Conc qualified as Conc+import Atelier.Effects.FileSystem qualified as FileSystem import Atelier.Effects.FileWatcher qualified as FileWatcher import Atelier.Effects.Log qualified as Log import Atelier.Effects.Publishing.Pub qualified as Pub@@ -52,12 +54,14 @@     , LoadedModule (..)     , resolveKnownTargets     )+import Tricorder.Daemon.Hpack.Effect (Hpack) import Tricorder.Daemon.TestRunner (TestRunner) import Tricorder.Daemon.Watch (WatchedFile) import Tricorder.Runtime (ProjectRoot (..)) import Tricorder.Session (Session (..), loadSession) import Tricorder.Session.CabalFile (CabalFile) import Tricorder.Session.Command (Command (..))+import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) import Tricorder.Session.TestTarget (TestTarget, renderTestTarget) import Tricorder.Session.TestTimeout (TestTimeout) import Tricorder.Waiters (Waiters)@@ -68,6 +72,7 @@ import Tricorder.Config qualified as Config import Tricorder.Daemon.Builder qualified as Builder import Tricorder.Daemon.EvalCommentRunner qualified as EvalCommentRunner+import Tricorder.Daemon.Hpack qualified as Hpack import Tricorder.Daemon.TestRunner qualified as TestRunner import Tricorder.Daemon.Watch qualified as Watch import Tricorder.Waiters qualified as Waiters@@ -90,6 +95,7 @@        , FileSystem :> es        , FileWatcher :> es        , GhciSession :> es+       , Hpack :> es        , Input LoadedConfig :> es        , Input [CabalFile] :> es        , Log :> es@@ -100,16 +106,17 @@        , Waiters :> es        )     => Eff es Void-main = runPubSub_ @ReloadSession-    . runPubSub_ @WatchedFile-    . runPubSub_ @CabalChangeDetected-    . runPubSub_ @SourceChangeDetected-    . runPubSub_ @RestartBuilder-    . runPubSub_ @ReloadBuilder+main = runPubSub @ReloadSession+    . runPubSub @WatchedFile+    . runPubSub @CabalChangeDetected+    . runPubSub @SourceChangeDetected+    . runPubSub @RestartBuilder+    . runPubSub @ReloadBuilder     $ Conc.restartableFork waitForReloadSession do         root <- Reader.ask         session <- loadSession         Conc.fork_ $ watchConfigFile root+        conditionallyWatchStackYaml root          Conc.fork_ $ Watch.files root session         Conc.fork_ $ Sub.listen_ Watch.publishChange@@ -120,9 +127,13 @@                 Pub.publish ReloadSession             else                 Pub.publish RestartBuilder+         Conc.fork_ $ Sub.listen_ \(SourceChangeDetected fp event) ->             Pub.publish $ ReloadBuilder fp event +        when session.generateWithHpack.getGenerateWithHpack do+            void $ Conc.fork Hpack.main+         State.evalState emptyBuilderState $ withSession session   where     waitForReloadSession = Waiters.wait $ Sub.listenOnce_ @ReloadSession@@ -150,7 +161,23 @@ watchConfigFile root = do     FileWatcher.watchFilePathsDebounced         [FileWatcher.dirWhere root.getProjectRoot (Config.configFileName `isSuffixOf`)]-        \_ _ -> Pub.publish $ ReloadSession+        \_ _ -> Pub.publish ReloadSession+++conditionallyWatchStackYaml+    :: ( Conc :> es+       , Debounce FilePath :> es+       , FileSystem :> es+       , FileWatcher :> es+       , Pub RestartBuilder :> es+       )+    => ProjectRoot -> Eff es ()+conditionallyWatchStackYaml root = do+    exists <- FileSystem.doesFileExist $ root.getProjectRoot </> "stack.yaml"+    when exists do+        Conc.fork_ $ FileWatcher.watchFilePathsDebounced+            [FileWatcher.dirWhere root.getProjectRoot ("stack.yaml" `isSuffixOf`)]+            \_ _ -> Pub.publish RestartBuilder   -- | For a given session, handles controlling the build process itself,
+ src/Tricorder/Daemon/Hpack.hs view
@@ -0,0 +1,81 @@+module Tricorder.Daemon.Hpack (main) where++import Atelier.Effects.Chan (Chan)+import Atelier.Effects.Clock (Clock)+import Atelier.Effects.Conc (Conc)+import Atelier.Effects.Debounce (Debounce)+import Atelier.Effects.FileWatcher (FileWatcher)+import Atelier.Effects.Log (Log)+import Atelier.Effects.Publishing (runPubSub)+import Atelier.Effects.Publishing.Pub (Pub)+import Effectful.Reader.Static (Reader)++import Atelier.Effects.Conc qualified as Conc+import Atelier.Effects.FileWatcher qualified as FileWatcher+import Atelier.Effects.Log qualified as Log+import Atelier.Effects.Publishing.Pub qualified as Pub+import Atelier.Effects.Publishing.Sub qualified as Sub+import Data.List qualified as List+import Effectful.Reader.Static qualified as Reader++import Tricorder.Daemon.Hpack.Effect (Hpack)+import Tricorder.Runtime (ProjectRoot (..))++import Tricorder.Daemon.Hpack.Effect qualified as Hpack+++main+    :: ( Chan :> es+       , Clock :> es+       , Conc :> es+       , Debounce FilePath :> es+       , FileWatcher :> es+       , Hpack :> es+       , Log :> es+       , Reader ProjectRoot :> es+       )+    => Eff es ()+main = runPubSub @RePack $ Conc.scoped do+    hpackInPath <- Hpack.hpackIsInPath+    when hpackInPath do+        projectRoot <- Reader.ask+        Conc.fork_ $ Sub.listen_ \(RePack path) -> runHpack path+        vacuous $ watchPackageYaml projectRoot+++runHpack :: (Hpack :> es, Log :> es) => FilePath -> Eff es ()+runHpack path = do+    res <- Hpack.hpack path+    case res of+        Left err -> Log.err $ "Hpack error: " <> err+        Right result ->+            Log.info+                $ "Hpack: " <> case result of+                    Hpack.Generated ->+                        "generated " <> toText path+                    Hpack.Unchanged ->+                        "already up-to-date: " <> toText path+                    Hpack.WasEditedManually ->+                        "cabal file was edited manually: " <> toText path+                    Hpack.WasGeneratedWithNewerHpack ->+                        "cabal file was generated with a newer version of Hpack: " <> toText path+                    Hpack.UnknownSuccess output -> "unknown successful output: " <> output+++watchPackageYaml+    :: ( Debounce FilePath :> es+       , FileWatcher :> es+       , Pub RePack :> es+       )+    => ProjectRoot -> Eff es Void+watchPackageYaml root = do+    FileWatcher.watchFilePathsDebounced+        [FileWatcher.dirWhere root.getProjectRoot (packageYaml `List.isSuffixOf`)]+        \path _ -> Pub.publish $ RePack path+++data RePack = RePack FilePath+++packageYaml :: FilePath+packageYaml = "package.yaml"
+ src/Tricorder/Daemon/Hpack/Effect.hs view
@@ -0,0 +1,62 @@+module Tricorder.Daemon.Hpack.Effect+    ( Hpack (..)+    , Result (..)+    , hpackIsInPath+    , hpack+    , run+    ) where++import Atelier.Effects.Process (Process, readProcess, runProcess, setWorkingDir, shell)+import Effectful (Effect)+import Effectful.Dispatch.Dynamic (interpret_)+import Effectful.Exception (trySync)+import Effectful.TH (makeEffect)+import System.Exit (ExitCode (..))++import Data.ByteString.Char8 qualified as B8+import System.FilePath qualified as Path+++data Hpack :: Effect where+    HpackIsInPath :: Hpack m Bool+    Hpack :: FilePath -> Hpack m (Either Text Result)+++data Result+    = Generated+    | Unchanged+    | WasGeneratedWithNewerHpack+    | WasEditedManually+    | UnknownSuccess Text+++makeEffect ''Hpack+++run :: (Process :> es) => Eff (Hpack : es) a -> Eff es a+run = interpret_ \case+    HpackIsInPath -> do+        exitCode <- runProcess $ shell "command -v hpack"+        pure $ exitCode == ExitSuccess+    Hpack path -> do+        res <-+            trySync+                $ readProcess+                $ setWorkingDir (Path.takeDirectory path)+                $ shell "hpack"+        case res of+            Left ex -> pure $ Left $ show ex+            Right (exitCode, lstdout, lstderr) -> do+                let stdout = toStrict lstdout+                    stderr = toStrict lstderr+                    infoMsg =+                        if+                            | "generated" `B8.isInfixOf` stdout -> Generated+                            | "is up-to-date" `B8.isInfixOf` stdout -> Unchanged+                            | "generated with a newer version" `B8.isInfixOf` stdout -> WasGeneratedWithNewerHpack+                            | "was modified manually" `B8.isInfixOf` stdout -> WasEditedManually+                            | otherwise -> UnknownSuccess $ decodeUtf8 stdout+                if exitCode == ExitSuccess then+                    pure $ Right $ infoMsg+                else+                    pure $ Left $ decodeUtf8 stderr
src/Tricorder/Daemon/Main.hs view
@@ -12,7 +12,7 @@ import Atelier.Effects.FileSystem (runFileSystemIO) import Atelier.Effects.FileWatcher (runFileWatcherIO) import Atelier.Effects.Process (runProcessIO)-import Atelier.Effects.Publishing (runPubSub_)+import Atelier.Effects.Publishing (runPubSub) import Atelier.Effects.Timeout (runTimeout) import Data.Default (def) import Effectful (runEff)@@ -40,6 +40,7 @@ import Tricorder.Daemon.Core qualified as Core import Tricorder.Daemon.DaemonInfo qualified as DaemonInfo import Tricorder.Daemon.EvalCommentRunner qualified as EvalCommentRunner+import Tricorder.Daemon.Hpack.Effect qualified as Hpack import Tricorder.Daemon.TestRunner qualified as TestRunner import Tricorder.Socket.Server qualified as Server import Tricorder.SourceLookup qualified as SourceLookup@@ -71,6 +72,7 @@         . runLogging         . inputLoadedConfig         . runChan+        . runEnv         . inputCabalFiles         . inputSession         . runReader @CacheConfig.Config def@@ -78,13 +80,13 @@         . runCacheTtl @ModuleName @PackageId         . runCacheTtl @(PackageId, SourceQuery) @SourceLookup.ModuleSourceResult         . runProcessIO-        . runEnv         . runGhcPkgIO         . runUnixSocketIO         . runGhciSession         . evalState (BuildId 1)         . Input.fromState @BuildId-        . runPubSub_ @BuildPhase+        . runPubSub @BuildPhase+        . Hpack.run         . Hackage.run         . PackageStore.run         . EvalCommentRunner.run
src/Tricorder/Session.hs view
@@ -18,6 +18,7 @@ import Tricorder.Session.CabalFile (CabalFile) import Tricorder.Session.Command (Command (..), resolveCommand) import Tricorder.Session.Config (Config (..))+import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) import Tricorder.Session.ReplBuildDir (ReplBuildDir (..)) import Tricorder.Session.Target (Target, definesCustomPrelude, resolveTargets) import Tricorder.Session.TestTarget (TestTarget, resolveTestTargets)@@ -34,6 +35,7 @@     , watchExclusionPatterns :: WatchExclusionPatterns     , replBuildDir :: ReplBuildDir     , testTimeout :: TestTimeout+    , generateWithHpack :: GenerateWithHpack     }     deriving stock (Eq) @@ -48,6 +50,7 @@             , watchExclusionPatterns = def             , replBuildDir = def             , testTimeout = def+            , generateWithHpack = def             }  @@ -101,6 +104,7 @@             , testTargets             , replBuildDir = ReplBuildDir cfgFile.replBuildDir             , testTimeout = TestTimeout cfgFile.testTimeout+            , generateWithHpack = GenerateWithHpack cfgFile.generateWithHpack             }  
src/Tricorder/Session/CabalFile.hs view
@@ -4,6 +4,7 @@     , discoverCabalFiles     ) where +import Atelier.Effects.Env (Env) import Atelier.Effects.FileSystem (FileSystem, doesFileExist, listDirectory, readFileBs) import Atelier.Effects.Input (Input, runInputEff) import Atelier.Effects.Log (Log)@@ -14,6 +15,7 @@ import Effectful.Reader.Static (Reader, ask) import System.FilePath (normalise, takeExtension, (</>)) +import Atelier.Effects.Env qualified as Env import Atelier.Effects.Log qualified as Log import Data.ByteString.Char8 qualified as BC import Data.Text qualified as T@@ -29,14 +31,14 @@   inputCabalFiles-    :: ( FileSystem :> es+    :: ( Env :> es+       , FileSystem :> es        , Log :> es        , Reader ProjectRoot :> es        )     => Eff (Input [CabalFile] : es) a -> Eff es a inputCabalFiles = runInputEff do-    projectRoot <- ask-    projectFilePaths <- discoverCabalFiles projectRoot+    projectFilePaths <- discoverCabalFiles     (faileds, packageDescriptions) <-         partitionEithers <$> for projectFilePaths \p -> do             contents <- readFileBs p@@ -50,38 +52,42 @@     pure $ packageDescriptions  --- | Locate every package's @.cabal@ file, logging what drove the result. In a--- multi-package project the packages live in subdirectories listed under--- @packages:@ in @cabal.project@; in a single-package project the @.cabal@--- file(s) sit in the root. Called once per session load; the result is shared--- by target and watch-dir resolution.-discoverCabalFiles :: (FileSystem :> es, Log :> es) => ProjectRoot -> Eff es [FilePath]-discoverCabalFiles (ProjectRoot projectRoot) = do-    hasProject <- doesFileExist projectFile-    cabalFiles <--        if hasProject then do-            contents <- readFileBs projectFile-            concat <$> traverse cabalFilesForEntry (projectPackageEntries contents)-        else+-- | Lists all `.cabal` files for packages listed in the project root's+-- `cabal.project` (or `cabal.project.local`) file. If no `cabal.project` file+-- is found, looks for a `.cabal` file in the project root, and uses that+-- instead.+discoverCabalFiles+    :: ( Env :> es+       , FileSystem :> es+       , Reader ProjectRoot :> es+       )+    => Eff es [FilePath]+discoverCabalFiles = do+    ProjectRoot projectRoot <- ask+    homeCabalFiles <- maybe [] (one . (</> ".cabal/config")) <$> Env.lookupEnv "HOME"+    let projectFilePaths = projectCabalFiles projectRoot <> homeCabalFiles+    projectFiles <- filterM doesFileExist projectFilePaths+    case nonEmpty projectFiles of+        Nothing ->             cabalFilesIn projectRoot-    let listed-            | null cabalFiles = "none"-            | otherwise = T.intercalate ", " (map toText cabalFiles)-    if hasProject then-        Log.debug-            $ "Found cabal.project; discovered "-                <> show (length cabalFiles)-                <> " package cabal file(s): "-                <> listed-    else-        Log.debug $ "No cabal.project; using cabal file(s) in project root: " <> listed-    pure cabalFiles+        Just neProjectFiles -> do+            packages <- fmap (find (not . null))+                $ for (toList neProjectFiles) \projectFile -> do+                    contents <- readFileBs projectFile+                    concat+                        <$> traverse+                            (cabalFilesForEntry projectRoot)+                            (projectPackageEntries contents)+            case packages of+                Nothing -> cabalFilesIn projectRoot+                Just pkgs -> pure pkgs   where-    projectFile = projectRoot </> "cabal.project"+    projectCabalFiles projectRoot =+        (projectRoot </>) <$> ["cabal.project.local", "cabal.project.freeze", "cabal.project"]      -- A @packages:@ entry is either a direct path to a @.cabal@ file or a     -- directory to search for one.-    cabalFilesForEntry entry+    cabalFilesForEntry projectRoot entry         | takeExtension entry == ".cabal" = pure [projectRoot </> entry]         | otherwise = cabalFilesIn (normalise (projectRoot </> entry)) 
src/Tricorder/Session/Config.hs view
@@ -14,6 +14,7 @@     , testTargets :: Maybe [Text]     , replBuildDir :: FilePath     , testTimeout :: Int+    , generateWithHpack :: Bool     }     deriving stock (Eq, Generic, Show)     deriving (FromJSON) via WithDefaults (QuietSnake Config)@@ -29,4 +30,5 @@             , testTargets = Nothing             , replBuildDir = "dist-newstyle/tricorder"             , testTimeout = 10+            , generateWithHpack = True             }
+ src/Tricorder/Session/GenerateWithHpack.hs view
@@ -0,0 +1,13 @@+module Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) where++import Data.Aeson (FromJSON, ToJSON)+import Data.Default (Default (..))+++newtype GenerateWithHpack = GenerateWithHpack {getGenerateWithHpack :: Bool}+    deriving stock (Eq, Generic, Show)+    deriving (FromJSON, ToJSON) via Bool+++instance Default GenerateWithHpack where+    def = GenerateWithHpack True
test/Unit/Tricorder/Session/CabalFileSpec.hs view
@@ -1,8 +1,9 @@ module Unit.Tricorder.Session.CabalFileSpec (spec_CabalFile) where +import Atelier.Effects.Env (runEnvConst) import Atelier.Effects.FileSystem (runFileSystemState)-import Atelier.Effects.Log (runLogNoOp) import Effectful (runPureEff)+import Effectful.Reader.Static (runReader) import Effectful.State.Static.Shared (evalState) import Test.Hspec (Spec, describe, it, shouldBe) @@ -10,7 +11,7 @@  import Tricorder.Runtime (ProjectRoot (..)) import Tricorder.Session.CabalFile (discoverCabalFiles)-import Unit.Tricorder.Session.Helpers (cabalFixture, multiPackageFs)+import Unit.Tricorder.Session.Helpers (cabalFixture, libTestCabal, multiPackageFs)   spec_CabalFile :: Spec@@ -18,26 +19,100 @@     describe "discoverCabalFiles" testDiscoverCabalFiles  --- | Pins the discovery contract: a @cabal.project@ selects per-package--- @.cabal@ files from its @packages:@ stanza; otherwise the @.cabal@ files in--- the project root are used.+-- | Pins the discovery contract: a @cabal.project@ (or @.local@/@.freeze@+-- variant) selects per-package @.cabal@ files from its @packages:@ stanza;+-- otherwise the @.cabal@ files in the project root are used. Falls back+-- further to @$HOME/.cabal/config@'s @packages:@ stanza if none of the+-- project-root files exist. testDiscoverCabalFiles :: Spec testDiscoverCabalFiles = do     describe "when there is no cabal.project" do         it "finds the .cabal files in the project root" do             let actual =-                    runDiscovery (Map.singleton "/myapp.cabal" cabalFixture)-                        $ discoverCabalFiles pr+                    runDiscovery (Map.singleton "/myapp.cabal" cabalFixture) []+                        $ discoverCabalFiles             actual `shouldBe` ["/myapp.cabal"]          it "returns no files when the root has no cabal file" do-            let actual = runDiscovery mempty $ discoverCabalFiles pr+            let actual = runDiscovery mempty [] discoverCabalFiles             actual `shouldBe` []      describe "when there is a multi-package cabal.project" do         it "resolves each listed package to its .cabal (regression: was root-only)" do-            let actual = runDiscovery multiPackageFs $ discoverCabalFiles pr+            let actual = runDiscovery multiPackageFs [] discoverCabalFiles             actual `shouldBe` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]++    describe "priority among cabal.project.local, cabal.project.freeze, and cabal.project" do+        it "prefers cabal.project.local over cabal.project" do+            let fs =+                    Map.fromList+                        [ ("/cabal.project.local", "packages: pkg-a\n")+                        , ("/cabal.project", "packages: pkg-b\n")+                        ]+                        `Map.union` multiPackageCabalFiles+                actual = runDiscovery fs [] discoverCabalFiles+            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]++        it "prefers cabal.project.freeze over cabal.project" do+            let fs =+                    Map.fromList+                        [ ("/cabal.project.freeze", "packages: pkg-a\n")+                        , ("/cabal.project", "packages: pkg-b\n")+                        ]+                        `Map.union` multiPackageCabalFiles+                actual = runDiscovery fs [] discoverCabalFiles+            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]++        describe "when a higher-priority file lists no packages" do+            it "falls through to the next file in priority order" do+                let fs =+                        Map.fromList+                            [ ("/cabal.project.local", "tests: True\n")+                            , ("/cabal.project", "packages: pkg-b\n")+                            ]+                            `Map.union` multiPackageCabalFiles+                    actual = runDiscovery fs [] discoverCabalFiles+                actual `shouldBe` ["/pkg-b/pkg-b.cabal"]++    describe "packages: entry resolution" do+        it "uses a direct .cabal path entry verbatim, without scanning a directory" do+            let fs = Map.singleton "/cabal.project" "packages: sub/foo.cabal\n"+                actual = runDiscovery fs [] discoverCabalFiles+            actual `shouldBe` ["/sub/foo.cabal"]++        it "skips glob entries under packages: (not expanded)" do+            let fs = Map.singleton "/cabal.project" "packages: */*.cabal\n"+                actual = runDiscovery fs [] discoverCabalFiles+            actual `shouldBe` []++    describe "$HOME/.cabal/config fallback" do+        describe "when no cabal.project files exist" do+            it "uses $HOME/.cabal/config as a last-resort packages source" do+                let fs =+                        Map.singleton "/home/user/.cabal/config" "packages: pkg-a\n"+                            `Map.union` multiPackageCabalFiles+                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles+                actual `shouldBe` ["/pkg-a/pkg-a.cabal"]++        describe "when $HOME/.cabal/config exists but lists no packages" do+            it "falls back to scanning the project root" do+                let fs =+                        Map.fromList+                            [ ("/home/user/.cabal/config", "")+                            , ("/myapp.cabal", cabalFixture)+                            ]+                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles+                actual `shouldBe` ["/myapp.cabal"]   where     pr = ProjectRoot "/"-    runDiscovery fs = runPureEff . evalState fs . runFileSystemState . runLogNoOp+    multiPackageCabalFiles =+        Map.fromList+            [ ("/pkg-a/pkg-a.cabal", libTestCabal "pkg-a")+            , ("/pkg-b/pkg-b.cabal", libTestCabal "pkg-b")+            ]+    runDiscovery fs env =+        runPureEff+            . runEnvConst env+            . evalState fs+            . runFileSystemState+            . runReader pr
tricorder.cabal view
@@ -4,18 +4,18 @@ -- -- see: https://github.com/sol/hpack -name:           tricorder-version:        0.2.0.1-synopsis:       Continuous Haskell build status, diagnostics, and tests via a shared daemon-description:    tricorder rebuilds your Haskell project continuously and surfaces build status, diagnostics, test results, and documentation - for developers and LLM coding agents. Like ghcid and ghciwatch it reloads on every change, but builds run in a background daemon so multiple clients (an interactive TUI, a status CLI, an agent skill) share a single build state without triggering redundant rebuilds. It discovers components across multi-package cabal.project workspaces automatically and ships context-friendly output for agentic use via the CLI.-category:       Development-homepage:       https://github.com/tweag/tricorder#readme-bug-reports:    https://github.com/tweag/tricorder/issues-author:         Christian Georgii-maintainer:     christian.georgii@tweag.io-license:        MIT-license-file:   LICENSE-build-type:     Simple+name:            tricorder+version:         0.2.1.0+synopsis:        Continuous Haskell build status, diagnostics, and tests via a shared daemon+description:     tricorder rebuilds your Haskell project continuously and surfaces build status, diagnostics, test results, and documentation - for developers and LLM coding agents. Like ghcid and ghciwatch it reloads on every change, but builds run in a background daemon so multiple clients (an interactive TUI, a status CLI, an agent skill) share a single build state without triggering redundant rebuilds. It discovers components across multi-package cabal.project workspaces automatically and ships context-friendly output for agentic use via the CLI.+category:        Development+homepage:        https://github.com/tweag/tricorder#readme+bug-reports:     https://github.com/tweag/tricorder/issues+author:          Victor Nascimento Bakke+maintainer:      victor.bakke@tweag.io+license:         MIT+license-file:    LICENSE+build-type:      Simple extra-doc-files:     README.md     CHANGELOG.md@@ -54,6 +54,8 @@       Tricorder.Daemon.GhciSession       Tricorder.Daemon.GhciSession.GhciParser       Tricorder.Daemon.GhciSession.GhciProcess+      Tricorder.Daemon.Hpack+      Tricorder.Daemon.Hpack.Effect       Tricorder.Daemon.Main       Tricorder.Daemon.TestRunner       Tricorder.Daemon.Watch@@ -64,6 +66,7 @@       Tricorder.Session.CabalFile       Tricorder.Session.Command       Tricorder.Session.Config+      Tricorder.Session.GenerateWithHpack       Tricorder.Session.ReplBuildDir       Tricorder.Session.Target       Tricorder.Session.TestTarget@@ -108,14 +111,13 @@       TypeFamilies   ghc-options: -Weverything -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-monomorphism-restriction -Wno-missing-kind-signatures -Wno-missing-local-signatures -Wno-missing-import-lists -Wno-implicit-prelude -Wno-unticked-promoted-constructors -Wno-unused-packages -Wno-all-missed-specialisations -Wno-missed-specialisations -fplugin=Effectful.Plugin -threaded   build-depends:-      Cabal >=3.12 && <3.17-    , Cabal-syntax >=3.12 && <3.17-    , aeson ==2.2.*-    , ansi-terminal ==1.1.*-    , atelier-core ==0.3.*+      Cabal >=3.12 && <3.19+    , Cabal-syntax >=3.12 && <3.19+    , aeson >=2.2 && <2.4+    , atelier-core >=0.3 && <0.5     , atelier-prelude >=0.1 && <0.3     , base >=4.18 && <4.23-    , brick ==2.10.*+    , brick >=2.10 && <2.14     , bytestring >=0.11 && <0.13     , casing ==0.1.*     , containers >=0.6 && <0.9@@ -127,7 +129,7 @@     , effectful-th ==1.0.*     , filepath >=1.4 && <1.6     , hashable ==1.5.*-    , megaparsec ==9.7.*+    , megaparsec >=9.7 && <9.9     , mtl ==2.3.*     , network ==3.2.*     , optparse-applicative ==0.19.*@@ -139,9 +141,9 @@     , tar >=0.6 && <0.8     , template-haskell >=2.20 && <2.25     , text ==2.1.*-    , time >=1.12 && <1.16+    , time >=1.12 && <1.17     , time-units ==1.0.*-    , vty ==6.5.*+    , vty >=6.5 && <6.7     , vty-crossplatform ==0.5.*     , yaml ==0.11.*     , zlib ==0.7.*@@ -281,9 +283,9 @@   build-tool-depends:       tasty-discover:tasty-discover   build-depends:-      Cabal-syntax >=3.12 && <3.17-    , aeson ==2.2.*-    , atelier-core ==0.3.*+      Cabal-syntax >=3.12 && <3.19+    , aeson >=2.2 && <2.4+    , atelier-core >=0.3 && <0.5     , atelier-prelude >=0.1 && <0.3     , base >=4.18 && <4.23     , bytestring >=0.11 && <0.13@@ -294,7 +296,7 @@     , effectful-plugin >=2.0 && <2.2     , filepath >=1.4 && <1.6     , hspec ==2.11.*-    , megaparsec ==9.7.*+    , megaparsec >=9.7 && <9.9     , process ==1.6.*     , regex-tdfa >=1.3.2.5 && <1.4     , stm ==2.5.*@@ -303,7 +305,7 @@     , tasty-discover ==5.2.*     , tasty-hspec ==1.2.*     , text ==2.1.*-    , time >=1.12 && <1.16+    , time >=1.12 && <1.17     , time-units ==1.0.*     , tricorder-internal     , typed-process ==0.2.*