diff --git a/NEWS.md b/NEWS.md index 332141e0..3f0e34fc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,8 @@ * 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 docs topics ` to the `btw` CLI for discovering a package's help topics and vignettes. Use `--only help` or `--only vignettes` to limit output to one section, or `--json` for machine-readable output (#195). + * Added `btw help` to the `btw` CLI, which prints the `r-btw-cli` skill — a usage guide designed for AI agents. ## Bug fixes diff --git a/exec/btw.R b/exec/btw.R index a1d85360..87a03aa4 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -63,9 +63,6 @@ btw_docs_help <- function(topic, package) { ) } btw_output(btw_this(btw:::as_btw_docs_topic(parts[1], parts[2]))) - } else if (grepl("^\\{.+\\}$", topic)) { - pkg_name <- sub("^\\{(.+)\\}$", "\\1", topic) - btw_output(btw_this(btw:::as_btw_docs_package(pkg_name))) } else if (has_value(package)) { btw_output(btw_this(btw:::as_btw_docs_topic(package, topic))) } else { @@ -81,6 +78,91 @@ btw_docs_help <- function(topic, package) { } } +btw_docs_topics <- function(package, only, json = FALSE) { + if (!only %in% c("", "help", "vignettes")) { + stop("--only must be \"help\" or \"vignettes\"", call. = FALSE) + } + + include_help <- only == "" || only == "help" + include_vignettes <- only == "" || only == "vignettes" + + if (json) { + out <- list() + + if (include_help) { + result <- btw:::btw_tool_docs_package_help_topics_impl(package) + df <- S7::prop(result, "extra")$data + out$help <- lapply(seq_len(nrow(df)), function(i) { + list(topic_id = df$topic_id[[i]], title = df$title[[i]], aliases = df$aliases[[i]]) + }) + } + + if (include_vignettes) { + out$vignettes <- tryCatch( + { + result <- btw:::btw_tool_docs_available_vignettes_impl(package) + df <- S7::prop(result, "extra")$data + lapply(seq_len(nrow(df)), function(i) { + list(vignette = df$vignette[[i]], title = df$title[[i]]) + }) + }, + error = function(e) list() + ) + } + + btw_json_output(out) + return(invisible(NULL)) + } + + if (include_help) { + result <- btw:::btw_tool_docs_package_help_topics_impl(package) + df <- S7::prop(result, "extra")$data + lines <- mapply( + function(topic_id, title, aliases) { + sprintf("* `%s` - %s", topic_id, title) + }, + df$topic_id, + df$title, + df$aliases + ) + cat( + "## Help topics\n\n", + lines, + sprintf( + "\nUse `btw docs help %s::` to read a help page.\n", + package + ), + sep = "\n" + ) + } + + if (include_vignettes) { + if (include_help) { + cat("\n") + } + cat("## Vignettes\n\n") + tryCatch( + { + result <- btw:::btw_tool_docs_available_vignettes_impl(package) + df <- S7::prop(result, "extra")$data + lines <- sprintf("* `%s` - %s", df$vignette, df$title) + cat( + lines, + sprintf( + "\nUse `btw docs vignette %s --name ` to read a vignette.\n", + package + ), + sep = "\n" + ) + }, + error = function(e) { + msg <- cli::ansi_strip(conditionMessage(e)) + cat(msg, "\n") + } + ) + } +} + btw_docs_vignette <- function(package, name, list) { if (list) { btw_output(btw_this(utils::vignette(package = package))) @@ -243,9 +325,22 @@ switch( switch( docs_cmd <- "", + #| title: List help topics and vignettes for a package + topics = { + #| description: Package name. + package <- NULL + #| description: Limit output to "help" topics or "vignettes". + #| short: 'o' + only <- "" + #| description: Output as JSON with top-level keys "help" (array of {topic_id, title, aliases[]}) and "vignettes" (array of {vignette, title}). + json <- FALSE + + tryCatch(btw_docs_topics(package, only, json), error = btw_error) + }, + #| title: Show help for a topic or package help = { - #| description: Help topic, package name, or {package} for package listing. + #| description: Help topic, or pkg::topic to scope to a specific package. topic <- NULL #| description: Package name to scope the help topic. #| short: 'p' @@ -417,7 +512,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)) { diff --git a/inst/cli-skill/r-btw-cli/SKILL.md b/inst/cli-skill/r-btw-cli/SKILL.md index 7ef6cf6f..ae6408ec 100644 --- a/inst/cli-skill/r-btw-cli/SKILL.md +++ b/inst/cli-skill/r-btw-cli/SKILL.md @@ -18,11 +18,11 @@ description: "Use the `btw` CLI to access R documentation, manage R package deve Use `btw docs` to read R help pages, vignettes, and package NEWS for locally installed packages. ``` -btw docs help [-p ] Read an R help page (tries topic first, falls back to package listing) -btw docs help {} List help topics for a package -btw docs help :: Read a specific help page (scoped) -btw docs vignette [-n ] Read a vignette (--list to list available) -btw docs news [-s ] Read package NEWS/changelog +btw docs topics [--only help|vignettes] List help topics and vignettes for a package +btw docs help [-p ] Read an R help page +btw docs help :: Read a specific help page (scoped) +btw docs vignette [-n ] Read a vignette (--list to list available) +btw docs news [-s ] Read package NEWS/changelog ``` Use `btw pkg` to run development tasks on an R package under active development. @@ -49,4 +49,4 @@ btw cran search [-n ] [--json] Search CRAN for packages btw cran info [--json] CRAN package details ``` -Run `btw --help`, `btw --help`, or `btw --help` for full usage details. +Run `btw --help`, `btw --help`, or `btw --help` for full usage details. \ No newline at end of file diff --git a/tests/testthat/_snaps/cli.md b/tests/testthat/_snaps/cli.md index 13782844..11831955 100644 --- a/tests/testthat/_snaps/cli.md +++ b/tests/testthat/_snaps/cli.md @@ -33,6 +33,7 @@ Usage: btw docs [OPTIONS] Commands: + topics List help topics and vignettes for a package help Show help for a topic or package vignette Read a package vignette news Show package NEWS @@ -61,7 +62,7 @@ Enable with `--version`. Arguments: - Help topic, package name, or {package} for package listing. + Help topic, or pkg::topic to scope to a specific package. # btw pkg --help shows pkg group help diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index e393a9c4..f1e1930b 100644 --- a/tests/testthat/test-cli.R +++ b/tests/testthat/test-cli.R @@ -81,12 +81,6 @@ test_that("btw docs help resolves topic first", { expect_equal(env$package, "") }) -test_that("btw docs help {package} lists help topics", { - env <- run_btw_quietly("docs", "help", "{stats}") - expect_true(is.environment(env)) - expect_equal(env$topic, "{stats}") - expect_equal(env$package, "") -}) test_that("btw docs help -p reads help page", { local_skip_pandoc_convert_text() @@ -126,6 +120,28 @@ test_that("btw docs help errors for unknown topic", { expect_match(result$stderr, "completely_nonexistent_xyz", ignore.case = TRUE) }) + +# docs topics ---------------------------------------------------------- + +test_that("btw docs topics shows topics and vignettes", { + env <- run_btw_quietly("docs", "topics", "stats") + expect_equal(env$package, "stats") + expect_equal(env$only, "") +}) + +test_that("btw docs topics --only help shows only help topics", { + env <- run_btw_quietly("docs", "topics", "stats", "--only", "help") + expect_equal(env$package, "stats") + expect_equal(env$only, "help") +}) + +test_that("btw docs topics --only vignettes shows only vignettes", { + skip_if_not_installed("dplyr") + env <- run_btw_quietly("docs", "topics", "dplyr", "--only", "vignettes") + expect_equal(env$package, "dplyr") + expect_equal(env$only, "vignettes") +}) + # docs vignette ---------------------------------------------------------- test_that("btw docs vignette --list lists vignettes", { @@ -458,4 +474,4 @@ test_that("btw pkg error exits with code 1 and message on stderr", { expect_equal(result$status, 1) expect_match(result$stderr, "nonexistent_pkg_xyz", ignore.case = TRUE) expect_equal(trimws(result$stdout), "") -}) +}) \ No newline at end of file