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 +26/−0
- src/Tricorder/CLI/App.hs +18/−5
- src/Tricorder/CLI/Arguments.hs +3/−14
- src/Tricorder/CLI/Main.hs +32/−1
- src/Tricorder/CLI/Operations.hs +27/−22
- src/Tricorder/Daemon/Core.hs +6/−16
- src/Tricorder/Daemon/Main.hs +4/−2
- src/Tricorder/Logging.hs +25/−2
- src/Tricorder/Session/CabalFile.hs +32/−17
- src/Tricorder/Session/Target.hs +5/−5
- src/Tricorder/Socket/Client.hs +0/−17
- src/Tricorder/Socket/Protocol.hs +0/−2
- src/Tricorder/Socket/Server.hs +4/−62
- test/Unit/Tricorder/Session/CabalFileSpec.hs +21/−3
- tricorder.cabal +8/−1
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.*