packages feed

tricorder 0.2.2.3 → 0.3.0.0

raw patch · 15 files changed

+211/−169 lines, 15 filesdep +GlobPVP ok

version bump matches the API change (PVP)

Dependencies added: Glob

API changes (from Hackage documentation)

- Tricorder.CLI.Arguments: Follow :: FollowMode
- Tricorder.CLI.Arguments: NoFollow :: FollowMode
- Tricorder.CLI.Arguments: data FollowMode
- Tricorder.Socket.Client: querySource :: forall (es :: [Effect]). (File :> es, UnixSocket :> es) => FilePath -> [SourceQuery] -> Eff es (Either Text [ModuleSourceResult])
- Tricorder.Socket.Protocol: Source :: [SourceQuery] -> Query
- Tricorder.CLI.App: run :: forall (es :: [Effect]). (Brick :> es, BrickChan :> es, Clock :> es, Conc :> es, Concurrent :> es, Console :> es, Daemons :> es, Delay :> es, Exit :> es, File :> es, FileSystem :> es, IOE :> es, Process :> es, Reader Command :> es, Reader Config :> es, Reader LogPath :> es, Reader PidFile :> es, Reader SocketPath :> es, Timeout :> es, UnixSocket :> es) => Eff es ()
+ Tricorder.CLI.App: run :: forall (es :: [Effect]). (Brick :> es, BrickChan :> es, Cache (PackageId, SourceQuery) ModuleSourceResult :> es, Cache ModuleName PackageId :> es, Clock :> es, Conc :> es, Concurrent :> es, Console :> es, Daemons :> es, Delay :> es, Exit :> es, File :> es, FileSystem :> es, GhcPkg :> es, Hackage :> es, IOE :> es, Input Repl :> es, Log :> es, PackageStore :> es, Process :> es, Reader Command :> es, Reader Config :> es, Reader LogPath :> es, Reader PidFile :> es, Reader SocketPath :> es, Timeout :> es, UnixSocket :> es) => Eff es ()
- Tricorder.CLI.Arguments: ShowLog :: FollowMode -> LogMode
+ Tricorder.CLI.Arguments: ShowLog :: LogMode
- Tricorder.CLI.Operations: showLog :: forall (es :: [Effect]). (Console :> es, Delay :> es, FileSystem :> es) => FilePath -> FollowMode -> Eff es ()
+ Tricorder.CLI.Operations: showLog :: forall (es :: [Effect]). (Console :> es, FileSystem :> es) => FilePath -> Eff es ()
- Tricorder.CLI.Operations: showSource :: forall (es :: [Effect]). (Console :> es, File :> es, Reader SocketPath :> es, UnixSocket :> es) => [SourceQuery] -> Eff es ()
+ Tricorder.CLI.Operations: showSource :: forall (es :: [Effect]). (Cache (PackageId, SourceQuery) ModuleSourceResult :> es, Cache ModuleName PackageId :> es, Console :> es, FileSystem :> es, GhcPkg :> es, Hackage :> es, Input Repl :> es, Log :> es, PackageStore :> es) => [SourceQuery] -> Eff es ()
- Tricorder.Daemon.Core: main :: forall (es :: [Effect]). (Chan :> es, Clock :> es, Conc :> es, Concurrent :> es, Debounce FilePath :> es, EvalCommentRunner :> es, FileSystem :> es, FileWatcher :> es, GhciSession :> es, Hpack :> es, Input LoadedConfig :> es, Input [CabalFile] :> es, Log :> es, Process :> es, Pub BuildPhase :> es, Reader ProjectRoot :> es, State BuildId :> es, State IdleTimeout :> es, State Repl :> es, TestRunner :> es, Waiters :> es) => Eff es Void
+ Tricorder.Daemon.Core: main :: forall (es :: [Effect]). (Chan :> es, Clock :> es, Conc :> es, Concurrent :> es, Debounce FilePath :> es, EvalCommentRunner :> es, FileSystem :> es, FileWatcher :> es, GhciSession :> es, Hpack :> es, Input Session :> es, Log :> es, Process :> es, Pub BuildPhase :> es, Reader ProjectRoot :> es, State BuildId :> es, State IdleTimeout :> es, State Repl :> es, TestRunner :> es, Waiters :> es) => Eff es Void
- Tricorder.Logging: runLogging :: forall (es :: [Effect]) a. (File :> es, IOE :> es, Reader LogPath :> es) => Eff (Log ': es) a -> Eff es a
+ Tricorder.Logging: runLogging :: forall (es :: [Effect]) a. (Env :> es, File :> es, IOE :> es, Input LoadedConfig :> es, Reader LogPath :> es) => Eff (Log ': es) a -> Eff es a
- Tricorder.Session.CabalFile: discoverCabalFiles :: forall (es :: [Effect]). (Env :> es, FileSystem :> es, Reader ProjectRoot :> es) => Eff es [FilePath]
+ Tricorder.Session.CabalFile: discoverCabalFiles :: forall (es :: [Effect]). (Env :> es, FileSystem :> es, Glob :> es, Reader ProjectRoot :> es) => Eff es [FilePath]
- Tricorder.Session.CabalFile: inputCabalFiles :: forall (es :: [Effect]) a. (Env :> es, FileSystem :> es, Log :> es, Reader ProjectRoot :> es) => Eff (Input [CabalFile] ': es) a -> Eff es a
+ Tricorder.Session.CabalFile: inputCabalFiles :: forall (es :: [Effect]) a. (Env :> es, FileSystem :> es, Glob :> es, Log :> es, Reader ProjectRoot :> es) => Eff (Input [CabalFile] ': es) a -> Eff es a
- Tricorder.Socket.Server: main :: forall (es :: [Effect]). (Cache (PackageId, SourceQuery) ModuleSourceResult :> es, Cache ModuleName PackageId :> es, Conc :> es, Exit :> es, FileSystem :> es, GhcPkg :> es, Hackage :> es, IdleTimer :> es, Input BuildId :> es, Input DaemonInfo :> es, Input Repl :> es, Log :> es, PackageStore :> es, Reader SocketPath :> es, Sub BuildPhase :> es, UnixSocket :> es, Waiters :> es) => Eff es Void
+ Tricorder.Socket.Server: main :: forall (es :: [Effect]). (Conc :> es, Exit :> es, IdleTimer :> es, Input BuildId :> es, Input DaemonInfo :> es, Log :> es, Reader SocketPath :> es, Sub BuildPhase :> es, UnixSocket :> es, Waiters :> es) => Eff es Void

Files

CHANGELOG.md view
@@ -7,6 +7,32 @@  ## [Unreleased] +## [0.3.0.0] - 2026-09-15++### Added++- Support for GHC 9.14.++### Changed++- `tricorder source` no longer requires the daemon to evaluate source code.++### Fixed++- The daemon did not respect environment variables and configuration options+  that specified minimum logging levels. It was hard-coded to `INFO` by mistake.+- Tricorder did not expand wildcard entries in `cabal.project`'s `packages`+  list. So an entry like `packages/*` resulted in no packages being found.+  Tricorder now correctly mimics Cabal's behavior, and expands wildcards+  properly.++### Removed++- `tricorder log` no longer has the `--follow` option. Use something like+  `tricorder log --print-path | xargs tail -f` instead. There are better tools to+  handle following a text file as it is being written to rather than including+  an extremely simple facsimile of one in Tricorder.+ ## [0.2.2.3] - 2026-09-11  ### Changed
src/Tricorder/CLI/App.hs view
@@ -1,5 +1,6 @@ module Tricorder.CLI.App (run) where +import Atelier.Effects.Cache (Cache) import Atelier.Effects.Clock (Clock) import Atelier.Effects.Conc (Conc) import Atelier.Effects.Console (Console)@@ -7,12 +8,15 @@ import Atelier.Effects.Exit (Exit) import Atelier.Effects.File (File) import Atelier.Effects.FileSystem (FileSystem)+import Atelier.Effects.Input (Input)+import Atelier.Effects.Log (Log) import Atelier.Effects.Posix.Daemons (Daemons) import Atelier.Effects.Process (Process) import Atelier.Effects.Timeout (Timeout) import Effectful (IOE) import Effectful.Concurrent (Concurrent) import Effectful.Reader.Static (Reader, ask, asks)+import Tricorder.SourceLookup.SourceQuery (ModuleName, SourceQuery) import Prelude hiding (force)  import Atelier.Effects.Console qualified as Console@@ -38,8 +42,14 @@ import Tricorder.CLI.UI.BrickChan (BrickChan) import Tricorder.Daemon.DaemonInfo (DaemonInfo (..)) import Tricorder.Runtime (LogPath (..), PidFile (..), SocketPath (..))+import Tricorder.Session.Command (Repl) import Tricorder.Socket.Client (isDaemonRunning, queryStatus) import Tricorder.Socket.UnixSocket (UnixSocket)+import Tricorder.SourceLookup (ModuleSourceResult)+import Tricorder.SourceLookup.GhcPkg (GhcPkg)+import Tricorder.SourceLookup.Hackage (Hackage)+import Tricorder.SourceLookup.PackageId (PackageId)+import Tricorder.SourceLookup.PackageStore (PackageStore)  import Tricorder.CLI.UI.Keys qualified as Keys @@ -47,6 +57,8 @@ run     :: ( Brick :> es        , BrickChan :> es+       , Cache (PackageId, SourceQuery) ModuleSourceResult :> es+       , Cache ModuleName PackageId :> es        , Clock :> es        , Conc :> es        , Concurrent :> es@@ -56,7 +68,12 @@        , Exit :> es        , File :> es        , FileSystem :> es+       , GhcPkg :> es+       , Hackage :> es        , IOE :> es+       , Input Repl :> es+       , Log :> es+       , PackageStore :> es        , Process :> es        , Reader Command :> es        , Reader Keys.Config :> es@@ -120,7 +137,7 @@                     else                         asks @LogPath (.getLogPath)             case logMode of-                ShowLog followMode -> showLog logFile followMode+                ShowLog -> showLog logFile                 ShowLogPath -> Console.putTextLn (toText logFile)         UI -> do             running <- isDaemonRunning@@ -129,10 +146,6 @@                 void waitForDaemon             viewUi         Source moduleNames -> do-            running <- isDaemonRunning-            unless running $ do-                startDaemon-                void waitForDaemon             showSource moduleNames         Restart force ->             restartDaemon force >>= \case
src/Tricorder/CLI/Arguments.hs view
@@ -1,6 +1,5 @@ module Tricorder.CLI.Arguments     ( Command (..)-    , FollowMode (..)     , LogMode (..)     , OutputFormat (..)     , StatusOptions (..)@@ -24,7 +23,6 @@     , command     , eitherReader     , flag-    , flag'     , fullDesc     , header     , help@@ -41,7 +39,6 @@ import Tricorder.CLI.Command     ( Command (..)     , EvalCommentsOptions (..)-    , FollowMode (..)     , Force (..)     , LogMode (..)     , OutputFormat (..)@@ -100,23 +97,15 @@  logParser :: Parser Command logParser =-    Log <$> (pathFlag <|> followFlag)+    Log <$> pathFlag   where     pathFlag =-        flag'+        flag+            ShowLog             ShowLogPath             ( long "print-path"                 <> help "Print the path to the log file instead of its contents"             )-    followFlag =-        ShowLog-            <$> flag-                NoFollow-                Follow-                ( long "follow"-                    <> short 'f'-                    <> help "Keep streaming new log lines as they are written"-                )   statusParser :: Parser Command
src/Tricorder/CLI/Main.hs view
@@ -6,24 +6,42 @@ import Atelier.Effects.Conc (runConc) import Atelier.Effects.Console (runConsole) import Atelier.Effects.Delay (runDelay)+import Atelier.Effects.Env (runEnv) import Atelier.Effects.Exit (runExit) import Atelier.Effects.File (runFile) import Atelier.Effects.FileSystem (runFileSystemIO)+import Atelier.Effects.Input (input, runInputEff)+import Atelier.Effects.Log (runLogNoOp) import Atelier.Effects.Posix.Daemons (runDaemons) import Atelier.Effects.Process (runProcessIO) import Atelier.Effects.Timeout (runTimeout)+import Data.Default (def) import Effectful (runEff) import Effectful.Concurrent (runConcurrent)+import Effectful.Reader.Static (runReader)+import Tricorder.SourceLookup.SourceQuery (ModuleName, SourceQuery) +import Atelier.Effects.Cache qualified as Cache+import Atelier.Effects.Cache qualified as CacheConfig+import Atelier.Effects.FileSystem.Glob qualified as Glob+ import Tricorder.CLI.Arguments (runArguments) import Tricorder.CLI.UI.Brick (runBrick) import Tricorder.CLI.UI.BrickChan (runBrickChan)-import Tricorder.Config (runLoadedConfig)+import Tricorder.Config (inputLoadedConfig, runLoadedConfig) import Tricorder.Runtime (runLogPath, runPidFile, runProjectRoot, runRuntimeDir, runSocketPath)+import Tricorder.Session (Session (..), loadSession)+import Tricorder.Session.CabalFile (inputCabalFiles)+import Tricorder.Session.Command (Command (..)) import Tricorder.Socket.UnixSocket (runUnixSocketIO)+import Tricorder.SourceLookup.PackageId (PackageId)  import Tricorder.CLI.App qualified as App import Tricorder.CLI.UI.Keys qualified as Keys+import Tricorder.SourceLookup qualified as SourceLookup+import Tricorder.SourceLookup.GhcPkg qualified as GhcPkg+import Tricorder.SourceLookup.Hackage qualified as Hackage+import Tricorder.SourceLookup.PackageStore qualified as PackageStore   main :: IO ()@@ -40,6 +58,7 @@         . runDelay         . runFile         . runFileSystemIO+        . Glob.runIO         . runProjectRoot         . runRuntimeDir         . runPidFile@@ -52,4 +71,16 @@         . runArgumentsIO         . runArguments         . runUnixSocketIO+        . runEnv+        . inputLoadedConfig+        . runLogNoOp+        . inputCabalFiles+        . runInputEff loadSession+        . runInputEff ((.command.repl) <$> input)+        . runReader @CacheConfig.Config def+        . Cache.runCacheTtl @ModuleName @PackageId+        . Cache.runCacheTtl @(PackageId, SourceQuery) @SourceLookup.ModuleSourceResult+        . GhcPkg.runGhcPkgIO+        . PackageStore.run+        . Hackage.run         $ App.run
src/Tricorder/CLI/Operations.hs view
@@ -7,17 +7,19 @@     ) where +import Atelier.Effects.Cache (Cache) import Atelier.Effects.Clock (Clock, currentTimeZone) import Atelier.Effects.Console (Console)-import Atelier.Effects.Delay (Delay) import Atelier.Effects.Exit (Exit, exitFailure) import Atelier.Effects.File (File)-import Atelier.Effects.FileSystem (FileSystem, doesFileExist, followFile, readFileLbs)+import Atelier.Effects.FileSystem (FileSystem, doesFileExist, readFileLbs)+import Atelier.Effects.Input (Input)+import Atelier.Effects.Log (Log) import Data.Aeson (encode) import Data.Time.Format (defaultTimeLocale, formatTime) import Data.Time.LocalTime (utcToLocalTime) import Effectful.Reader.Static (Reader, ask)-import Tricorder.SourceLookup.SourceQuery (SourceQuery)+import Tricorder.SourceLookup.SourceQuery (ModuleName, SourceQuery)  import Atelier.Effects.Console qualified as Console import Data.ByteString.Lazy qualified as BSL@@ -29,7 +31,6 @@ import Tricorder.Build.Test (Suites (..)) import Tricorder.CLI.Arguments     ( EvalCommentsOptions (..)-    , FollowMode (..)     , OutputFormat (..)     , StatusOptions (..)     , TestOptions (..)@@ -42,9 +43,15 @@     , renderSourceResults     ) import Tricorder.Runtime (SocketPath (..))+import Tricorder.Session.Command (Repl) import Tricorder.Session.TestTarget (renderTestTarget)-import Tricorder.Socket.Client (querySource, queryStatus, queryStatusWait)+import Tricorder.Socket.Client (queryStatus, queryStatusWait) import Tricorder.Socket.UnixSocket (UnixSocket)+import Tricorder.SourceLookup (ModuleSourceResult, lookupModuleSource)+import Tricorder.SourceLookup.GhcPkg (GhcPkg)+import Tricorder.SourceLookup.Hackage (Hackage)+import Tricorder.SourceLookup.PackageId (PackageId)+import Tricorder.SourceLookup.PackageStore (PackageStore) import Tricorder.TestOutput (stripGhciNoise)  import Tricorder.Build qualified as Build@@ -175,18 +182,14 @@  showLog     :: ( Console :> es-       , Delay :> es        , FileSystem :> es        )-    => FilePath -> FollowMode -> Eff es ()-showLog path followMode = do+    => FilePath -> Eff es ()+showLog path = do     exists <- doesFileExist path     if not exists-        then-            Console.putTextLn $ "Log file does not exist yet: " <> toText path-        else case followMode of-            Follow -> followFile path Console.putStr-            NoFollow -> readFileLbs path >>= Console.putStr . BSL.toStrict+        then Console.putTextLn $ "Log file does not exist yet: " <> toText path+        else readFileLbs path >>= Console.putStr . BSL.toStrict   showTests@@ -256,19 +259,21 @@   showSource-    :: ( Console :> es-       , File :> es-       , Reader SocketPath :> es-       , UnixSocket :> es+    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es+       , Cache ModuleName PackageId :> es+       , Console :> es+       , FileSystem :> es+       , GhcPkg :> es+       , Hackage :> es+       , Input Repl :> es+       , Log :> es+       , PackageStore :> es        )     => [SourceQuery]     -> Eff es () showSource queries = do-    SocketPath sockPath <- ask-    result <- querySource sockPath queries-    case result of-        Left err -> Console.putTextLn $ "Error: " <> err-        Right results -> renderSourceResults results+    results <- mapM lookupModuleSource queries+    renderSourceResults results   showEvalComments
src/Tricorder/Daemon/Core.hs view
@@ -1,13 +1,12 @@ module Tricorder.Daemon.Core (main) where -import Atelier.Config (LoadedConfig) import Atelier.Effects.Chan (Chan) import Atelier.Effects.Clock (Clock) import Atelier.Effects.Conc (Conc) import Atelier.Effects.Debounce (Debounce) import Atelier.Effects.FileSystem (FileSystem) import Atelier.Effects.FileWatcher (FileEvent, FileWatcher)-import Atelier.Effects.Input (Input)+import Atelier.Effects.Input (Input, input) import Atelier.Effects.Log (Log) import Atelier.Effects.Process (Process) import Atelier.Effects.Publishing (runPubSub)@@ -62,8 +61,7 @@ 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 (Session (..)) import Tricorder.Session.Command (Command (..), Repl) import Tricorder.Session.GenerateWithHpack (GenerateWithHpack (..)) import Tricorder.Session.IdleTimeout (IdleTimeout)@@ -112,8 +110,7 @@        , FileWatcher :> es        , GhciSession :> es        , Hpack :> es-       , Input LoadedConfig :> es-       , Input [CabalFile] :> es+       , Input Session :> es        , Log :> es        , Process :> es        , Pub BuildPhase :> es@@ -133,7 +130,7 @@     . runPubSub @ReloadBuilder     $ Conc.restartableFork waitForReloadSession do         root <- Reader.ask-        session <- loadSession+        session <- input         logSession session          State.put session.command.repl@@ -163,16 +160,9 @@     waitForReloadSession = Waiters.wait $ Sub.listenOnce_ @ReloadSession  -shouldReloadSession-    :: ( FileSystem :> es-       , Input LoadedConfig :> es-       , Input [CabalFile] :> es-       , Log :> es-       , Reader ProjectRoot :> es-       )-    => Session -> Eff es Bool+shouldReloadSession :: (Input Session :> es) => Session -> Eff es Bool shouldReloadSession oldSession = do-    newSession <- loadSession+    newSession <- input     pure $ newSession /= oldSession  
src/Tricorder/Daemon/Main.hs view
@@ -23,6 +23,7 @@  import Atelier.Effects.Cache.Config qualified as CacheConfig import Atelier.Effects.Conc qualified as Conc+import Atelier.Effects.FileSystem.Glob qualified as Glob import Atelier.Effects.Input qualified as Input import Atelier.Effects.Log qualified as Log @@ -66,16 +67,17 @@         . runDebounce @FilePath         . runFileWatcherIO         . runFileSystemIO+        . Glob.runIO         . runProjectRoot         . runExit         . runFile         . runRuntimeDir         . runSocketPath         . runLogPath-        . runLogging+        . runEnv         . inputLoadedConfig+        . runLogging         . runChan-        . runEnv         . inputCabalFiles         . inputSession         . runReader @CacheConfig.Config def
src/Tricorder/Logging.hs view
@@ -1,18 +1,41 @@ module Tricorder.Logging (runLogging) where +import Atelier.Config (LoadedConfig, extractNestedConfig)+import Atelier.Effects.Env (Env) import Atelier.Effects.File (BufferMode (..), File)+import Atelier.Effects.Input (Input, input) import Atelier.Effects.Log (Log, Severity (..), runLogToHandle) import Effectful (IOE) import Effectful.Reader.Static (Reader, asks)  import Atelier.Effects.File qualified as File+import Atelier.Effects.Log qualified as Log  import Tricorder.Runtime (LogPath (..))  -runLogging :: (File :> es, IOE :> es, Reader LogPath :> es) => Eff (Log : es) a -> Eff es a+runLogging+    :: ( Env :> es+       , File :> es+       , IOE :> es+       , Input LoadedConfig :> es+       , Reader LogPath :> es+       )+    => Eff (Log : es) a -> Eff es a runLogging act = do     path <- asks @LogPath (.getLogPath)     File.withFile path AppendMode \h -> do         File.hSetBuffering h LineBuffering-        runLogToHandle h INFO act+        sev <- getMinimumSeverity+        runLogToHandle h sev act+++getMinimumSeverity :: (Env :> es, Input LoadedConfig :> es) => Eff es Severity+getMinimumSeverity = do+    mSev <- Log.minimumSeverityFromEnv+    case mSev of+        Just sev -> pure sev+        Nothing -> do+            loadedConfig <- input+            let config = extractNestedConfig @"session.logging" @Log.Config loadedConfig+            pure config.minimumSeverity
src/Tricorder/Session/CabalFile.hs view
@@ -7,6 +7,7 @@  import Atelier.Effects.Env (Env) import Atelier.Effects.FileSystem (FileSystem, doesFileExist, listDirectory, readFileBs)+import Atelier.Effects.FileSystem.Glob (Glob, globDir1) import Atelier.Effects.Input (Input, runInputEff) import Atelier.Effects.Log (Log) import Data.Traversable (for)@@ -15,6 +16,7 @@ import Distribution.Types.GenericPackageDescription (GenericPackageDescription) import Effectful.Reader.Static (Reader, ask) import System.FilePath (normalise, takeExtension, (</>))+import System.FilePath.Glob (compile)  import Atelier.Effects.Env qualified as Env import Atelier.Effects.Log qualified as Log@@ -34,6 +36,7 @@ inputCabalFiles     :: ( Env :> es        , FileSystem :> es+       , Glob :> es        , Log :> es        , Reader ProjectRoot :> es        )@@ -53,13 +56,12 @@     pure $ packageDescriptions  --- | 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.+-- | Discovers `.cabal` files in all locations and formats Cabal itself+-- supports. discoverCabalFiles     :: ( Env :> es        , FileSystem :> es+       , Glob :> es        , Reader ProjectRoot :> es        )     => Eff es [FilePath]@@ -68,12 +70,12 @@     homeCabalFiles <- maybe [] (one . (</> ".cabal/config")) <$> Env.lookupEnv "HOME"     let projectFilePaths = projectCabalFiles projectRoot <> homeCabalFiles     projectFiles <- filterM doesFileExist projectFilePaths-    case nonEmpty projectFiles of-        Nothing ->+    if null projectFiles+        then             cabalFilesIn projectRoot-        Just neProjectFiles -> do+        else do             packages <- fmap (find (not . null))-                $ for (toList neProjectFiles) \projectFile -> do+                $ for projectFiles \projectFile -> do                     contents <- readFileBs projectFile                     concat                         <$> traverse@@ -86,28 +88,37 @@     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 projectRoot entry-        | takeExtension entry == ".cabal" = pure [projectRoot </> entry]-        | otherwise = cabalFilesIn (normalise (projectRoot </> entry))+        | hasWildcard entry = do+            matches <- globDir1 (compile entry) projectRoot+            concat <$> traverse resolveMatch matches+        | isCabalFile entry = pure [projectRoot </> entry]+        | otherwise = cabalFilesIn $ normalise $ projectRoot </> entry+      where+        resolveMatch path+            | isCabalFile path = pure [path]+            | otherwise = cabalFilesIn path   -- | List the @.cabal@ files directly inside a directory. cabalFilesIn :: (FileSystem :> es) => FilePath -> Eff es [FilePath] cabalFilesIn dir = do-    entries <- filter (\f -> takeExtension f == ".cabal") <$> listDirectory dir-    pure $ map (dir </>) entries+    entries <- filter isCabalFile <$> listDirectory dir+    pure $ (dir </>) <$> entries  +-- | Does a @packages:@ entry contain a glob wildcard?+hasWildcard :: FilePath -> Bool+hasWildcard = elem '*'++ -- | Extract the directory/file entries from the @packages:@ field of a--- @cabal.project@. Glob entries (containing @*@) are not expanded and are--- skipped.+-- @cabal.project@. projectPackageEntries :: ByteString -> [FilePath] projectPackageEntries contents =     case readFields contents of         Left _ -> []-        Right fields -> filter (notElem '*') $ concatMap fromField fields+        Right fields -> concatMap fromField fields   where     fromField = \case         (Field (Name _ name) fieldLines)@@ -119,3 +130,7 @@      dropComma ',' = ' '     dropComma c = c+++isCabalFile :: FilePath -> Bool+isCabalFile = (== ".cabal") . takeExtension
src/Tricorder/Session/Target.hs view
@@ -135,11 +135,11 @@ -- way the result is sorted with 'compareTargets' so libraries exposing a custom -- @Prelude@ come last [ref:lib_sort_order]. resolveTargets :: [CabalFile] -> [Text] -> [Target]-resolveTargets cabalFiles targets@(_ : _) =-    sortBy (compareTargets (definesCustomPrelude cabalFiles)) $ parseTarget <$> targets-resolveTargets cabalFiles [] =-    sortBy (compareTargets (definesCustomPrelude cabalFiles))-        $ foldMap (allComponentTargets . (.projectPackageDescription)) cabalFiles+resolveTargets cabalFiles = \case+    targets@(_ : _) -> sortTargets $ parseTarget <$> targets+    [] -> sortTargets $ foldMap (allComponentTargets . (.projectPackageDescription)) cabalFiles+  where+    sortTargets = sortBy (compareTargets (definesCustomPrelude cabalFiles))   -- | [tag:lib_sort_order] When running @cabal repl <package defining custom
src/Tricorder/Socket/Client.hs view
@@ -3,7 +3,6 @@     , queryStatusWait     , queryWatch     , Restarting (..)-    , querySource     , queryDiagnostic     , requestShutdown     , isDaemonRunning@@ -21,7 +20,6 @@ import Effectful.Reader.Static (Reader, ask) import Effectful.State.Static.Shared (evalState, get, modify, put) import System.IO.Error (isEOFError)-import Tricorder.SourceLookup.SourceQuery (SourceQuery) import Prelude hiding (force)  import Atelier.Effects.Delay qualified as Delay@@ -40,7 +38,6 @@     , Waiters (..)     ) import Tricorder.Socket.UnixSocket (UnixSocket, withConnection)-import Tricorder.SourceLookup (ModuleSourceResult)  import Tricorder.Version qualified as Version @@ -110,20 +107,6 @@                     Just state -> do                         put retryLimit                         inject (handler (Right state)) >> loop h----- | Look up the source for one or more modules via the daemon.-querySource-    :: (File :> es, UnixSocket :> es)-    => FilePath-    -> [SourceQuery]-    -> Eff es (Either Text [ModuleSourceResult])-querySource sockPath queries = withConnection sockPath \h -> do-    sendQuery h (Source queries)-    line <- File.hGetLine h-    case eitherDecode (BSL.fromStrict (encodeUtf8 (toText line))) of-        Left err -> pure $ Left (toText err)-        Right results -> pure $ Right results   -- | Fetch the full body of a single diagnostic by 1-based index.
src/Tricorder/Socket/Protocol.hs view
@@ -11,7 +11,6 @@  import Data.Aeson (FromJSON, ToJSON) import Tricorder.CLI.Command (Force (..))-import Tricorder.SourceLookup.SourceQuery (SourceQuery)   data StatusQuery = StatusQuery {awaitDone :: Bool}@@ -27,7 +26,6 @@ data Query     = Status StatusQuery     | Watch-    | Source [SourceQuery]     | DiagnosticAt DiagnosticQuery     | Quit Waiters     deriving stock (Eq, Generic, Show)
src/Tricorder/Socket/Server.hs view
@@ -1,9 +1,7 @@ module Tricorder.Socket.Server (main, SocketRemoved (..)) where -import Atelier.Effects.Cache (Cache) import Atelier.Effects.Conc (Conc) import Atelier.Effects.Exit (Exit, exitSuccess)-import Atelier.Effects.FileSystem (FileSystem) import Atelier.Effects.Input (Input, input) import Atelier.Effects.Log (Log) import Atelier.Effects.Publishing.Sub (Sub)@@ -12,7 +10,6 @@ import Effectful.Reader.Static (Reader, ask) import Effectful.State.Static.Shared (State) import System.IO (Handle)-import Tricorder.SourceLookup.SourceQuery (ModuleName, SourceQuery)  import Atelier.Effects.Conc qualified as Conc import Atelier.Effects.Log qualified as Log@@ -24,7 +21,6 @@ import Tricorder.Daemon.DaemonInfo (DaemonInfo) import Tricorder.Daemon.IdleTimer (IdleTimer) import Tricorder.Runtime (SocketPath (..))-import Tricorder.Session.Command (Repl) import Tricorder.Socket.Protocol     ( ClientMessage (..)     , DiagnosticQuery (..)@@ -41,11 +37,6 @@     , removeSocketFile     , sendLine     )-import Tricorder.SourceLookup (ModuleSourceResult, lookupModuleSource)-import Tricorder.SourceLookup.GhcPkg (GhcPkg)-import Tricorder.SourceLookup.Hackage (Hackage)-import Tricorder.SourceLookup.PackageId (PackageId)-import Tricorder.SourceLookup.PackageStore (PackageStore) import Tricorder.Version (VersionMismatch (..), checkVersion) import Tricorder.Waiters (Waiters) @@ -62,19 +53,12 @@   main-    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es-       , Cache ModuleName PackageId :> es-       , Conc :> es+    :: ( Conc :> es        , Exit :> es-       , FileSystem :> es-       , GhcPkg :> es-       , Hackage :> es        , IdleTimer :> es        , Input BuildId :> es        , Input DaemonInfo :> es-       , Input Repl :> es        , Log :> es-       , PackageStore :> es        , Reader SocketPath :> es        , Sub BuildPhase :> es        , UnixSocket :> es@@ -90,19 +74,12 @@   acceptTrigger-    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es-       , Cache ModuleName PackageId :> es-       , Conc :> es+    :: ( Conc :> es        , Exit :> es-       , FileSystem :> es-       , GhcPkg :> es-       , Hackage :> es        , IdleTimer :> es        , Input BuildId :> es        , Input DaemonInfo :> es-       , Input Repl :> es        , Log :> es-       , PackageStore :> es        , Reader SocketPath :> es        , State BuildPhase :> es        , Sub BuildPhase :> es@@ -120,19 +97,12 @@   handleConnection-    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es-       , Cache ModuleName PackageId :> es-       , Conc :> es+    :: ( Conc :> es        , Exit :> es-       , FileSystem :> es-       , GhcPkg :> es-       , Hackage :> es        , IdleTimer :> es        , Input BuildId :> es        , Input DaemonInfo :> es-       , Input Repl :> es        , Log :> es-       , PackageStore :> es        , State BuildPhase :> es        , Sub BuildPhase :> es        , UnixSocket :> es@@ -154,18 +124,11 @@   dispatch-    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es-       , Cache ModuleName PackageId :> es-       , Conc :> es+    :: ( Conc :> es        , Exit :> es-       , FileSystem :> es-       , GhcPkg :> es-       , Hackage :> es        , Input BuildId :> es        , Input DaemonInfo :> es-       , Input Repl :> es        , Log :> es-       , PackageStore :> es        , State BuildPhase :> es        , Sub BuildPhase :> es        , UnixSocket :> es@@ -178,7 +141,6 @@     Status (StatusQuery False) -> respondOnce h     Status (StatusQuery True) -> respondWhenDone h     Watch -> watchStream h-    Source moduleNames -> respondSource moduleNames h     DiagnosticAt dq -> respondDiagnostic dq.index h     Quit waiters -> quit h waiters @@ -272,26 +234,6 @@             | otherwise -> sendJson h $ ErrorResponse "Build in progress"         Build.Failed msg -> sendJson h $ ErrorResponse $ "Build command failed:\n" <> msg         _ -> sendJson h $ ErrorResponse "Build in progress"----- | Look up source for each requested module and send the results as a JSON array.-respondSource-    :: ( Cache (PackageId, SourceQuery) ModuleSourceResult :> es-       , Cache ModuleName PackageId :> es-       , FileSystem :> es-       , GhcPkg :> es-       , Hackage :> es-       , Input Repl :> es-       , Log :> es-       , PackageStore :> es-       , UnixSocket :> es-       )-    => [SourceQuery]-    -> Handle-    -> Eff es ()-respondSource queries h = do-    results <- mapM lookupModuleSource queries-    sendJson h results   sendJson :: (ToJSON a, UnixSocket :> es) => Handle -> a -> Eff es ()
test/Unit/Tricorder/Session/CabalFileSpec.hs view
@@ -7,6 +7,7 @@ import Effectful.State.Static.Shared (evalState) import Test.Hspec (Spec, describe, it, shouldBe, shouldMatchList) +import Atelier.Effects.FileSystem.Glob qualified as Glob import Data.Map.Strict qualified as Map  import Tricorder.Runtime (ProjectRoot (..))@@ -80,9 +81,24 @@                 actual = runDiscovery fs [] discoverCabalFiles             actual `shouldBe` ["/sub/foo.cabal"] -        it "skips glob entries under packages: (not expanded)" do+        it "expands a glob entry matching .cabal files directly" do             let fs = Map.singleton "/cabal.project" "packages: */*.cabal\n"-                actual = runDiscovery fs [] discoverCabalFiles+                script = [Glob.NextGlobDir1 ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]]+                actual = runDiscoveryGlob fs [] script discoverCabalFiles+            actual `shouldMatchList` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]++        it "expands a glob entry matching package directories" do+            let fs =+                    Map.singleton "/cabal.project" "packages: */\n"+                        `Map.union` multiPackageCabalFs+                script = [Glob.NextGlobDir1 ["/pkg-a", "/pkg-b"]]+                actual = runDiscoveryGlob fs [] script discoverCabalFiles+            actual `shouldMatchList` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]++        it "returns no files when a glob entry matches nothing" do+            let fs = Map.singleton "/cabal.project" "packages: */*.cabal\n"+                script = [Glob.NextGlobDir1 []]+                actual = runDiscoveryGlob fs [] script discoverCabalFiles             actual `shouldBe` []      describe "$HOME/.cabal/config fallback" do@@ -114,9 +130,11 @@                 actual `shouldMatchList` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]   where     pr = ProjectRoot "/"-    runDiscovery fs env =+    runDiscovery fs env = runDiscoveryGlob fs env []+    runDiscoveryGlob fs env script =         runPureEff             . runEnvConst env             . evalState fs             . runFileSystemState+            . Glob.runScripted script             . runReader pr
tricorder.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:            tricorder-version:         0.2.2.3+version:         0.3.0.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@@ -16,6 +16,12 @@ license:         MIT license-file:    LICENSE build-type:      Simple+tested-with:+    GHC == 9.10.3+  , GHC == 9.6.7+  , GHC == 9.8.4+  , GHC == 9.12.4+  , GHC == 9.14.1 extra-doc-files:     README.md     CHANGELOG.md@@ -118,6 +124,7 @@   build-depends:       Cabal >=3.12 && <3.19     , Cabal-syntax >=3.12 && <3.19+    , Glob ==0.10.*     , aeson >=2.2 && <2.4     , atelier-core ==0.6.*     , atelier-prelude ==0.3.*