gws is a Rust CLI tool for interacting with Google Workspace APIs. It dynamically generates its command surface at runtime by parsing Google Discovery Service JSON documents.
Important
Dynamic Discovery: This project does NOT use generated Rust crates (e.g., google-drive3) for API interaction. Instead, it fetches the Discovery JSON at runtime and builds clap commands dynamically. When adding a new service, you only need to register it in src/services.rs and verify the Discovery URL pattern in src/discovery.rs. Do NOT add new crates to Cargo.toml for standard Google APIs.
Note
Package Manager: Use pnpm instead of npm for Node.js package management in this repository.
Important
Test Coverage: The codecov/patch check requires that new or modified lines are covered by tests. When adding code, extract testable helper functions rather than embedding logic in main/run where it's hard to unit-test. Run cargo test locally and verify new branches are exercised.
cargo build # Build in dev mode
cargo clippy -- -D warnings # Lint check
cargo test # Run testsEvery PR must include a changeset file. Create one at .changeset/<descriptive-name>.md:
---
"@googleworkspace/cli": patch
---
Brief description of the changeUse patch for fixes/chores, minor for new features, major for breaking changes. The CI policy check will fail without a changeset.
The CLI uses a two-phase argument parsing strategy:
- Parse argv to extract the service name (e.g.,
drive) - Fetch the service's Discovery Document, build a dynamic
clap::Commandtree, then re-parse
| File | Purpose |
|---|---|
src/main.rs |
Entrypoint, two-phase CLI parsing, --account global flag extraction, method resolution |
src/discovery.rs |
Serde models for Discovery Document + fetch/cache |
src/services.rs |
Service alias → Discovery API name/version mapping |
src/auth.rs |
OAuth2 token acquisition with multi-account support via accounts.json registry |
src/accounts.rs |
Multi-account registry (accounts.json), email normalisation, base64 encoding |
src/credential_store.rs |
AES-256-GCM encryption/decryption, per-account credential file paths |
src/auth_commands.rs |
gws auth subcommands: login, logout, list, default, setup, status, export |
src/commands.rs |
Recursive clap::Command builder from Discovery resources |
src/executor.rs |
HTTP request construction, response handling, schema validation |
src/schema.rs |
gws schema command — introspect API method schemas |
src/error.rs |
Structured JSON error output |
Demo recordings are generated with VHS (.tape files).
vhs docs/demo.tape- Use double quotes for simple strings:
Type "gws --help" Enter - Use backtick quotes when the typed text contains JSON with double quotes:
Type `gws drive files list --params '{"pageSize":5}'` Enter\"escapes inside double-quotedTypestrings are not supported by VHS and will cause parse errors.
ASCII art title cards live in art/. The scripts/show-art.sh helper clears the screen and cats the file. Portrait scenes use scene*.txt; landscape chapters use long-*.txt.
Important
This CLI is frequently invoked by AI/LLM agents. Always assume inputs can be adversarial — validate paths against traversal (../../.ssh), restrict format strings to allowlists, reject control characters, and encode user values before embedding them in URLs.
When adding new helpers or CLI flags that accept file paths, always validate using the shared helpers:
| Scenario | Validator | Rejects |
|---|---|---|
File path for writing (--output-dir) |
validate::validate_safe_output_dir() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
File path for reading (--dir) |
validate::validate_safe_dir_path() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
Enum/allowlist values (--msg-format) |
clap value_parser (see gmail/mod.rs) |
Any value not in the allowlist |
// In your argument parser:
if let Some(output_dir) = matches.get_one::<String>("output-dir") {
crate::validate::validate_safe_output_dir(output_dir)?;
builder.output_dir(Some(output_dir.clone()));
}User-supplied values embedded in URL path segments must be percent-encoded. Use the shared helper:
// CORRECT — encodes slashes, spaces, and special characters
let url = format!(
"https://www.googleapis.com/drive/v3/files/{}",
crate::helpers::encode_path_segment(file_id),
);
// WRONG — raw user input in URL path
let url = format!("https://www.googleapis.com/drive/v3/files/{}", file_id);For query parameters, use reqwest's .query() builder which handles encoding automatically:
// CORRECT — reqwest encodes query values
client.get(url).query(&[("q", user_query)]).send().await?;
// WRONG — manual string interpolation in query strings
let url = format!("{}?q={}", base_url, user_query);When a user-supplied string is used as a GCP resource identifier (project ID, topic name, space name, etc.) that gets embedded in a URL path, validate it first:
// Validates the string does not contain path traversal segments (`..`), control characters, or URL-breaking characters like `?` and `#`.
let project = crate::helpers::validate_resource_name(&project_id)?;
let url = format!("https://pubsub.googleapis.com/v1/projects/{}/topics/my-topic", project);This prevents injection of query parameters, path traversal, or other malicious payloads through resource name arguments like --project or --space.
When adding a new helper or CLI command:
- File paths → Use
validate_safe_output_dir/validate_safe_dir_path - Enum flags → Constrain via clap
value_parserorvalidate_msg_format - URL path segments → Use
encode_path_segment() - Query parameters → Use reqwest
.query()builder - Resource names (project IDs, space names, topic names) → Use
validate_resource_name() - Write tests for both the happy path AND the rejection path (e.g., pass
../../.sshand assertErr)
GOOGLE_WORKSPACE_CLI_TOKEN— Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading)GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE— Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in~/.config/gws/)GOOGLE_WORKSPACE_CLI_ACCOUNT— Default account email for multi-account usage (overridden by--accountflag)- Supports
.envfiles viadotenvy