From 407256503454a50f0029b00f213e1f3263789ce4 Mon Sep 17 00:00:00 2001 From: rochala Date: Tue, 14 Apr 2026 18:04:41 +0200 Subject: [PATCH 1/4] Add starvation-checks config to control CE CPU starvation warnings - Add StarvationChecksConfig with `enabled` flag (default: false via reference.conf) - Override runtimeConfig to disable starvation checker when not enabled - Make Config fully synchronous (loadSync/bootstrap) so runtimeConfig can read it - Simplify configOpt from Opts[IO[Config]] to Opts[Config] - Add .cellar/cellar.conf with starvation-checks.enabled=true for dev use - Update .gitignore to track .cellar/ (except cache) - Document new config in README and add docs note to CLAUDE.md - Env var override: CELLAR_STARVATION_CHECKS_ENABLED Co-Authored-By: Claude Opus 4.6 --- .cellar/cellar.conf | 3 ++ .gitignore | 2 +- CLAUDE.md | 6 +++- README.md | 6 ++++ cli/src/cellar/cli/CellarApp.scala | 25 ++++++++++----- lib/resources/reference.conf | 9 ++++++ lib/src/cellar/Config.scala | 32 ++++++++++--------- .../cellar/ProjectAwareIntegrationTest.scala | 26 +++++++-------- 8 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 .cellar/cellar.conf diff --git a/.cellar/cellar.conf b/.cellar/cellar.conf new file mode 100644 index 0000000..9955095 --- /dev/null +++ b/.cellar/cellar.conf @@ -0,0 +1,3 @@ +starvation-checks { + enabled = true +} diff --git a/.gitignore b/.gitignore index 2e8c887..85a36d4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,5 @@ langoustine-tracer .vscode .claude ai -.cellar +.cellar/cache result diff --git a/CLAUDE.md b/CLAUDE.md index 302c99d..195d454 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,10 @@ Examples are testing match exhaustivity, typesystem etc. - Use `fs2.io.file.Path` for file references, not `java.io.File` or `java.nio.file.Path` - Coursier error handling: match `coursierapi.error.CoursierError`, call `CoordinateCompleter.suggest` to attach suggestions to `CellarError.CoordinateNotFound` -## Documentation / Development history +## Documentation + +When adding or modifying CLI commands, flags, or config options, update `README.md` accordingly. + +## Development history All plans, decision and architecture designs are available in ai/ folder diff --git a/README.md b/README.md index 71287bf..ec48be9 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,12 @@ sbt { # Extra arguments passed to sbt, space-separated (e.g. "--client") extra-args = "" # env: CELLAR_SBT_EXTRA_ARGS } + +starvation-checks { + # Enable Cats Effect CPU starvation warnings (default: false). + # Set to true during development or CI to surface warnings. + enabled = false # env: CELLAR_STARVATION_CHECKS_ENABLED +} ``` ### Examples diff --git a/cli/src/cellar/cli/CellarApp.scala b/cli/src/cellar/cli/CellarApp.scala index 601a07f..2fe2996 100644 --- a/cli/src/cellar/cli/CellarApp.scala +++ b/cli/src/cellar/cli/CellarApp.scala @@ -1,6 +1,7 @@ package cellar.cli import cats.effect.{ExitCode, IO} +import cats.effect.unsafe.IORuntimeConfig import cats.syntax.all.* import cellar.* import cellar.handlers.{DepsHandler, GetHandler, GetSourceHandler, ListHandler, MetaHandler, ProjectGetHandler, ProjectListHandler, ProjectSearchHandler, SearchHandler} @@ -9,6 +10,8 @@ import com.monovore.decline.effect.* import coursierapi.{MavenRepository, Repository} import fs2.io.file.Path +import scala.concurrent.duration.Duration + object CellarApp extends CommandIOApp( name = "cellar", @@ -16,6 +19,11 @@ object CellarApp version = BuildInfo.version ): + override def runtimeConfig: IORuntimeConfig = + val base = super.runtimeConfig + if Config.bootstrap.starvationChecks.enabled then base + else base.copy(cpuStarvationCheckInitialDelay = Duration.Inf) + override def main: Opts[IO[ExitCode]] = getSubcmd orElse getExternalSubcmd orElse getSourceSubcmd orElse @@ -50,8 +58,9 @@ object CellarApp private val noCacheOpt: Opts[Boolean] = Opts.flag("no-cache", "Skip classpath cache (re-extract from build tool)").orFalse - private val configOpt: Opts[IO[Config]] = - Opts.option[Path]("config", "Path to config file", "c").orNone.map(Config.load) + private val configOpt: Opts[Config] = + Opts.option[Path]("config", "Path to config file", "c").orNone + .map(p => if p.isDefined then Config.loadSync(p) else Config.bootstrap) private def parseAndResolve(raw: String, extraRepos: List[Repository]): IO[Either[String, MavenCoordinate]] = MavenCoordinate.parse(raw) match @@ -60,23 +69,23 @@ object CellarApp private val getSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("get", "Fetch symbol info from the current project") { - (symbolArg, moduleOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, configIO, javaHome, noCache) => - configIO.flatMap(ProjectGetHandler.run(fqn, module, _, javaHome, noCache)) + (symbolArg, moduleOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, config, javaHome, noCache) => + ProjectGetHandler.run(fqn, module, config, javaHome, noCache) } } private val listSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("list", "List symbols in a package or class from the current project") { - (symbolArg, moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, configIO, javaHome, noCache) => - configIO.flatMap(ProjectListHandler.run(fqn, module, limit, _, javaHome, noCache)) + (symbolArg, moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, config, javaHome, noCache) => + ProjectListHandler.run(fqn, module, limit, config, javaHome, noCache) } } private val searchSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("search", "Substring search for symbol names in the current project") { (Opts.argument[String]("query"), moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { - (query, module, limit, configIO, javaHome, noCache) => - configIO.flatMap(ProjectSearchHandler.run(query, module, limit, _, javaHome, noCache)) + (query, module, limit, config, javaHome, noCache) => + ProjectSearchHandler.run(query, module, limit, config, javaHome, noCache) } } diff --git a/lib/resources/reference.conf b/lib/resources/reference.conf index 48c8934..28199cd 100644 --- a/lib/resources/reference.conf +++ b/lib/resources/reference.conf @@ -12,4 +12,13 @@ sbt { # Several arguments can be passed by separating them with spaces extra-args = "" extra-args = ${?CELLAR_SBT_EXTRA_ARGS} +} + +starvation-checks { + # cats-effect's CPU starvation checker emits warnings when the compute pool + # is blocked. cellar shells out to mill/sbt for classpath extraction, which + # routinely trips the default checker with noise that is not actionable for + # a short-lived CLI. Disabled by default; set to true if debugging. + enabled = false + enabled = ${?CELLAR_STARVATION_CHECKS_ENABLED} } \ No newline at end of file diff --git a/lib/src/cellar/Config.scala b/lib/src/cellar/Config.scala index 5ded817..03a8c3e 100644 --- a/lib/src/cellar/Config.scala +++ b/lib/src/cellar/Config.scala @@ -1,8 +1,6 @@ package cellar -import cats.effect.IO -import cats.syntax.all.* -import fs2.io.file.{Files, Path} +import fs2.io.file.Path import pureconfig.* case class MillConfig(binary: String) derives ConfigReader @@ -11,25 +9,29 @@ case class SbtConfig(binary: String, extraArgs: String) derives ConfigReader { def effectiveExtraArgs: List[String] = extraArgs.split("\\s+").filter(_.nonEmpty).toList } -case class Config(mill: MillConfig, sbt: SbtConfig) derives ConfigReader +case class StarvationChecksConfig(enabled: Boolean) derives ConfigReader -object Config { - lazy val default: IO[Config] = load(None) +case class Config(mill: MillConfig, sbt: SbtConfig, starvationChecks: StarvationChecksConfig) derives ConfigReader +object Config { val defaultUserPath: Option[Path] = sys.props.get("user.home").map(Path(_).resolve(".cellar").resolve("cellar.conf")) val defaultProjectPath: Path = Path(".cellar").resolve("cellar.conf") - def load(path: Option[Path]): IO[Config] = { - def load0(path: List[Path]) = - IO.blocking { - path.foldLeft(ConfigSource.default)((cs, p) => ConfigSource.file(p.toNioPath).withFallback(cs)).loadOrThrow[Config] - } + /** Memoized bootstrap config loaded from default locations. Accessed before + * the IO runtime starts (from `IOApp.runtimeConfig`), so it must be + * synchronous. Throws `ConfigReaderException` on malformed config. + */ + lazy val bootstrap: Config = loadSync(None) - path match - case sp: Some[_] => load0(sp.toList) + def loadSync(path: Option[Path]): Config = { + val paths = path match + case Some(p) => List(p) case None => - val relevantPaths = defaultUserPath.toList ++ List(defaultProjectPath) - relevantPaths.filterA(p => Files[IO].exists(p)).flatMap(load0) + (defaultUserPath.toList ++ List(defaultProjectPath)) + .filter(p => java.nio.file.Files.exists(p.toNioPath)) + paths + .foldLeft(ConfigSource.default)((cs, p) => ConfigSource.file(p.toNioPath).withFallback(cs)) + .loadOrThrow[Config] } } diff --git a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala index 3e69ed0..99df0b3 100644 --- a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala +++ b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala @@ -175,8 +175,8 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: // --- MillBuildTool tests --- test("MillBuildTool: rejects missing --module"): - Config.default.map(config => build.MillBuildTool(Path("."), config.mill)) - .flatMap(_.extractClasspath(None)).attempt.map { result => + build.MillBuildTool(Path("."), Config.bootstrap.mill) + .extractClasspath(None).attempt.map { result => assert(result.isLeft) assert(result.left.exists(_.getMessage.contains("--module is required for Mill"))) } @@ -184,7 +184,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: // --- SbtBuildTool tests --- test("SbtBuildTool: rejects missing --module"): - Config.default.map(config => build.SbtBuildTool(Path("."), config.sbt)).flatMap(_.extractClasspath(None)) + build.SbtBuildTool(Path("."), Config.bootstrap.sbt).extractClasspath(None) .attempt.map { result => assert(result.isLeft) assert(result.left.exists(_.getMessage.contains("--module is required for sbt"))) @@ -299,7 +299,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def hello: String = "world" |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectGetHandler.run("example.MyClass", module = None, _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("example.MyClass", module = None, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("MyClass"), s"Output: ${console.outBuf}") assert(console.outBuf.toString.contains("hello"), s"Output: ${console.outBuf}") @@ -320,7 +320,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectGetHandler.run("cats.Monad", module = None, _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("cats.Monad", module = None, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("Monad"), s"Output: ${console.outBuf}") } @@ -339,7 +339,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def goodbye: Int = 42 |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, _, cwd = Some(dir))).map { code => + handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) val out = console.outBuf.toString assert(out.contains("hello"), s"Output: $out") @@ -359,7 +359,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, _, cwd = Some(dir))).map { code => + handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("UniqueTestClassName123"), s"Output: ${console.outBuf}") } @@ -375,7 +375,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |class Foo |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is not supported"), s"Stderr: ${console.errBuf}") } @@ -393,7 +393,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |} |""".stripMargin )) >> - Config.default.flatMap(handlers.ProjectGetHandler.run("example.Bad", module = None, _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("example.Bad", module = None, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("Compilation failed"), s"Stderr: ${console.errBuf}") } @@ -425,7 +425,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |""".stripMargin ) } >> - Config.default.flatMap(config => handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), config = config.copy(mill = MillConfig(millBinary)))).map { code => + handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), config = Config.bootstrap.copy(mill = MillConfig(millBinary))).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}\nStdout: ${console.outBuf}") assert(console.outBuf.toString.contains("MillClass"), s"Output: ${console.outBuf}") assert(console.outBuf.toString.contains("greet"), s"Output: ${console.outBuf}") @@ -438,7 +438,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: val console = CapturingConsole() given Console[IO] = console IO.blocking(Files.writeString(dir.resolve("build.mill").toNioPath, "")) >> - Config.default.flatMap(config => handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), config = config.copy(mill = MillConfig(millBinary)))).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), config = Config.bootstrap.copy(mill = MillConfig(millBinary))).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is required for Mill"), s"Stderr: ${console.errBuf}") } @@ -468,7 +468,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |""".stripMargin ) } >> - Config.default.flatMap(handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("SbtClass"), s"Output: ${console.outBuf}") } @@ -480,7 +480,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: val console = CapturingConsole() given Console[IO] = console IO.blocking(Files.writeString(dir.resolve("build.sbt").toNioPath, "")) >> - Config.default.flatMap(handlers.ProjectGetHandler.run("example.Foo", module = None, _, cwd = Some(dir))).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = None, Config.bootstrap, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is required for sbt"), s"Stderr: ${console.errBuf}") } From 571515eff7865d6a0b418411bbf165d3ae91d0ed Mon Sep 17 00:00:00 2001 From: rochala Date: Wed, 15 Apr 2026 00:06:39 +0200 Subject: [PATCH 2/4] Add starvation-checks config, simplify Config to sync global singleton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add StarvationChecksConfig with `enabled` flag (default: false via reference.conf) - Override runtimeConfig to disable CE starvation checker when not enabled - Replace IO-based Config.load with sync Config.global lazy val - Drop --config flag — config is loaded from default locations only - Check in .cellar/cellar.conf with starvation-checks.enabled=true for dev - Update .gitignore to track .cellar/ (except cache) - Document new config in README, add docs note to CLAUDE.md - Env var override: CELLAR_STARVATION_CHECKS_ENABLED Co-Authored-By: Claude Opus 4.6 --- README.md | 3 --- cli/src/cellar/cli/CellarApp.scala | 20 +++++++--------- lib/src/cellar/Config.scala | 20 ++++++---------- .../cellar/ProjectAwareIntegrationTest.scala | 24 +++++++++---------- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index ec48be9..de1f78a 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,6 @@ cellar get-external org.typelevel:cats-core_3:latest cats.Monad |---|---|---| | `--module `, `-m` | project commands | Build module name (required for Mill/sbt) | | `--no-cache` | project commands | Skip classpath cache, re-extract from build tool | -| `--config `, `-c` | project commands | Path to config file | | `--java-home ` | all | Use a specific JDK for JRE classpath | | `-r`, `--repository ` | external commands | Extra Maven repository URL (repeatable) | | `-l`, `--limit ` | `list`, `list-external`, `search`, `search-external` | Max results (default: 50) | @@ -152,8 +151,6 @@ Cellar loads configuration from HOCON files and environment variables. Files are 2. `~/.cellar/cellar.conf` (user-level, optional) 3. `.cellar/cellar.conf` (project-level, optional) -Use `--config ` / `-c ` to load a specific file instead. - ### Default config ```hocon diff --git a/cli/src/cellar/cli/CellarApp.scala b/cli/src/cellar/cli/CellarApp.scala index 2fe2996..dff5f60 100644 --- a/cli/src/cellar/cli/CellarApp.scala +++ b/cli/src/cellar/cli/CellarApp.scala @@ -21,7 +21,7 @@ object CellarApp override def runtimeConfig: IORuntimeConfig = val base = super.runtimeConfig - if Config.bootstrap.starvationChecks.enabled then base + if Config.global.starvationChecks.enabled then base else base.copy(cpuStarvationCheckInitialDelay = Duration.Inf) override def main: Opts[IO[ExitCode]] = @@ -58,10 +58,6 @@ object CellarApp private val noCacheOpt: Opts[Boolean] = Opts.flag("no-cache", "Skip classpath cache (re-extract from build tool)").orFalse - private val configOpt: Opts[Config] = - Opts.option[Path]("config", "Path to config file", "c").orNone - .map(p => if p.isDefined then Config.loadSync(p) else Config.bootstrap) - private def parseAndResolve(raw: String, extraRepos: List[Repository]): IO[Either[String, MavenCoordinate]] = MavenCoordinate.parse(raw) match case Left(err) => IO.pure(Left(err)) @@ -69,23 +65,23 @@ object CellarApp private val getSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("get", "Fetch symbol info from the current project") { - (symbolArg, moduleOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, config, javaHome, noCache) => - ProjectGetHandler.run(fqn, module, config, javaHome, noCache) + (symbolArg, moduleOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, javaHome, noCache) => + ProjectGetHandler.run(fqn, module, Config.global, javaHome, noCache) } } private val listSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("list", "List symbols in a package or class from the current project") { - (symbolArg, moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, config, javaHome, noCache) => - ProjectListHandler.run(fqn, module, limit, config, javaHome, noCache) + (symbolArg, moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, javaHome, noCache) => + ProjectListHandler.run(fqn, module, limit, Config.global, javaHome, noCache) } } private val searchSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("search", "Substring search for symbol names in the current project") { - (Opts.argument[String]("query"), moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { - (query, module, limit, config, javaHome, noCache) => - ProjectSearchHandler.run(query, module, limit, config, javaHome, noCache) + (Opts.argument[String]("query"), moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { + (query, module, limit, javaHome, noCache) => + ProjectSearchHandler.run(query, module, limit, Config.global, javaHome, noCache) } } diff --git a/lib/src/cellar/Config.scala b/lib/src/cellar/Config.scala index 03a8c3e..13f3dab 100644 --- a/lib/src/cellar/Config.scala +++ b/lib/src/cellar/Config.scala @@ -14,22 +14,16 @@ case class StarvationChecksConfig(enabled: Boolean) derives ConfigReader case class Config(mill: MillConfig, sbt: SbtConfig, starvationChecks: StarvationChecksConfig) derives ConfigReader object Config { - val defaultUserPath: Option[Path] = + private val defaultUserPath: Option[Path] = sys.props.get("user.home").map(Path(_).resolve(".cellar").resolve("cellar.conf")) - val defaultProjectPath: Path = Path(".cellar").resolve("cellar.conf") + private val defaultProjectPath: Path = Path(".cellar").resolve("cellar.conf") - /** Memoized bootstrap config loaded from default locations. Accessed before - * the IO runtime starts (from `IOApp.runtimeConfig`), so it must be - * synchronous. Throws `ConfigReaderException` on malformed config. + /** Global config loaded from default locations (user-level + project-level). + * Cached on first access. Throws on malformed config. */ - lazy val bootstrap: Config = loadSync(None) - - def loadSync(path: Option[Path]): Config = { - val paths = path match - case Some(p) => List(p) - case None => - (defaultUserPath.toList ++ List(defaultProjectPath)) - .filter(p => java.nio.file.Files.exists(p.toNioPath)) + lazy val global: Config = { + val paths = (defaultUserPath.toList ++ List(defaultProjectPath)) + .filter(p => java.nio.file.Files.exists(p.toNioPath)) paths .foldLeft(ConfigSource.default)((cs, p) => ConfigSource.file(p.toNioPath).withFallback(cs)) .loadOrThrow[Config] diff --git a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala index 99df0b3..8c7d64a 100644 --- a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala +++ b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala @@ -175,7 +175,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: // --- MillBuildTool tests --- test("MillBuildTool: rejects missing --module"): - build.MillBuildTool(Path("."), Config.bootstrap.mill) + build.MillBuildTool(Path("."), Config.global.mill) .extractClasspath(None).attempt.map { result => assert(result.isLeft) assert(result.left.exists(_.getMessage.contains("--module is required for Mill"))) @@ -184,7 +184,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: // --- SbtBuildTool tests --- test("SbtBuildTool: rejects missing --module"): - build.SbtBuildTool(Path("."), Config.bootstrap.sbt).extractClasspath(None) + build.SbtBuildTool(Path("."), Config.global.sbt).extractClasspath(None) .attempt.map { result => assert(result.isLeft) assert(result.left.exists(_.getMessage.contains("--module is required for sbt"))) @@ -299,7 +299,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def hello: String = "world" |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.MyClass", module = None, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.MyClass", module = None, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("MyClass"), s"Output: ${console.outBuf}") assert(console.outBuf.toString.contains("hello"), s"Output: ${console.outBuf}") @@ -320,7 +320,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - handlers.ProjectGetHandler.run("cats.Monad", module = None, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("cats.Monad", module = None, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("Monad"), s"Output: ${console.outBuf}") } @@ -339,7 +339,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def goodbye: Int = 42 |""".stripMargin )) >> - handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) val out = console.outBuf.toString assert(out.contains("hello"), s"Output: $out") @@ -359,7 +359,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("UniqueTestClassName123"), s"Output: ${console.outBuf}") } @@ -375,7 +375,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |class Foo |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is not supported"), s"Stderr: ${console.errBuf}") } @@ -393,7 +393,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |} |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.Bad", module = None, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Bad", module = None, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("Compilation failed"), s"Stderr: ${console.errBuf}") } @@ -425,7 +425,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |""".stripMargin ) } >> - handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), config = Config.bootstrap.copy(mill = MillConfig(millBinary))).map { code => + handlers.ProjectGetHandler.run("example.MillClass", module = Some("app"), cwd = Some(dir), config = Config.global.copy(mill = MillConfig(millBinary))).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}\nStdout: ${console.outBuf}") assert(console.outBuf.toString.contains("MillClass"), s"Output: ${console.outBuf}") assert(console.outBuf.toString.contains("greet"), s"Output: ${console.outBuf}") @@ -438,7 +438,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: val console = CapturingConsole() given Console[IO] = console IO.blocking(Files.writeString(dir.resolve("build.mill").toNioPath, "")) >> - handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), config = Config.bootstrap.copy(mill = MillConfig(millBinary))).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir), config = Config.global.copy(mill = MillConfig(millBinary))).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is required for Mill"), s"Stderr: ${console.errBuf}") } @@ -468,7 +468,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |""".stripMargin ) } >> - handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("SbtClass"), s"Output: ${console.outBuf}") } @@ -480,7 +480,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: val console = CapturingConsole() given Console[IO] = console IO.blocking(Files.writeString(dir.resolve("build.sbt").toNioPath, "")) >> - handlers.ProjectGetHandler.run("example.Foo", module = None, Config.bootstrap, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = None, Config.global, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is required for sbt"), s"Stderr: ${console.errBuf}") } From 59be07c71499864f8a83f0ea95e54839353e4dec Mon Sep 17 00:00:00 2001 From: rochala Date: Wed, 15 Apr 2026 00:18:53 +0200 Subject: [PATCH 3/4] Default config parameter to Config.global, drop --config flag Config is now accessed via Config.global throughout. Handlers default to it but accept an override for testing (e.g. custom mill binary). CellarApp no longer passes config explicitly. Co-Authored-By: Claude Opus 4.6 --- cli/src/cellar/cli/CellarApp.scala | 6 +++--- .../cellar/build/ProjectClasspathProvider.scala | 2 +- lib/src/cellar/handlers/ProjectGetHandler.scala | 4 ++-- lib/src/cellar/handlers/ProjectHandler.scala | 2 +- lib/src/cellar/handlers/ProjectListHandler.scala | 4 ++-- .../cellar/handlers/ProjectSearchHandler.scala | 4 ++-- .../src/cellar/ProjectAwareIntegrationTest.scala | 16 ++++++++-------- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/src/cellar/cli/CellarApp.scala b/cli/src/cellar/cli/CellarApp.scala index dff5f60..7b7b2e5 100644 --- a/cli/src/cellar/cli/CellarApp.scala +++ b/cli/src/cellar/cli/CellarApp.scala @@ -66,14 +66,14 @@ object CellarApp private val getSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("get", "Fetch symbol info from the current project") { (symbolArg, moduleOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, javaHome, noCache) => - ProjectGetHandler.run(fqn, module, Config.global, javaHome, noCache) + ProjectGetHandler.run(fqn, module, javaHome, noCache) } } private val listSubcmd: Opts[IO[ExitCode]] = Opts.subcommand("list", "List symbols in a package or class from the current project") { (symbolArg, moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, javaHome, noCache) => - ProjectListHandler.run(fqn, module, limit, Config.global, javaHome, noCache) + ProjectListHandler.run(fqn, module, limit, javaHome, noCache) } } @@ -81,7 +81,7 @@ object CellarApp Opts.subcommand("search", "Substring search for symbol names in the current project") { (Opts.argument[String]("query"), moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { (query, module, limit, javaHome, noCache) => - ProjectSearchHandler.run(query, module, limit, Config.global, javaHome, noCache) + ProjectSearchHandler.run(query, module, limit, javaHome, noCache) } } diff --git a/lib/src/cellar/build/ProjectClasspathProvider.scala b/lib/src/cellar/build/ProjectClasspathProvider.scala index 074ffde..71b60fd 100644 --- a/lib/src/cellar/build/ProjectClasspathProvider.scala +++ b/lib/src/cellar/build/ProjectClasspathProvider.scala @@ -13,7 +13,7 @@ object ProjectClasspathProvider: module: Option[String], jreClasspath: Classpath, noCache: Boolean, - config: Config + config: Config = Config.global ): Resource[IO, (Context, Classpath)] = Resource.eval(resolveClasspath(cwd, module, noCache, config)).flatMap { paths => ContextResource.make(paths, jreClasspath) diff --git a/lib/src/cellar/handlers/ProjectGetHandler.scala b/lib/src/cellar/handlers/ProjectGetHandler.scala index 5db8660..397626a 100644 --- a/lib/src/cellar/handlers/ProjectGetHandler.scala +++ b/lib/src/cellar/handlers/ProjectGetHandler.scala @@ -10,10 +10,10 @@ object ProjectGetHandler: def run( fqn: String, module: Option[String], - config: Config, javaHome: Option[Path] = None, noCache: Boolean = false, - cwd: Option[Path] = None + cwd: Option[Path] = None, + config: Config = Config.global )(using Console[IO]): IO[ExitCode] = ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, classpath, _) => given Context = ctx diff --git a/lib/src/cellar/handlers/ProjectHandler.scala b/lib/src/cellar/handlers/ProjectHandler.scala index 45a2be9..2683156 100644 --- a/lib/src/cellar/handlers/ProjectHandler.scala +++ b/lib/src/cellar/handlers/ProjectHandler.scala @@ -13,7 +13,7 @@ object ProjectHandler: cwd: Option[Path], module: Option[String], noCache: Boolean, - config: Config + config: Config = Config.global )(body: (Context, Classpath, Classpath) => IO[ExitCode])(using Console[IO]): IO[ExitCode] = val program = for diff --git a/lib/src/cellar/handlers/ProjectListHandler.scala b/lib/src/cellar/handlers/ProjectListHandler.scala index 3485a3e..2b03c27 100644 --- a/lib/src/cellar/handlers/ProjectListHandler.scala +++ b/lib/src/cellar/handlers/ProjectListHandler.scala @@ -11,10 +11,10 @@ object ProjectListHandler: fqn: String, module: Option[String], limit: Int, - config: Config, javaHome: Option[Path] = None, noCache: Boolean = false, - cwd: Option[Path] = None + cwd: Option[Path] = None, + config: Config = Config.global )(using Console[IO]): IO[ExitCode] = ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, _, _) => given Context = ctx diff --git a/lib/src/cellar/handlers/ProjectSearchHandler.scala b/lib/src/cellar/handlers/ProjectSearchHandler.scala index 6ca1361..2b255ee 100644 --- a/lib/src/cellar/handlers/ProjectSearchHandler.scala +++ b/lib/src/cellar/handlers/ProjectSearchHandler.scala @@ -11,10 +11,10 @@ object ProjectSearchHandler: query: String, module: Option[String], limit: Int, - config: Config, javaHome: Option[Path] = None, noCache: Boolean = false, - cwd: Option[Path] = None + cwd: Option[Path] = None, + config: Config = Config.global )(using Console[IO]): IO[ExitCode] = ProjectHandler.run(javaHome, cwd, module, noCache, config) { (ctx, classpath, jreClasspath) => given Context = ctx diff --git a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala index 8c7d64a..0373c97 100644 --- a/lib/test/src/cellar/ProjectAwareIntegrationTest.scala +++ b/lib/test/src/cellar/ProjectAwareIntegrationTest.scala @@ -299,7 +299,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def hello: String = "world" |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.MyClass", module = None, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.MyClass", module = None, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("MyClass"), s"Output: ${console.outBuf}") assert(console.outBuf.toString.contains("hello"), s"Output: ${console.outBuf}") @@ -320,7 +320,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - handlers.ProjectGetHandler.run("cats.Monad", module = None, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("cats.Monad", module = None, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("Monad"), s"Output: ${console.outBuf}") } @@ -339,7 +339,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def goodbye: Int = 42 |""".stripMargin )) >> - handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectListHandler.run("example.MyClass", module = None, limit = 50, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) val out = console.outBuf.toString assert(out.contains("hello"), s"Output: $out") @@ -359,7 +359,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: | def run: Unit = () |""".stripMargin )) >> - handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectSearchHandler.run("UniqueTestClassName123", module = None, limit = 50, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success) assert(console.outBuf.toString.contains("UniqueTestClassName123"), s"Output: ${console.outBuf}") } @@ -375,7 +375,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |class Foo |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = Some("bar"), cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is not supported"), s"Stderr: ${console.errBuf}") } @@ -393,7 +393,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |} |""".stripMargin )) >> - handlers.ProjectGetHandler.run("example.Bad", module = None, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Bad", module = None, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("Compilation failed"), s"Stderr: ${console.errBuf}") } @@ -468,7 +468,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: |""".stripMargin ) } >> - handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.SbtClass", module = Some("cellar-test"), cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Success, s"Stderr: ${console.errBuf}") assert(console.outBuf.toString.contains("SbtClass"), s"Output: ${console.outBuf}") } @@ -480,7 +480,7 @@ class ProjectAwareIntegrationTest extends CatsEffectSuite: val console = CapturingConsole() given Console[IO] = console IO.blocking(Files.writeString(dir.resolve("build.sbt").toNioPath, "")) >> - handlers.ProjectGetHandler.run("example.Foo", module = None, Config.global, cwd = Some(dir)).map { code => + handlers.ProjectGetHandler.run("example.Foo", module = None, cwd = Some(dir)).map { code => assertEquals(code, ExitCode.Error) assert(console.errBuf.toString.contains("--module is required for sbt"), s"Stderr: ${console.errBuf}") } From 7180935ce3d5a255fd2428514992e3ac623c8cd2 Mon Sep 17 00:00:00 2001 From: rochala Date: Wed, 15 Apr 2026 00:22:16 +0200 Subject: [PATCH 4/4] Remove redundant scaladoc, fix missing newline in reference.conf Co-Authored-By: Claude Opus 4.6 --- lib/resources/reference.conf | 2 +- lib/src/cellar/Config.scala | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/resources/reference.conf b/lib/resources/reference.conf index 28199cd..6245847 100644 --- a/lib/resources/reference.conf +++ b/lib/resources/reference.conf @@ -21,4 +21,4 @@ starvation-checks { # a short-lived CLI. Disabled by default; set to true if debugging. enabled = false enabled = ${?CELLAR_STARVATION_CHECKS_ENABLED} -} \ No newline at end of file +} diff --git a/lib/src/cellar/Config.scala b/lib/src/cellar/Config.scala index 13f3dab..9927f62 100644 --- a/lib/src/cellar/Config.scala +++ b/lib/src/cellar/Config.scala @@ -18,9 +18,6 @@ object Config { sys.props.get("user.home").map(Path(_).resolve(".cellar").resolve("cellar.conf")) private val defaultProjectPath: Path = Path(".cellar").resolve("cellar.conf") - /** Global config loaded from default locations (user-level + project-level). - * Cached on first access. Throws on malformed config. - */ lazy val global: Config = { val paths = (defaultUserPath.toList ++ List(defaultProjectPath)) .filter(p => java.nio.file.Files.exists(p.toNioPath))