From aaa671fd0034176cb59d697bb5bccac7768a081d Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 09:36:40 -0400 Subject: [PATCH 1/5] refactor(cli): replace btw info with system-info, check-installed, installed-packages - btw system-info: replaces btw info platform - btw check-installed : replaces btw info packages --check; exits 0 by default regardless of install status, --fail for non-zero exit on missing packages; --json returns array with package/version/installed fields - btw installed-packages : replaces btw info packages; requires explicit package names - btw info: deprecated stub that prints migration guidance and exits 1 - btw_json_output: add null = "null" to toJSON call --- exec/btw.R | 148 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index a1d85360..9f9be4af 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -22,7 +22,16 @@ if (version) { has_value <- function(x) !is.na(x) && nzchar(x) btw_json_output <- function(x) { - cat(jsonlite::toJSON(x, auto_unbox = TRUE, pretty = TRUE, na = "null"), "\n") + cat( + jsonlite::toJSON( + x, + auto_unbox = TRUE, + pretty = TRUE, + na = "null", + null = "null" + ), + "\n" + ) } btw_output <- function(x) { @@ -145,7 +154,7 @@ btw_pkg_coverage <- function(path, file, json = FALSE) { } } -btw_info_platform <- function(json = FALSE) { +btw_system_info <- function(json = FALSE) { result <- btw:::btw_tool_sessioninfo_platform_impl() if (json) { btw_json_output(S7::prop(result, "extra")) @@ -154,31 +163,50 @@ btw_info_platform <- function(json = FALSE) { } } -btw_info_packages <- function(packages, deps, check, json = FALSE) { - pkgs <- packages - if (check && length(pkgs) > 0) { - if (json) { - results <- lapply(pkgs, function(pkg) { - result <- btw:::btw_tool_sessioninfo_is_package_installed_impl(pkg) - S7::prop(result, "extra") - }) - btw_json_output(results) - } else { - for (pkg in pkgs) { - btw_output(btw:::btw_tool_sessioninfo_is_package_installed_impl(pkg)) +btw_check_installed <- function(packages, fail = FALSE, json = FALSE) { + results <- lapply(packages, function(pkg) { + tryCatch( + btw:::btw_tool_sessioninfo_is_package_installed_impl(pkg), + error = function(e) e + ) + }) + + errors <- vapply(results, inherits, logical(1), "error") + + if (json) { + data <- unname(Map( + function(pkg, r) { + if (inherits(r, "error")) { + list(package = pkg, version = NULL, installed = FALSE) + } else { + c(S7::prop(r, "extra"), list(installed = TRUE)) + } + }, + packages, + results + )) + btw_json_output(data) + } else { + for (r in results) { + if (inherits(r, "error")) { + cat(cli::ansi_strip(conditionMessage(r)), "\n") + } else { + btw_output(r) } } + } + + if (fail && any(errors)) quit(status = 1) +} + +btw_installed_packages <- function(packages, deps, json = FALSE) { + pkgs <- packages + deps_val <- if (has_value(deps)) deps else "" + result <- btw:::btw_tool_sessioninfo_package_impl(pkgs, deps_val) + if (json) { + btw_json_output(S7::prop(result, "extra")$data) } else { - if (length(pkgs) == 0) { - pkgs <- "attached" - } - deps_val <- if (has_value(deps)) deps else "" - result <- btw:::btw_tool_sessioninfo_package_impl(pkgs, deps_val) - if (json) { - btw_json_output(S7::prop(result, "extra")$data) - } else { - btw_output(result) - } + btw_output(result) } } @@ -325,36 +353,62 @@ switch( if (pkg_cmd == "") btw_self_help("pkg") }, - #| title: Inspect the R session and environment + #| title: "[Deprecated] Inspect the R session and environment" info = { - #| description: Output as JSON. - json <- FALSE - switch( info_cmd <- "", - - #| title: Show platform and session info - platform = { - tryCatch(btw_info_platform(json), error = btw_error) - }, - - #| title: Show installed package information + platform = {}, packages = { - #| description: Package names to query. `packages...` <- c() - #| description: Dependency types to include. deps <- "" - #| description: Check if packages are installed. - #| short: 'c' check <- FALSE - - tryCatch( - btw_info_packages(`packages...`, deps, check, json), - error = btw_error - ) } ) - if (info_cmd == "") btw_self_help("info") + cat( + "btw info is deprecated. Use:\n", + " btw system-info (was: btw info platform)\n", + " btw check-installed (was: btw info packages --check)\n", + " btw installed-packages (was: btw info packages)\n", + file = stderr() + ) + quit(status = 1) + }, + + #| title: Show platform and R session info + system_info = { + #| description: "Output as JSON object with fields: r_version, os, system, ui, language, locale, encoding, timezone, date." + json <- FALSE + + tryCatch(btw_system_info(json), error = btw_error) + }, + + #| title: Check if packages are installed + check_installed = { + #| description: Package names to check. + #| required: true + `packages...` <- c() + #| description: Exit with a non-zero status if any package is not installed. + fail <- FALSE + #| description: "Output as JSON array of objects with fields: package (string), version (string or null if not installed), installed (bool)." + json <- FALSE + + tryCatch(btw_check_installed(`packages...`, fail, json), error = btw_error) + }, + + #| title: Show installed package information + installed_packages = { + #| description: Package names to query. + #| required: true + `packages...` <- c() + #| description: "Dependency types to include. Use TRUE for all types, FALSE for none, or a comma-separated list of types: Depends, Imports, Suggests, LinkingTo, Enhances." + deps <- "" + #| description: "Output as JSON array of objects with fields: package, ondiskversion, loadedversion, path, loadedpath, attached, is_base, date, source, md5ok, library." + json <- FALSE + + tryCatch( + btw_installed_packages(`packages...`, deps, json), + error = btw_error + ) }, #| title: Query CRAN package metadata @@ -417,7 +471,9 @@ switch( #| title: Show btw CLI usage guide for AI agents help = { skill_path <- system.file( - "cli-skill", "r-btw-cli", "SKILL.md", + "cli-skill", + "r-btw-cli", + "SKILL.md", package = "btw" ) if (!nzchar(skill_path)) { From b8246d08b2046b6c4cade7dc2bcfb4eea81244b5 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 09:38:34 -0400 Subject: [PATCH 2/5] test(cli): update tests for system-info, check-installed, installed-packages - Replace btw info tests with tests for new top-level commands - Add deprecation test for btw info - Add btw check-installed exit code tests (0 by default, 1 with --fail) - Add btw check-installed --json null version for not-installed package - Update btw --help snapshot --- tests/testthat/_snaps/cli.md | 39 +++------ tests/testthat/test-cli.R | 148 +++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 87 deletions(-) diff --git a/tests/testthat/_snaps/cli.md b/tests/testthat/_snaps/cli.md index 13782844..2bdb43a7 100644 --- a/tests/testthat/_snaps/cli.md +++ b/tests/testthat/_snaps/cli.md @@ -9,13 +9,16 @@ Wraps btw package tools for docs, pkg, info, and cran operations. Commands: - docs Access R documentation - pkg Work with an R package under development - info Inspect the R session and environment - cran Query CRAN package metadata - skills Manage btw skills - help Show btw CLI usage guide for AI agents - app Run btw_app() in the current directory + docs Access R documentation + pkg Work with an R package under development + info [Deprecated] Inspect the R session and environment + system-info Show platform and R session info + check-installed Check if packages are installed + installed-packages Show installed package information + cran Query CRAN package metadata + skills Manage btw skills + help Show btw CLI usage guide for AI agents + app Run btw_app() in the current directory Options: --version / --no-version Print btw version and exit. [default: false] @@ -88,28 +91,6 @@ For help with a specific command, run: `btw pkg --help`. -# btw info --help shows info group help - - Code - run_btw("info", "--help") - Output - Inspect the R session and environment - - Usage: btw info [OPTIONS] - - Commands: - platform Show platform and session info - packages Show installed package information - - Options: - --json / --no-json Output as JSON. [default: false] Enable with `--json`. - - Global options: - --version / --no-version Print btw version and exit. [default: false] - Enable with `--version`. - - For help with a specific command, run: `btw info --help`. - # btw cran --help shows cran group help Code diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index e393a9c4..78413082 100644 --- a/tests/testthat/test-cli.R +++ b/tests/testthat/test-cli.R @@ -55,10 +55,6 @@ test_that("btw pkg --help shows pkg group help", { expect_snapshot(run_btw("pkg", "--help")) }) -test_that("btw info --help shows info group help", { - expect_snapshot(run_btw("info", "--help")) -}) - test_that("btw cran --help shows cran group help", { expect_snapshot(run_btw("cran", "--help")) }) @@ -284,78 +280,66 @@ test_that("btw pkg coverage --file passes filename", { expect_equal(mock_filename, "utils.R") }) -# info group ------------------------------------------------------------- +# btw info deprecated ---------------------------------------------------- + +test_that("btw info exits 1 with deprecation message", { + result <- run_btw_subprocess("info", "platform") + expect_equal(result$status, 1) + expect_match(result$stderr, "deprecated", ignore.case = TRUE) + expect_match(result$stderr, "system-info") + expect_match(result$stderr, "check-installed") + expect_match(result$stderr, "installed-packages") +}) + +# btw system-info -------------------------------------------------------- -test_that("btw info platform shows platform info", { +test_that("btw system-info shows platform info", { local_sessioninfo_quarto_version() - env <- run_btw_quietly("info", "platform") + env <- run_btw_quietly("system-info") expect_true(is.environment(env)) }) -test_that("btw info packages with no args defaults to attached", { - mock_pkgs <- NULL - local_mocked_bindings( - btw_tool_sessioninfo_package_impl = function(packages, dependencies) { - mock_pkgs <<- packages - "package info" - } - ) - run_btw_quietly("info", "packages") - expect_equal(mock_pkgs, "attached") +test_that("btw system-info --json outputs valid JSON", { + local_sessioninfo_quarto_version() + env <- run_btw_quietly("system-info", "--json") + parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n")) + expect_type(parsed, "list") + expect_true("os:" %in% names(parsed)) }) -test_that("btw info packages with package names", { - mock_pkgs <- NULL - local_mocked_bindings( - btw_tool_sessioninfo_package_impl = function(packages, dependencies) { - mock_pkgs <<- packages - "package info" - } - ) - run_btw_quietly("info", "packages", "dplyr", "ggplot2") - expect_equal(mock_pkgs, c("dplyr", "ggplot2")) -}) +# btw check-installed ---------------------------------------------------- -test_that("btw info packages --check calls is_package_installed", { +test_that("btw check-installed calls is_package_installed for each package", { checked_pkgs <- character() local_mocked_bindings( btw_tool_sessioninfo_is_package_installed_impl = function(package_name) { checked_pkgs <<- c(checked_pkgs, package_name) - paste(package_name, "is installed") + btw:::BtwToolResult( + value = paste(package_name, "is installed"), + extra = list(package = package_name, version = "1.0.0") + ) } ) - run_btw_quietly("info", "packages", "dplyr", "ggplot2", "--check") + run_btw_quietly("check-installed", "dplyr", "ggplot2") expect_equal(checked_pkgs, c("dplyr", "ggplot2")) }) -test_that("btw info platform --json outputs valid JSON", { - local_sessioninfo_quarto_version() - env <- run_btw_quietly("info", "platform", "--json") - parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n")) - expect_type(parsed, "list") - expect_true("os:" %in% names(parsed)) +test_that("btw check-installed exits 0 when package is not installed", { + result <- run_btw_subprocess("check-installed", "completely_nonexistent_xyz_pkg") + expect_equal(result$status, 0) }) -test_that("btw info packages --json outputs valid JSON", { - mock_df <- data.frame( - package = c("dplyr", "ggplot2"), - version = c("1.1.4", "3.5.0"), - stringsAsFactors = FALSE - ) - local_mocked_bindings( - btw_tool_sessioninfo_package_impl = function(packages, dependencies) { - btw:::BtwPackageInfoToolResult( - value = "pkg info", - extra = list(data = mock_df) - ) - } - ) - env <- run_btw_quietly("info", "packages", "--json") - parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n")) - expect_equal(parsed$package, c("dplyr", "ggplot2")) +test_that("btw check-installed --fail exits 1 when package is not installed", { + result <- run_btw_subprocess("check-installed", "completely_nonexistent_xyz_pkg", "--fail") + expect_equal(result$status, 1) }) -test_that("btw info packages --check --json outputs valid JSON", { +test_that("btw check-installed --fail exits 0 when all packages are installed", { + result <- run_btw_subprocess("check-installed", "base", "--fail") + expect_equal(result$status, 0) +}) + +test_that("btw check-installed --json outputs array with installed field", { local_mocked_bindings( btw_tool_sessioninfo_is_package_installed_impl = function(package_name) { btw:::BtwToolResult( @@ -364,13 +348,42 @@ test_that("btw info packages --check --json outputs valid JSON", { ) } ) - env <- run_btw_quietly("info", "packages", "dplyr", "ggplot2", "--check", "--json") + env <- run_btw_quietly("check-installed", "dplyr", "ggplot2", "--json") parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n")) expect_equal(nrow(parsed), 2) expect_equal(parsed$package, c("dplyr", "ggplot2")) + expect_true(all(parsed$installed)) +}) + +test_that("btw check-installed --json has null version for not-installed package", { + local_mocked_bindings( + btw_tool_sessioninfo_is_package_installed_impl = function(package_name) { + stop(paste("Package", package_name, "is not installed.")) + } + ) + env <- run_btw_quietly("check-installed", "notapkg", "--json") + parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n"), simplifyVector = FALSE) + expect_length(parsed, 1) + expect_equal(parsed[[1]]$package, "notapkg") + expect_null(parsed[[1]]$version) + expect_false(parsed[[1]]$installed) +}) + +# btw installed-packages ------------------------------------------------- + +test_that("btw installed-packages passes package names", { + mock_pkgs <- NULL + local_mocked_bindings( + btw_tool_sessioninfo_package_impl = function(packages, dependencies) { + mock_pkgs <<- packages + "package info" + } + ) + run_btw_quietly("installed-packages", "dplyr", "ggplot2") + expect_equal(mock_pkgs, c("dplyr", "ggplot2")) }) -test_that("btw info packages --deps passes dependency types", { +test_that("btw installed-packages --deps passes dependency types", { mock_deps <- NULL local_mocked_bindings( btw_tool_sessioninfo_package_impl = function(packages, dependencies) { @@ -378,10 +391,29 @@ test_that("btw info packages --deps passes dependency types", { "package info" } ) - run_btw_quietly("info", "packages", "dplyr", "--deps", "Imports,Suggests") + run_btw_quietly("installed-packages", "dplyr", "--deps", "Imports,Suggests") expect_equal(mock_deps, "Imports,Suggests") }) +test_that("btw installed-packages --json outputs valid JSON", { + mock_df <- data.frame( + package = c("dplyr", "ggplot2"), + version = c("1.1.4", "3.5.0"), + stringsAsFactors = FALSE + ) + local_mocked_bindings( + btw_tool_sessioninfo_package_impl = function(packages, dependencies) { + btw:::BtwPackageInfoToolResult( + value = "pkg info", + extra = list(data = mock_df) + ) + } + ) + env <- run_btw_quietly("installed-packages", "dplyr", "ggplot2", "--json") + parsed <- jsonlite::fromJSON(paste(env$.output, collapse = "\n")) + expect_equal(parsed$package, c("dplyr", "ggplot2")) +}) + # cran group ------------------------------------------------------------- test_that("btw cran search calls pkgsearch and btw_this", { From 5f668646d21c69836acb8b1381ba3e85657a7972 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 09:57:22 -0400 Subject: [PATCH 3/5] fix(cli): address code review feedback - btw_check_installed: use extra[["installed"]] <- TRUE instead of c() to avoid duplication if upstream adds installed to extra - btw_check_installed: route not-installed messages to stderr for consistency and to avoid corrupting stdout when piping --json output - btw_installed_packages: remove redundant pkgs alias --- exec/btw.R | 9 +++++---- tests/testthat/test-cli.R | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index 9f9be4af..414ef273 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -179,7 +179,9 @@ btw_check_installed <- function(packages, fail = FALSE, json = FALSE) { if (inherits(r, "error")) { list(package = pkg, version = NULL, installed = FALSE) } else { - c(S7::prop(r, "extra"), list(installed = TRUE)) + extra <- S7::prop(r, "extra") + extra[["installed"]] <- TRUE + extra } }, packages, @@ -189,7 +191,7 @@ btw_check_installed <- function(packages, fail = FALSE, json = FALSE) { } else { for (r in results) { if (inherits(r, "error")) { - cat(cli::ansi_strip(conditionMessage(r)), "\n") + cat(cli::ansi_strip(conditionMessage(r)), "\n", file = stderr()) } else { btw_output(r) } @@ -200,9 +202,8 @@ btw_check_installed <- function(packages, fail = FALSE, json = FALSE) { } btw_installed_packages <- function(packages, deps, json = FALSE) { - pkgs <- packages deps_val <- if (has_value(deps)) deps else "" - result <- btw:::btw_tool_sessioninfo_package_impl(pkgs, deps_val) + result <- btw:::btw_tool_sessioninfo_package_impl(packages, deps_val) if (json) { btw_json_output(S7::prop(result, "extra")$data) } else { diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index 78413082..8272830d 100644 --- a/tests/testthat/test-cli.R +++ b/tests/testthat/test-cli.R @@ -355,6 +355,7 @@ test_that("btw check-installed --json outputs array with installed field", { expect_true(all(parsed$installed)) }) + test_that("btw check-installed --json has null version for not-installed package", { local_mocked_bindings( btw_tool_sessioninfo_is_package_installed_impl = function(package_name) { From ce1ae0b5cad8b768791ef81cd8a2afdaa84f1fd7 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 10:01:28 -0400 Subject: [PATCH 4/5] refactor(cli): rename installed-packages title to 'Show installed package versions' --- exec/btw.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exec/btw.R b/exec/btw.R index 414ef273..bac143e2 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -396,7 +396,7 @@ switch( tryCatch(btw_check_installed(`packages...`, fail, json), error = btw_error) }, - #| title: Show installed package information + #| title: Show installed package versions installed_packages = { #| description: Package names to query. #| required: true From 64c515d4f359c076f66beae48db566cbbb6d642d Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 10:03:32 -0400 Subject: [PATCH 5/5] docs(news): add entry for btw info refactor (#195) --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 332141e0..08e8bce5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,8 @@ * Added `btw_tool_files_patch()`, a new files tool that applies a structured diff-style patch envelope to make coordinated changes across multiple files in a single call. One patch can add, update, delete, and rename files atomically: all operations are validated before any file is written, so a partial failure leaves the working tree untouched (#190). +* The `btw info` CLI command group has been replaced by three focused top-level commands: `btw system-info` (platform and R session info), `btw check-installed ` (check if packages are installed, exits 0 by default with `--fail` for non-zero exit on missing packages), and `btw installed-packages ` (show installed package versions). `btw info` is retained as a deprecated stub that prints migration guidance. All three commands support `--json` output with documented field shapes (#195). + * Added two new commands to the `btw` CLI: `btw app` to launch a `btw_app()` session in the current working directory and `btw skills install` to install skills from the terminal. * Added `btw help` to the `btw` CLI, which prints the `r-btw-cli` skill — a usage guide designed for AI agents.