From d7bcf9d640bb4c19ccab6902a873751127f50df8 Mon Sep 17 00:00:00 2001 From: spindouken Date: Tue, 28 Apr 2026 13:48:54 -0500 Subject: [PATCH 1/6] feat: rewrite ojo_use_template to use new okpolicy-report-template --- R/template.R | 240 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 187 insertions(+), 53 deletions(-) diff --git a/R/template.R b/R/template.R index 7d2d3a9..1f3dd5b 100644 --- a/R/template.R +++ b/R/template.R @@ -1,74 +1,208 @@ -#' @title Function to use the OJO quarto HTML template +#' @title Ask for Yes/No Confirmation #' -#' @description Wrapper for `quarto use template` command. Defaults to OJO html template. +#' @description Interactive yes/no confirmation with cli styling. +#' Returns TRUE for yes, FALSE for no. Follows the pattern from cli issue #488. #' -#' @param path The name of the directory where the template should be added. This should be a string representing an absolute path, or a path relative to the current working directory. -#' @param template The name of the Quarto template to use. This should be a string in the format "username/repository". Default is "openjusticeok/ojo-report-template". -#' @return This function does not return a value. If the user chooses not to proceed with the operation, the function exits silently. +#' @param text The question text (supports cli styling) +#' @param yes Character vector of affirmative options (default: c("Yes", "yeah")) +#' @param no Character vector of negative options (default: c("No", "nope")) +#' @param n_yes Number of yes options to show (default: 1) +#' @param n_no Number of no options to show (default: 1) +#' @param shuffle Whether to shuffle the options (default: TRUE) +#' @param ... Additional arguments passed to cli::cli_alert() +#' @param .envir Environment for glue interpolation #' -#' @export +#' @return Logical TRUE for yes, FALSE for no +#' +#' @keywords internal #' #' @examples #' \dontrun{ -#' ojo_use_template(template = "openjusticeok/ojo-report-template", path = "my_directory") +#' # Simple confirmation +#' if (cli_yeah("Delete all files?")) { +#' # Proceed with deletion #' } -ojo_use_template <- function( - path, - template = "openjusticeok/ojo-report-template" +#' +#' # Custom options +#' proceed <- cli_yeah( +#' "Continue with migration?", +#' yes = c("Yes, proceed", "Continue"), +#' no = c("Cancel", "Stop") +#' ) +#' } +cli_yeah <- function( + text, + yes = c("Yes", "yeah"), + no = c("No", "nope"), + n_yes = 1, + n_no = 1, + shuffle = TRUE, + ..., + .envir = parent.frame() ) { - command <- paste("quarto use template", template, "--no-prompt") + if (!rlang::is_interactive()) { + cli::cli_abort( + c( + "User input required, but session is not interactive.", + i = "Query: {text}" + ), + .envir = .envir + ) + } - full_dir <- dplyr::if_else( - fs::is_absolute_path(path), - path, - fs::path_wd(path) - ) + n_yes <- min(n_yes, length(yes)) + n_no <- min(n_no, length(no)) - # Check if the directory exists - if (!dir.exists(full_dir)) { - # If not, ask if we should create it - ans <- utils::menu( - choices = c("Yes", "No"), - title = paste0( - "The path `", - full_dir, - "` does not exist.", - "\nDo you want to create it?" - ) - ) - # If they say no, just stop - if (ans == 2) { - return(invisible()) - } - # Otherwise, go ahead - fs::dir_create(full_dir) + # Sample options + qs <- c(sample(yes, n_yes), sample(no, n_no)) + + if (shuffle) { + qs <- sample(qs) } - # Check that the directory is empty - if (!dir_empty(full_dir)) { + # Show the question + cli::cli_alert(text, ..., .envir = .envir) + + # Present menu and get choice + choice <- utils::menu(qs, title = "Choose an option:") + + # Return TRUE if yes option selected + choice != 0L && qs[[choice]] %in% yes +} + +#' @title Function to use the OKPolicy quarto website template +#' @description Wrapper for `quarto use template` command. +#' @param base_dir The base directory where the new project folder will be created. Will default to current dir in terminal. +#' @param project_name The name of the new project folder to create. This will create a sub-directory for the new project. +#' @param template The name of the Quarto template to use. Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template". +#' @param .interactive set if environment is interactive or not. Default is TRUE (terminal is interactive). +#' +#' @examples +#' \dontrun{ +#' # Create a new project in the current directory +#' ojo_use_template(project_name = "my_new_project") +#' +#' # Create a project in a specific base directory +#' ojo_use_template( +#' base_dir = "C:/Users/Documents/Reports", +#' project_name = "my_new_project" +#' ) +#' +#' # Create a project in a specific base directory with interactive turned off +#' ojo_use_template( +#' base_dir = "C:/Users/Documents/Reports", +#' project_name = "my_new_project", +#' .interactive = FALSE +#' ) +#' +#' # Can even specify another template +#' ojo_use_template( +#' base_dir = "C:/Users/Documents/Reports", +#' project_name = "my_new_project", +#' template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template", +#' .interactive = FALSE +#' ) +#' } +#' @export +ojo_use_template <- function( + base_dir = ".", + project_name, + template = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", + .interactive = rlang::is_interactive() +) { + quarto_args <- c("use", "template", template, "--no-prompt") + + # Path Resolution + project_dir <- fs::path_abs(fs::path(base_dir, project_name)) + + # Check if directory already exists + # if it exists installation will abort with error message posted to CLI + if (fs::dir_exists(project_dir)) { cli::cli_abort( - paste0( - "Provided directory `", full_dir, "` is not empty!\n", - "Please specify an empty directory (or enter the name of one to create) for the report to live in!" + c( + "x" = "Directory {.path {project_dir}} already exists!", + "i" = "Please choose a different {.arg project_name} or remove the existing directory." ) ) } - # Confirm that directory is correct - if (utils::menu(choices = c("Yes", "No"), title = paste0( - "The template `", - template, - "` will be added to the directory `", - full_dir, - "`.\nDo you want to proceed?" - )) == 2) { - return(invisible()) + # Check if quarto is installed + if (!requireNamespace("quarto", quietly = TRUE)) { + cli::cli_abort("The {.pkg quarto} package is required to find the Quarto installation.") } - # Run command - withr::with_dir(new = full_dir, code = system(command)) + # Find the executable + quarto_bin <- quarto::quarto_path(normalize = TRUE) + + # Graceful fail if Quarto is not installed + if (is.null(quarto_bin) || !nzchar(quarto_bin)) { + cli::cli_abort("Quarto is not installed or could not be found.") + } + + # Confirm user wants to proceed + if (.interactive) { + if (!cli_yeah("Create directory {.path {project_dir}} and install the template?")) { + cli::cli_alert_info("Aborted by user.") + return(invisible()) + } + } + + # Create the target directory + cli::cli_alert_info("Creating directory {.path {project_dir}}...") + fs::dir_create(project_dir) + success <- FALSE + on.exit(if (!success) try(fs::dir_delete(project_dir), silent = TRUE), add = TRUE) + + cli::cli_alert_info("Running {.code quarto use template {template}}...") + + # Error/Status/StdErr/StdOut capturing + result <- tryCatch( + withr::with_dir( + new = project_dir, + code = { + processx::run( + command = quarto_bin, + args = quarto_args, + echo = .interactive, # print the stdout of the command + echo_cmd = .interactive, # print the command that will be run + error_on_status = TRUE, # if the command fails, throw an R error, which is then caught by the tryCatch block below + spinner = .interactive # show a spinner while the command is running + ) + } + ), + # c() is concatenating the following CLI styling together for cli::abort message + # x = red + # i = blue + # " " = invisible indent for clean continuation lines + # v = green + # ! = yellow warning + system_command_status_error = function(error) { + cli::cli_abort( + c( + "x" = "Quarto encountered an error. Template installation failed.", + # if quarto wrote a message to stderr write it to CLI, if not write "(no stderr)" + " " = if (nzchar(error$stderr)) error$stderr else "(no stderr)", + "i" = "Exit status: {error$status}" + ), + parent = error + ) + }, + # captures other errors that happen before or during quarto launch + error = function(error) { + cli::cli_abort( + c( + "!" = "Something went wrong while preparing the Quarto command.", + "i" = conditionMessage(error) + ), + parent = error + ) + } + ) + + success <- TRUE + cli::cli_alert_success( + "Template {.val {template}} successfully added to directory {.path {project_dir}}!" + ) - cli::cli_alert_success(paste0( - "Template `", template, "` successfully added to directory `", full_dir, "`!" - )) + invisible(result) } From 1af64d0e4d805af85e5e8cab4fb3caa7535d4f3f Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Wed, 29 Apr 2026 13:06:30 -0500 Subject: [PATCH 2/6] refactor: simplify template api to single path parameter with validation - change from base_dir/project_name to single path parameter - add kebab-case validation for project names - create directory automatically if missing - abort if directory exists and is non-empty - extract cli_yeah() to cli.R for reuse --- DESCRIPTION | 2 + R/cli.R | 71 ++++++++++++++++ R/template.R | 181 +++++++++++++--------------------------- man/cli_yeah.Rd | 57 +++++++++++++ man/ojo_use_template.Rd | 40 +++++++-- 5 files changed, 221 insertions(+), 130 deletions(-) create mode 100644 R/cli.R create mode 100644 man/cli_yeah.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 5d90fa1..293cf50 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,6 +23,8 @@ Imports: janitor, lubridate, nanoarrow, + processx, + quarto, readr, renv, rlang, diff --git a/R/cli.R b/R/cli.R new file mode 100644 index 0000000..5df4e55 --- /dev/null +++ b/R/cli.R @@ -0,0 +1,71 @@ +#' @title Ask for Yes/No Confirmation +#' +#' @description Interactive yes/no confirmation with cli styling. +#' Returns TRUE for yes, FALSE for no. Follows the pattern from cli issue #488. +#' +#' @param text The question text (supports cli styling) +#' @param yes Character vector of affirmative options (default: c("Yes", "yeah")) +#' @param no Character vector of negative options (default: c("No", "nope")) +#' @param n_yes Number of yes options to show (default: 1) +#' @param n_no Number of no options to show (default: 1) +#' @param shuffle Whether to shuffle the options (default: TRUE) +#' @param ... Additional arguments passed to cli::cli_alert() +#' @param .envir Environment for glue interpolation +#' +#' @return Logical TRUE for yes, FALSE for no +#' +#' @keywords internal +#' +#' @examples +#' \dontrun{ +#' # Simple confirmation +#' if (cli_yeah("Delete all files?")) { +#' # Proceed with deletion +#' } +#' +#' # Custom options +#' proceed <- cli_yeah( +#' "Continue with migration?", +#' yes = c("Yes, proceed", "Continue"), +#' no = c("Cancel", "Stop") +#' ) +#' } +cli_yeah <- function( + text, + yes = c("Yes", "yeah"), + no = c("No", "nope"), + n_yes = 1, + n_no = 1, + shuffle = TRUE, + ..., + .envir = parent.frame() +) { + if (!rlang::is_interactive()) { + cli::cli_abort( + c( + "User input required, but session is not interactive.", + i = "Query: {text}" + ), + .envir = .envir + ) + } + + n_yes <- min(n_yes, length(yes)) + n_no <- min(n_no, length(no)) + + # Sample options + qs <- c(sample(yes, n_yes), sample(no, n_no)) + + if (shuffle) { + qs <- sample(qs) + } + + # Show the question + cli::cli_alert(text, ..., .envir = .envir) + + # Present menu and get choice + choice <- utils::menu(qs, title = "Choose an option:") + + # Return TRUE if yes option selected + choice != 0L && qs[[choice]] %in% yes +} diff --git a/R/template.R b/R/template.R index 1f3dd5b..0b85699 100644 --- a/R/template.R +++ b/R/template.R @@ -1,135 +1,54 @@ -#' @title Ask for Yes/No Confirmation -#' -#' @description Interactive yes/no confirmation with cli styling. -#' Returns TRUE for yes, FALSE for no. Follows the pattern from cli issue #488. -#' -#' @param text The question text (supports cli styling) -#' @param yes Character vector of affirmative options (default: c("Yes", "yeah")) -#' @param no Character vector of negative options (default: c("No", "nope")) -#' @param n_yes Number of yes options to show (default: 1) -#' @param n_no Number of no options to show (default: 1) -#' @param shuffle Whether to shuffle the options (default: TRUE) -#' @param ... Additional arguments passed to cli::cli_alert() -#' @param .envir Environment for glue interpolation -#' -#' @return Logical TRUE for yes, FALSE for no -#' -#' @keywords internal -#' -#' @examples -#' \dontrun{ -#' # Simple confirmation -#' if (cli_yeah("Delete all files?")) { -#' # Proceed with deletion -#' } -#' -#' # Custom options -#' proceed <- cli_yeah( -#' "Continue with migration?", -#' yes = c("Yes, proceed", "Continue"), -#' no = c("Cancel", "Stop") -#' ) -#' } -cli_yeah <- function( - text, - yes = c("Yes", "yeah"), - no = c("No", "nope"), - n_yes = 1, - n_no = 1, - shuffle = TRUE, - ..., - .envir = parent.frame() -) { - if (!rlang::is_interactive()) { - cli::cli_abort( - c( - "User input required, but session is not interactive.", - i = "Query: {text}" - ), - .envir = .envir - ) - } - - n_yes <- min(n_yes, length(yes)) - n_no <- min(n_no, length(no)) - - # Sample options - qs <- c(sample(yes, n_yes), sample(no, n_no)) - - if (shuffle) { - qs <- sample(qs) - } - - # Show the question - cli::cli_alert(text, ..., .envir = .envir) - - # Present menu and get choice - choice <- utils::menu(qs, title = "Choose an option:") - - # Return TRUE if yes option selected - choice != 0L && qs[[choice]] %in% yes -} - #' @title Function to use the OKPolicy quarto website template -#' @description Wrapper for `quarto use template` command. -#' @param base_dir The base directory where the new project folder will be created. Will default to current dir in terminal. -#' @param project_name The name of the new project folder to create. This will create a sub-directory for the new project. -#' @param template The name of the Quarto template to use. Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template". -#' @param .interactive set if environment is interactive or not. Default is TRUE (terminal is interactive). +#' @description Wrapper for `quarto use template` command. Creates a new project directory +#' and installs a Quarto template. The project name is derived from the last component +#' of the path and must be in kebab-case format. +#' @param path Character. The path to the project directory. Can be absolute or relative +#' to the current working directory. The last component of the path will be used as the +#' project name and must be in kebab-case (lowercase letters, numbers, and hyphens only). +#' @param template Character. The name of the Quarto template to use. +#' Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev". +#' @param .interactive Logical. Whether to prompt for user confirmation in interactive mode. +#' Defaults to `rlang::is_interactive()`. +#' +#' @return Invisibly returns the result of the quarto command execution. #' #' @examples #' \dontrun{ #' # Create a new project in the current directory -#' ojo_use_template(project_name = "my_new_project") +#' ojo_use_template("my-new-project") #' -#' # Create a project in a specific base directory -#' ojo_use_template( -#' base_dir = "C:/Users/Documents/Reports", -#' project_name = "my_new_project" -#' ) -#' -#' # Create a project in a specific base directory with interactive turned off -#' ojo_use_template( -#' base_dir = "C:/Users/Documents/Reports", -#' project_name = "my_new_project", -#' .interactive = FALSE -#' ) +#' # Create a project in a specific directory +#' ojo_use_template("~/Documents/Reports/my-new-project") +#' +#' # Create a project with interactive turned off +#' ojo_use_template("my-new-project", .interactive = FALSE) #' -#' # Can even specify another template +#' # Use a different template #' ojo_use_template( -#' base_dir = "C:/Users/Documents/Reports", -#' project_name = "my_new_project", -#' template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template", -#' .interactive = FALSE +#' "my-new-project", +#' template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" #' ) #' } #' @export ojo_use_template <- function( - base_dir = ".", - project_name, + path, template = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", .interactive = rlang::is_interactive() ) { - quarto_args <- c("use", "template", template, "--no-prompt") - # Path Resolution - project_dir <- fs::path_abs(fs::path(base_dir, project_name)) - - # Check if directory already exists - # if it exists installation will abort with error message posted to CLI - if (fs::dir_exists(project_dir)) { - cli::cli_abort( - c( - "x" = "Directory {.path {project_dir}} already exists!", - "i" = "Please choose a different {.arg project_name} or remove the existing directory." - ) - ) + project_dir <- fs::path_abs(path) + project_name <- fs::path_file(path) + + # Validate report name + if (!stringr::str_detect(project_name, "^[a-z0-9]+(-[a-z0-9]+)*$")) { + cli::cli_abort(c( + "x" = "{.arg path} must end with a valid kebab-case project name.", + "i" = "Allowed: lowercase letters, numbers, hyphens", + "i" = "Examples: {.code my-project}, {.code report-2024}, {.code data-analysis}" + )) } - # Check if quarto is installed - if (!requireNamespace("quarto", quietly = TRUE)) { - cli::cli_abort("The {.pkg quarto} package is required to find the Quarto installation.") - } + quarto_args <- c("use", "template", template, "--no-prompt") # Find the executable quarto_bin <- quarto::quarto_path(normalize = TRUE) @@ -138,21 +57,37 @@ ojo_use_template <- function( if (is.null(quarto_bin) || !nzchar(quarto_bin)) { cli::cli_abort("Quarto is not installed or could not be found.") } + # Directory handling logic + success <- FALSE + + if (!fs::dir_exists(project_dir)) { + # Directory doesn't exist, create automatically + cli::cli_alert_info("Creating directory {.path {project_dir}}...") + fs::dir_create(project_dir) + + # Set up cleanup on failure + on.exit(if (!success) try(fs::dir_delete(project_dir), silent = TRUE), add = TRUE) + } else { + # Directory exists, check if empty + if (!dir_empty(project_dir)) { + # Directory not empty, abort + cli::cli_abort(c( + "x" = "Directory {.path {project_dir}} exists and is not empty.", + "i" = "Remove the directory first or choose a different path." + )) + } + # Directory exists but empty, proceed without asking + } - # Confirm user wants to proceed - if (.interactive) { - if (!cli_yeah("Create directory {.path {project_dir}} and install the template?")) { + # Confirm user wants to proceed (only for new directories in interactive mode) + if (.interactive && !fs::dir_exists(project_dir)) { + if (!cli_yeah("Install the template to {.path {project_dir}}?")) { cli::cli_alert_info("Aborted by user.") + fs::dir_delete(project_dir) return(invisible()) } } - # Create the target directory - cli::cli_alert_info("Creating directory {.path {project_dir}}...") - fs::dir_create(project_dir) - success <- FALSE - on.exit(if (!success) try(fs::dir_delete(project_dir), silent = TRUE), add = TRUE) - cli::cli_alert_info("Running {.code quarto use template {template}}...") # Error/Status/StdErr/StdOut capturing diff --git a/man/cli_yeah.Rd b/man/cli_yeah.Rd new file mode 100644 index 0000000..f841782 --- /dev/null +++ b/man/cli_yeah.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/cli.R +\name{cli_yeah} +\alias{cli_yeah} +\title{Ask for Yes/No Confirmation} +\usage{ +cli_yeah( + text, + yes = c("Yes", "yeah"), + no = c("No", "nope"), + n_yes = 1, + n_no = 1, + shuffle = TRUE, + ..., + .envir = parent.frame() +) +} +\arguments{ +\item{text}{The question text (supports cli styling)} + +\item{yes}{Character vector of affirmative options (default: c("Yes", "yeah"))} + +\item{no}{Character vector of negative options (default: c("No", "nope"))} + +\item{n_yes}{Number of yes options to show (default: 1)} + +\item{n_no}{Number of no options to show (default: 1)} + +\item{shuffle}{Whether to shuffle the options (default: TRUE)} + +\item{...}{Additional arguments passed to cli::cli_alert()} + +\item{.envir}{Environment for glue interpolation} +} +\value{ +Logical TRUE for yes, FALSE for no +} +\description{ +Interactive yes/no confirmation with cli styling. +Returns TRUE for yes, FALSE for no. Follows the pattern from cli issue #488. +} +\examples{ +\dontrun{ +# Simple confirmation +if (cli_yeah("Delete all files?")) { + # Proceed with deletion +} + +# Custom options +proceed <- cli_yeah( + "Continue with migration?", + yes = c("Yes, proceed", "Continue"), + no = c("Cancel", "Stop") +) +} +} +\keyword{internal} diff --git a/man/ojo_use_template.Rd b/man/ojo_use_template.Rd index c8b4742..d0974b6 100644 --- a/man/ojo_use_template.Rd +++ b/man/ojo_use_template.Rd @@ -2,23 +2,49 @@ % Please edit documentation in R/template.R \name{ojo_use_template} \alias{ojo_use_template} -\title{Function to use the OJO quarto HTML template} +\title{Function to use the OKPolicy quarto website template} \usage{ -ojo_use_template(path, template = "openjusticeok/ojo-report-template") +ojo_use_template( + path, + template = + "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", + .interactive = rlang::is_interactive() +) } \arguments{ -\item{path}{The name of the directory where the template should be added. This should be a string representing an absolute path, or a path relative to the current working directory.} +\item{path}{Character. The path to the project directory. Can be absolute or relative +to the current working directory. The last component of the path will be used as the +project name and must be in kebab-case (lowercase letters, numbers, and hyphens only).} -\item{template}{The name of the Quarto template to use. This should be a string in the format "username/repository". Default is "openjusticeok/ojo-report-template".} +\item{template}{Character. The name of the Quarto template to use. +Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev".} + +\item{.interactive}{Logical. Whether to prompt for user confirmation in interactive mode. +Defaults to \code{rlang::is_interactive()}.} } \value{ -This function does not return a value. If the user chooses not to proceed with the operation, the function exits silently. +Invisibly returns the result of the quarto command execution. } \description{ -Wrapper for \verb{quarto use template} command. Defaults to OJO html template. +Wrapper for \verb{quarto use template} command. Creates a new project directory +and installs a Quarto template. The project name is derived from the last component +of the path and must be in kebab-case format. } \examples{ \dontrun{ -ojo_use_template(template = "openjusticeok/ojo-report-template", path = "my_directory") +# Create a new project in the current directory +ojo_use_template("my-new-project") + +# Create a project in a specific directory +ojo_use_template("~/Documents/Reports/my-new-project") + +# Create a project with interactive turned off +ojo_use_template("my-new-project", .interactive = FALSE) + +# Use a different template +ojo_use_template( + "my-new-project", + template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" +) } } From 55ea599d46740efa641379e1b7eaa7456887724e Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Wed, 29 Apr 2026 13:37:36 -0500 Subject: [PATCH 3/6] refactor: use website or report template options - validate template with rlang::arg_match() - map friendly names to github specs internally --- R/template.R | 41 ++++++++++++++++++++++++----------------- man/ojo_use_template.Rd | 27 +++++++++++++-------------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/R/template.R b/R/template.R index 0b85699..f5d434a 100644 --- a/R/template.R +++ b/R/template.R @@ -5,8 +5,9 @@ #' @param path Character. The path to the project directory. Can be absolute or relative #' to the current working directory. The last component of the path will be used as the #' project name and must be in kebab-case (lowercase letters, numbers, and hyphens only). -#' @param template Character. The name of the Quarto template to use. -#' Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev". +#' @param template Character. The type of project template to use. Must be one of: +#' * `"website"` (default) - Multi-page website template +#' * `"report"` - Single-page report template #' @param .interactive Logical. Whether to prompt for user confirmation in interactive mode. #' Defaults to `rlang::is_interactive()`. #' @@ -14,27 +15,33 @@ #' #' @examples #' \dontrun{ -#' # Create a new project in the current directory -#' ojo_use_template("my-new-project") +#' # Create a new website project (default) +#' ojo_use_template("my-new-website") +#' +#' # Create a report project +#' ojo_use_template("my-new-report", template = "report") #' #' # Create a project in a specific directory -#' ojo_use_template("~/Documents/Reports/my-new-project") +#' ojo_use_template("~/Documents/Reports/my-new-website") #' #' # Create a project with interactive turned off -#' ojo_use_template("my-new-project", .interactive = FALSE) -#' -#' # Use a different template -#' ojo_use_template( -#' "my-new-project", -#' template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" -#' ) +#' ojo_use_template("my-new-website", .interactive = FALSE) #' } #' @export ojo_use_template <- function( path, - template = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", + template = c("website", "report"), .interactive = rlang::is_interactive() ) { + # Validate template argument + template <- rlang::arg_match(template) + + # Map friendly names to GitHub template specs + template_spec <- switch(template, + website = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", + report = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" + ) + # Path Resolution project_dir <- fs::path_abs(path) project_name <- fs::path_file(path) @@ -48,7 +55,7 @@ ojo_use_template <- function( )) } - quarto_args <- c("use", "template", template, "--no-prompt") + quarto_args <- c("use", "template", template_spec, "--no-prompt") # Find the executable quarto_bin <- quarto::quarto_path(normalize = TRUE) @@ -88,8 +95,8 @@ ojo_use_template <- function( } } - cli::cli_alert_info("Running {.code quarto use template {template}}...") - + cli::cli_alert_info("Installing {.field {template}} template to {.path {project_dir}}...") + # Error/Status/StdErr/StdOut capturing result <- tryCatch( withr::with_dir( @@ -136,7 +143,7 @@ ojo_use_template <- function( success <- TRUE cli::cli_alert_success( - "Template {.val {template}} successfully added to directory {.path {project_dir}}!" + "{.field {template}} template successfully added to directory {.path {project_dir}}!" ) invisible(result) diff --git a/man/ojo_use_template.Rd b/man/ojo_use_template.Rd index d0974b6..b387fe0 100644 --- a/man/ojo_use_template.Rd +++ b/man/ojo_use_template.Rd @@ -6,8 +6,7 @@ \usage{ ojo_use_template( path, - template = - "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", + template = c("website", "report"), .interactive = rlang::is_interactive() ) } @@ -16,8 +15,11 @@ ojo_use_template( to the current working directory. The last component of the path will be used as the project name and must be in kebab-case (lowercase letters, numbers, and hyphens only).} -\item{template}{Character. The name of the Quarto template to use. -Default is "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev".} +\item{template}{Character. The type of project template to use. Must be one of: +\itemize{ +\item \code{"website"} (default) - Multi-page website template +\item \code{"report"} - Single-page report template +}} \item{.interactive}{Logical. Whether to prompt for user confirmation in interactive mode. Defaults to \code{rlang::is_interactive()}.} @@ -32,19 +34,16 @@ of the path and must be in kebab-case format. } \examples{ \dontrun{ -# Create a new project in the current directory -ojo_use_template("my-new-project") +# Create a new website project (default) +ojo_use_template("my-new-website") + +# Create a report project +ojo_use_template("my-new-report", template = "report") # Create a project in a specific directory -ojo_use_template("~/Documents/Reports/my-new-project") +ojo_use_template("~/Documents/Reports/my-new-website") # Create a project with interactive turned off -ojo_use_template("my-new-project", .interactive = FALSE) - -# Use a different template -ojo_use_template( - "my-new-project", - template = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" -) +ojo_use_template("my-new-website", .interactive = FALSE) } } From a9c8ae8ea16edc6aa68b9c721ee69554adb79426 Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Wed, 29 Apr 2026 13:52:28 -0500 Subject: [PATCH 4/6] refactor: break out template project name validation to helper --- R/template.R | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/R/template.R b/R/template.R index f5d434a..f62825a 100644 --- a/R/template.R +++ b/R/template.R @@ -1,3 +1,7 @@ +validate_project_name <- function(name) { + stringr::str_detect(name, "^[a-z0-9]+(-[a-z0-9]+)*$") +} + #' @title Function to use the OKPolicy quarto website template #' @description Wrapper for `quarto use template` command. Creates a new project directory #' and installs a Quarto template. The project name is derived from the last component @@ -46,8 +50,8 @@ ojo_use_template <- function( project_dir <- fs::path_abs(path) project_name <- fs::path_file(path) - # Validate report name - if (!stringr::str_detect(project_name, "^[a-z0-9]+(-[a-z0-9]+)*$")) { + # Validate project name + if (!validate_project_name(project_name)) { cli::cli_abort(c( "x" = "{.arg path} must end with a valid kebab-case project name.", "i" = "Allowed: lowercase letters, numbers, hyphens", From 43fa50ed6cceac14b9c6e0da235e62606c3350b2 Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Wed, 29 Apr 2026 13:59:36 -0500 Subject: [PATCH 5/6] feat: full interactive mode for ojo_use_template --- R/template.R | 64 ++++++++++++++++++++++++++++++++++++++++- man/ojo_use_template.Rd | 8 ++++-- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/R/template.R b/R/template.R index f62825a..7a0bf1a 100644 --- a/R/template.R +++ b/R/template.R @@ -2,6 +2,45 @@ validate_project_name <- function(name) { stringr::str_detect(name, "^[a-z0-9]+(-[a-z0-9]+)*$") } +prompt_project_name <- function() { + project_name <- readline(cli::format_inline("Project name (kebab-case): ")) + while (!validate_project_name(project_name)) { + cli::cli_alert_danger("Invalid name. Use lowercase, numbers, hyphens only.") + project_name <- readline(cli::format_inline("Project name (kebab-case): ")) + } + project_name +} + +prompt_parent_dir <- function(default = fs::path_wd()) { + prompt_text <- cli::format_inline("Parent directory [{.path {default}}]: ") + parent_dir <- readline(prompt_text) + if (parent_dir == "") { + parent_dir <- default + } + parent_dir <- fs::path_abs(parent_dir) + if (!fs::dir_exists(parent_dir)) { + cli::cli_alert_info("Creating parent directory {.path {parent_dir}}...") + fs::dir_create(parent_dir, recurse = TRUE) + } + parent_dir +} + +prompt_template <- function(default = "website") { + choices <- c( + "website" = "website (multi-page site)", + "report" = "report (single-page document)" + ) + # Pre-select default by putting it first + if (default == "report") { + choices <- choices[c("report", "website")] + } + choice <- utils::menu(choices, title = cli::format_inline("Select template type:")) + if (choice == 0L) { + return(invisible()) + } + names(choices)[choice] +} + #' @title Function to use the OKPolicy quarto website template #' @description Wrapper for `quarto use template` command. Creates a new project directory #' and installs a Quarto template. The project name is derived from the last component @@ -9,6 +48,7 @@ validate_project_name <- function(name) { #' @param path Character. The path to the project directory. Can be absolute or relative #' to the current working directory. The last component of the path will be used as the #' project name and must be in kebab-case (lowercase letters, numbers, and hyphens only). +#' If `NULL` (default) and in interactive mode, prompts for project name and location. #' @param template Character. The type of project template to use. Must be one of: #' * `"website"` (default) - Multi-page website template #' * `"report"` - Single-page report template @@ -19,6 +59,9 @@ validate_project_name <- function(name) { #' #' @examples #' \dontrun{ +#' # Interactive mode (prompts for project name and location) +#' ojo_use_template() +#' #' # Create a new website project (default) #' ojo_use_template("my-new-website") #' @@ -33,10 +76,29 @@ validate_project_name <- function(name) { #' } #' @export ojo_use_template <- function( - path, + path = NULL, template = c("website", "report"), .interactive = rlang::is_interactive() ) { + # If path is NULL and interactive, enter full interactive mode + if (is.null(path)) { + if (!.interactive) { + cli::cli_abort("{.arg path} is required in non-interactive mode.") + } + + project_name <- prompt_project_name() + parent_dir <- prompt_parent_dir() + selected_template <- prompt_template() + + # Handle cancellation from template prompt + if (is.null(selected_template)) { + return(invisible()) + } + + template <- selected_template + path <- fs::path(parent_dir, project_name) + } + # Validate template argument template <- rlang::arg_match(template) diff --git a/man/ojo_use_template.Rd b/man/ojo_use_template.Rd index b387fe0..c14d936 100644 --- a/man/ojo_use_template.Rd +++ b/man/ojo_use_template.Rd @@ -5,7 +5,7 @@ \title{Function to use the OKPolicy quarto website template} \usage{ ojo_use_template( - path, + path = NULL, template = c("website", "report"), .interactive = rlang::is_interactive() ) @@ -13,7 +13,8 @@ ojo_use_template( \arguments{ \item{path}{Character. The path to the project directory. Can be absolute or relative to the current working directory. The last component of the path will be used as the -project name and must be in kebab-case (lowercase letters, numbers, and hyphens only).} +project name and must be in kebab-case (lowercase letters, numbers, and hyphens only). +If \code{NULL} (default) and in interactive mode, prompts for project name and location.} \item{template}{Character. The type of project template to use. Must be one of: \itemize{ @@ -34,6 +35,9 @@ of the path and must be in kebab-case format. } \examples{ \dontrun{ +# Interactive mode (prompts for project name and location) +ojo_use_template() + # Create a new website project (default) ojo_use_template("my-new-website") From 7b9de86b007c4ed36838bb42cd3d21650aef046c Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Wed, 29 Apr 2026 16:04:16 -0500 Subject: [PATCH 6/6] feat: point templates at main branch --- R/template.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/template.R b/R/template.R index 7a0bf1a..1c85d8c 100644 --- a/R/template.R +++ b/R/template.R @@ -104,8 +104,8 @@ ojo_use_template <- function( # Map friendly names to GitHub template specs template_spec <- switch(template, - website = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template@mason-dev", - report = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template@mason-dev" + website = "openjusticeok/okpolicy-quarto-templates/okpolicy-website-template", + report = "openjusticeok/okpolicy-quarto-templates/okpolicy-report-template" ) # Path Resolution