Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/verbose-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

Add `--verbose` / `-v` flag to print HTTP request method, URL, response status, and timing to stderr for debugging
25 changes: 24 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@ pub fn build_cli(doc: &RestDescription) -> Command {
.arg(
clap::Arg::new("format")
.long("format")
.help("Output format: json (default), table, yaml, csv")
.help("Output format: json (default), table, yaml, csv, tsv")
Comment thread
abhiram304 marked this conversation as resolved.
Outdated
.value_name("FORMAT")
.global(true),
)
.arg(
clap::Arg::new("verbose")
.long("verbose")
.short('v')
.help("Print request and response details to stderr (method, URL, status, timing)")
.action(clap::ArgAction::SetTrue)
.global(true),
);

// Inject helper commands
Expand Down Expand Up @@ -279,4 +287,19 @@ mod tests {
"--sanitize arg should be present on root command"
);
}

#[test]
fn test_verbose_arg_present_and_global() {
let doc = make_doc();
let cmd = build_cli(&doc);

let args: Vec<_> = cmd.get_arguments().collect();
let verbose_arg = args.iter().find(|a| a.get_id() == "verbose");
assert!(
verbose_arg.is_some(),
"--verbose arg should be present on root command"
);
let verbose_arg = verbose_arg.unwrap();
assert!(verbose_arg.is_global_set(), "--verbose should be global");
}
}
37 changes: 37 additions & 0 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ async fn handle_binary_response(
/// 5. Handling various response types (JSON, binary).
/// 6. Auto-pagination for list endpoints.
/// 7. Model Armor prompt injection scanning.
///
/// # Stdout / stderr contract
///
/// * **stdout** — all structured output: JSON responses, dry-run previews,
/// download summaries. Must remain machine-readable so that pipes like
/// `gws ... | jq` work correctly.
/// * **stderr** — all human-readable diagnostics: hints, warnings, verbose
/// request/response details. Never emitted to stdout.
#[allow(clippy::too_many_arguments)]
pub async fn execute_method(
doc: &RestDescription,
Expand All @@ -376,6 +384,7 @@ pub async fn execute_method(
upload_path: Option<&str>,
upload_content_type: Option<&str>,
dry_run: bool,
verbose: bool,
pagination: &PaginationConfig,
sanitize_template: Option<&str>,
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
Expand Down Expand Up @@ -408,6 +417,10 @@ pub async fn execute_method(
let mut captured_values = Vec::new();

loop {
if verbose && pages_fetched > 0 {
eprintln!("[page {}]", pages_fetched + 1);
}

let client = crate::client::build_client()?;
let request = build_http_request(
&client,
Expand All @@ -423,11 +436,33 @@ pub async fn execute_method(
.await?;

let method_id = method.id.as_deref().unwrap_or("unknown");
if verbose {
let display_url = {
let mut params = input.query_params.clone();
if let Some(pt) = page_token.as_deref() {
params.push(("pageToken".to_string(), pt.to_string()));
}
if params.is_empty() {
input.full_url.clone()
} else {
let qs = params
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&");
format!("{}?{}", input.full_url, qs)
Comment thread
abhiram304 marked this conversation as resolved.
Outdated
}
};
Comment thread
abhiram304 marked this conversation as resolved.
eprintln!("> {} {}", method.http_method, display_url);
}
Comment thread
abhiram304 marked this conversation as resolved.
let start = std::time::Instant::now();
let response = request.send().await.context("HTTP request failed")?;
let latency_ms = start.elapsed().as_millis() as u64;

let status = response.status();
if verbose {
eprintln!("< {} ({}ms)", status, latency_ms);
}
let content_type = response
.headers()
.get("content-type")
Expand Down Expand Up @@ -2026,6 +2061,7 @@ async fn test_execute_method_dry_run() {
None,
None,
true, // dry_run
false,
&pagination,
None,
&sanitize_mode,
Expand Down Expand Up @@ -2070,6 +2106,7 @@ async fn test_execute_method_missing_path_param() {
None,
None,
true,
false,
&PaginationConfig::default(),
None,
&sanitize_mode,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/drive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ TIPS:
Some(file_path),
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/gmail/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ pub(super) async fn send_raw_email(
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
1 change: 1 addition & 0 deletions src/helpers/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/sheets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&pagination,
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down Expand Up @@ -181,6 +182,7 @@ TIPS:
None,
None,
matches.get_flag("dry-run"),
matches.get_flag("verbose"),
&executor::PaginationConfig::default(),
None,
&crate::helpers::modelarmor::SanitizeMode::Warn,
Expand Down
7 changes: 5 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ async fn run() -> Result<(), GwsError> {
Ok(fmt) => fmt,
Err(unknown) => {
eprintln!(
"warning: unknown output format '{unknown}'; falling back to json (valid options: json, table, yaml, csv)"
"warning: unknown output format '{unknown}'; falling back to json (valid options: json, table, yaml, csv, tsv)"
);
formatter::OutputFormat::Json
}
Expand Down Expand Up @@ -228,6 +228,7 @@ async fn run() -> Result<(), GwsError> {
.map(|s| s.as_str());

let dry_run = matched_args.get_flag("dry-run");
let verbose = matched_args.get_flag("verbose");

// Build pagination config from flags
let pagination = parse_pagination_config(matched_args);
Expand Down Expand Up @@ -266,6 +267,7 @@ async fn run() -> Result<(), GwsError> {
upload_path,
upload_content_type,
dry_run,
verbose,
&pagination,
sanitize_config.template.as_deref(),
&sanitize_config.mode,
Expand Down Expand Up @@ -434,7 +436,8 @@ fn print_usage() {
println!(" --upload <PATH> Local file to upload as media content (multipart)");
println!(" --upload-content-type <MIME> MIME type of the uploaded file (auto-detected from extension if omitted)");
println!(" --output <PATH> Output file path for binary responses");
println!(" --format <FMT> Output format: json (default), table, yaml, csv");
println!(" --format <FMT> Output format: json (default), table, yaml, csv, tsv");
println!(" --verbose / -v Print request/response details to stderr");
println!(" --api-version <VER> Override the API version (e.g., v2, v3)");
println!(" --page-all Auto-paginate, one JSON line per page (NDJSON)");
println!(" --page-limit <N> Max pages to fetch with --page-all (default: 10)");
Expand Down
Loading