From 585e0576cfeb55a0dc1ec2ac26241de546edf5b6 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 13 May 2026 16:56:51 -0400 Subject: [PATCH 01/10] Add `btw docs topics` command, remove `{pkg}` brace syntax - Add `btw docs topics [--only help|vignettes]` as a dedicated discovery command that lists help topics and vignettes for a package in two clearly-labelled sections - Remove the ambiguous `{pkg}` brace syntax from `btw docs help`; topic listing is now handled exclusively by `btw docs topics` - Update `btw docs help ` description to drop the `{package}` reference and clarify `pkg::topic` scoping - Add `btw_docs_topics()` implementation with input validation on `--only` (accepts "", "help", or "vignettes") - Update SKILL.md to document the new command and drop the brace syntax - Add tests for all three `--only` modes and `--help` snapshot --- exec/btw.R | 33 ++++++++++++++++++++++++++---- inst/cli-skill/r-btw-cli/SKILL.md | 12 +++++------ tests/testthat/_snaps/cli.md | 23 ++++++++++++++++++++- tests/testthat/test-cli.R | 34 ++++++++++++++++++++++++------- 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index a1d85360..ded2e48c 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,23 @@ btw_docs_help <- function(topic, package) { } } +btw_docs_topics <- function(package, only) { + 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 (include_help) { + btw_output(btw_this(btw:::as_btw_docs_package(package))) + } + + if (include_vignettes) { + btw_output(btw_this(utils::vignette(package = package))) + } +} + btw_docs_vignette <- function(package, name, list) { if (list) { btw_output(btw_this(utils::vignette(package = package))) @@ -243,9 +257,20 @@ 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 <- "" + + tryCatch(btw_docs_topics(package, only), 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' 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..2dcb7107 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 @@ -132,3 +133,23 @@ For help with a specific command, run: `btw cran --help`. +# btw docs topics --help shows topics subcommand usage + + Code + run_btw("docs", "topics", "--help") + Output + List help topics and vignettes for a package + + Usage: btw docs topics [OPTIONS] + + Options: + -o, --only Limit output to "help" topics or "vignettes". + [default: ""] [type: string] + + Global options: + --version / --no-version Print btw version and exit. [default: false] + Enable with `--version`. + + Arguments: + Package name. + diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index e393a9c4..01f090f1 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,32 @@ 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 --help shows topics subcommand usage", { + expect_snapshot(run_btw("docs", "topics", "--help")) +}) + +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 +478,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 From 834b924c9f45f9ba9e6f4a6284159276899dab93 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 13 May 2026 17:05:38 -0400 Subject: [PATCH 02/10] Format `btw docs topics` output with headings and YAML-style rows - Add `## Help topics` and `## Vignettes` section headings - Replace JSON output with YAML-style key: value pairs, one entry per topic/vignette, blank line between entries - Rewrite `btw_docs_topics()` to call `_impl` functions directly and pull structured data from `extra$data` rather than consuming the JSON `@value` strings - Add `format_as_yaml_rows()` helper in exec/btw.R --- exec/btw.R | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index ded2e48c..ff21dd28 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -78,6 +78,16 @@ btw_docs_help <- function(topic, package) { } } +format_as_yaml_rows <- function(df) { + rows <- lapply(seq_len(nrow(df)), function(i) { + paste( + mapply(function(key, val) paste0(key, ": ", val), names(df), df[i, ]), + collapse = "\n" + ) + }) + paste(rows, collapse = "\n\n") +} + btw_docs_topics <- function(package, only) { if (!only %in% c("", "help", "vignettes")) { stop("--only must be \"help\" or \"vignettes\"", call. = FALSE) @@ -87,11 +97,18 @@ btw_docs_topics <- function(package, only) { include_vignettes <- only == "" || only == "vignettes" if (include_help) { - btw_output(btw_this(btw:::as_btw_docs_package(package))) + result <- btw:::btw_tool_docs_package_help_topics_impl(package) + df <- S7::prop(result, "extra")$data[, c("topic_id", "title")] + cat("## Help topics\n\n") + cat(format_as_yaml_rows(df), "\n") } if (include_vignettes) { - btw_output(btw_this(utils::vignette(package = package))) + result <- btw:::btw_tool_docs_available_vignettes_impl(package) + df <- S7::prop(result, "extra")$data + if (include_help) cat("\n") + cat("## Vignettes\n\n") + cat(format_as_yaml_rows(df), "\n") } } From 29fff6bb63e03681dc221b3376442e54e4e91ba7 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 13 May 2026 17:08:15 -0400 Subject: [PATCH 03/10] Use markdown list format in `btw docs topics` output Replace YAML-style key/value rows with compact markdown list items: * `topic_id` - Title Simpler, more readable, and consistent with how markdown lists are used elsewhere in btw output. Replaces format_as_yaml_rows() with a one-liner format_as_md_list() helper. --- exec/btw.R | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index ff21dd28..155ddfc3 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -78,14 +78,8 @@ btw_docs_help <- function(topic, package) { } } -format_as_yaml_rows <- function(df) { - rows <- lapply(seq_len(nrow(df)), function(i) { - paste( - mapply(function(key, val) paste0(key, ": ", val), names(df), df[i, ]), - collapse = "\n" - ) - }) - paste(rows, collapse = "\n\n") +format_as_md_list <- function(df) { + paste0("* `", df[[1]], "` - ", df[[2]], collapse = "\n") } btw_docs_topics <- function(package, only) { @@ -100,7 +94,7 @@ btw_docs_topics <- function(package, only) { result <- btw:::btw_tool_docs_package_help_topics_impl(package) df <- S7::prop(result, "extra")$data[, c("topic_id", "title")] cat("## Help topics\n\n") - cat(format_as_yaml_rows(df), "\n") + cat(format_as_md_list(df), "\n") } if (include_vignettes) { @@ -108,7 +102,7 @@ btw_docs_topics <- function(package, only) { df <- S7::prop(result, "extra")$data if (include_help) cat("\n") cat("## Vignettes\n\n") - cat(format_as_yaml_rows(df), "\n") + cat(format_as_md_list(df), "\n") } } From bc62bc495d2146def9fe50f440bc1a5bb68e5f12 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 13 May 2026 17:10:26 -0400 Subject: [PATCH 04/10] Include aliases in help topics listing, inline formatting logic - Show aliases parenthetically after the topic ID when they differ: * `mutate` *(also: mutate_all, mutate_at, mutate_if)* - Create, modify, and delete columns - Drop the format_as_md_list() helper; write the formatting logic inline in btw_docs_topics() where it's used - Use full df (with aliases column) for help topics instead of pre-subsetting to topic_id + title --- exec/btw.R | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index 155ddfc3..ab7bb89b 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -78,10 +78,6 @@ btw_docs_help <- function(topic, package) { } } -format_as_md_list <- function(df) { - paste0("* `", df[[1]], "` - ", df[[2]], collapse = "\n") -} - btw_docs_topics <- function(package, only) { if (!only %in% c("", "help", "vignettes")) { stop("--only must be \"help\" or \"vignettes\"", call. = FALSE) @@ -92,17 +88,34 @@ btw_docs_topics <- function(package, only) { if (include_help) { result <- btw:::btw_tool_docs_package_help_topics_impl(package) - df <- S7::prop(result, "extra")$data[, c("topic_id", "title")] + df <- S7::prop(result, "extra")$data + lines <- mapply( + function(topic_id, title, aliases) { + other <- aliases[aliases != topic_id] + also <- if (length(other) > 0) { + paste0(" *(also: ", paste(other, collapse = ", "), ")*") + } else { + "" + } + paste0("* `", topic_id, "`", also, " - ", title) + }, + df$topic_id, + df$title, + df$aliases + ) cat("## Help topics\n\n") - cat(format_as_md_list(df), "\n") + cat(paste(lines, collapse = "\n"), "\n") } if (include_vignettes) { result <- btw:::btw_tool_docs_available_vignettes_impl(package) df <- S7::prop(result, "extra")$data - if (include_help) cat("\n") + lines <- paste0("* `", df$vignette, "` - ", df$title) + if (include_help) { + cat("\n") + } cat("## Vignettes\n\n") - cat(format_as_md_list(df), "\n") + cat(paste(lines, collapse = "\n"), "\n") } } @@ -453,7 +466,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 33ff43d303517cf8d83d125c4f933178bcae9541 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 08:26:51 -0400 Subject: [PATCH 05/10] fix(docs): print Vignettes heading before fetching vignette list When a package has no vignettes, the abort from `btw_tool_docs_available_vignettes_impl()` was propagating before the `## Vignettes` heading was printed, causing the error message to appear without its heading. Now the heading is printed first and the error is caught locally so it renders as body text beneath it. --- exec/btw.R | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index ab7bb89b..672170f3 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -108,14 +108,22 @@ btw_docs_topics <- function(package, only) { } if (include_vignettes) { - result <- btw:::btw_tool_docs_available_vignettes_impl(package) - df <- S7::prop(result, "extra")$data - lines <- paste0("* `", df$vignette, "` - ", df$title) if (include_help) { cat("\n") } cat("## Vignettes\n\n") - cat(paste(lines, collapse = "\n"), "\n") + tryCatch( + { + result <- btw:::btw_tool_docs_available_vignettes_impl(package) + df <- S7::prop(result, "extra")$data + lines <- paste0("* `", df$vignette, "` - ", df$title) + cat(paste(lines, collapse = "\n"), "\n") + }, + error = function(e) { + msg <- cli::ansi_strip(conditionMessage(e)) + cat(msg, "\n") + } + ) } } From bddf635aac8e7a18d3819285a26d8657160f67b9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 08:28:39 -0400 Subject: [PATCH 06/10] chore: drop aliases --- exec/btw.R | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index 672170f3..0475e740 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -91,13 +91,7 @@ btw_docs_topics <- function(package, only) { df <- S7::prop(result, "extra")$data lines <- mapply( function(topic_id, title, aliases) { - other <- aliases[aliases != topic_id] - also <- if (length(other) > 0) { - paste0(" *(also: ", paste(other, collapse = ", "), ")*") - } else { - "" - } - paste0("* `", topic_id, "`", also, " - ", title) + sprintf("* `%s` - %s", topic_id, title) }, df$topic_id, df$title, @@ -116,7 +110,7 @@ btw_docs_topics <- function(package, only) { { result <- btw:::btw_tool_docs_available_vignettes_impl(package) df <- S7::prop(result, "extra")$data - lines <- paste0("* `", df$vignette, "` - ", df$title) + lines <- sprintf("* `%s` - %s", df$vignette, df$title) cat(paste(lines, collapse = "\n"), "\n") }, error = function(e) { From 0856b0bf985cd74118339482b3b6e2816244a997 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 08:36:08 -0400 Subject: [PATCH 07/10] refactor --- exec/btw.R | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index 0475e740..1c024454 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -97,8 +97,15 @@ btw_docs_topics <- function(package, only) { df$title, df$aliases ) - cat("## Help topics\n\n") - cat(paste(lines, collapse = "\n"), "\n") + cat( + "## Help topics\n\n", + lines, + sprintf( + "\nUse `btw docs help %s::` to read a help page.\n", + package + ), + sep = "\n" + ) } if (include_vignettes) { @@ -111,7 +118,14 @@ btw_docs_topics <- function(package, only) { result <- btw:::btw_tool_docs_available_vignettes_impl(package) df <- S7::prop(result, "extra")$data lines <- sprintf("* `%s` - %s", df$vignette, df$title) - cat(paste(lines, collapse = "\n"), "\n") + 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)) From e90cca7ce4093fe5bdd00de5c289c9d92b071cc5 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 08:46:23 -0400 Subject: [PATCH 08/10] feat(docs): add --json flag to btw docs topics Outputs structured JSON with top-level keys "help" (array of {topic_id, title, aliases[]}) and "vignettes" (array of {vignette, title}), compatible with --only to limit which sections are included. --- exec/btw.R | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/exec/btw.R b/exec/btw.R index 1c024454..87a03aa4 100755 --- a/exec/btw.R +++ b/exec/btw.R @@ -78,7 +78,7 @@ btw_docs_help <- function(topic, package) { } } -btw_docs_topics <- function(package, only) { +btw_docs_topics <- function(package, only, json = FALSE) { if (!only %in% c("", "help", "vignettes")) { stop("--only must be \"help\" or \"vignettes\"", call. = FALSE) } @@ -86,6 +86,34 @@ btw_docs_topics <- function(package, only) { 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 @@ -304,8 +332,10 @@ switch( #| 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), error = btw_error) + tryCatch(btw_docs_topics(package, only, json), error = btw_error) }, #| title: Show help for a topic or package From f152718862abd60195128fbf525e1c522529074e Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 08:49:33 -0400 Subject: [PATCH 09/10] test(cli): remove snapshot for btw docs topics --help --- tests/testthat/_snaps/cli.md | 20 -------------------- tests/testthat/test-cli.R | 4 ---- 2 files changed, 24 deletions(-) diff --git a/tests/testthat/_snaps/cli.md b/tests/testthat/_snaps/cli.md index 2dcb7107..11831955 100644 --- a/tests/testthat/_snaps/cli.md +++ b/tests/testthat/_snaps/cli.md @@ -133,23 +133,3 @@ For help with a specific command, run: `btw cran --help`. -# btw docs topics --help shows topics subcommand usage - - Code - run_btw("docs", "topics", "--help") - Output - List help topics and vignettes for a package - - Usage: btw docs topics [OPTIONS] - - Options: - -o, --only Limit output to "help" topics or "vignettes". - [default: ""] [type: string] - - Global options: - --version / --no-version Print btw version and exit. [default: false] - Enable with `--version`. - - Arguments: - Package name. - diff --git a/tests/testthat/test-cli.R b/tests/testthat/test-cli.R index 01f090f1..f1e1930b 100644 --- a/tests/testthat/test-cli.R +++ b/tests/testthat/test-cli.R @@ -123,10 +123,6 @@ test_that("btw docs help errors for unknown topic", { # docs topics ---------------------------------------------------------- -test_that("btw docs topics --help shows topics subcommand usage", { - expect_snapshot(run_btw("docs", "topics", "--help")) -}) - test_that("btw docs topics shows topics and vignettes", { env <- run_btw_quietly("docs", "topics", "stats") expect_equal(env$package, "stats") From 1ea335ece7e5b2d4c177e5dc78509e049967cf49 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 14 May 2026 10:06:51 -0400 Subject: [PATCH 10/10] chore: add NEWS.md entry for btw docs topics (#195) --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) 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