Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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/add-library-crate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": minor
---

Expose library crate (`lib.rs`) for programmatic API access. Extracts `config_dir()` and Model Armor sanitization types into standalone modules so they can be shared between the binary and library targets without pulling in CLI-only code.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ authors = ["Justin Poehnelt"]
keywords = ["cli", "google-workspace", "google", "drive", "gmail"]
categories = ["command-line-utilities", "web-programming"]

[lib]
name = "gws"
path = "src/lib.rs"

[[bin]]
name = "gws"
path = "src/main.rs"
Expand Down
2 changes: 1 addition & 1 deletion src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub fn email_to_b64(email: &str) -> String {

/// Path to `accounts.json` inside the config directory.
pub fn accounts_path() -> PathBuf {
crate::auth_commands::config_dir().join("accounts.json")
crate::config::config_dir().join("accounts.json")
}

/// Load the accounts registry from disk. Returns `None` if the file does not
Expand Down
2 changes: 1 addition & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub async fn get_token(scopes: &[&str], account: Option<&str>) -> anyhow::Result

let creds_file = std::env::var("GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE").ok();
let impersonated_user = std::env::var("GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER").ok();
let config_dir = crate::auth_commands::config_dir();
let config_dir = crate::config::config_dir();

// If env var credentials are specified, skip account resolution entirely
if creds_file.is_some() {
Expand Down
25 changes: 1 addition & 24 deletions src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,30 +92,7 @@ const READONLY_SCOPES: &[&str] = &[
];

pub fn config_dir() -> PathBuf {
#[cfg(test)]
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
return PathBuf::from(dir);
}

// Use ~/.config/gws on all platforms for a consistent, user-friendly path.
let primary = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config")
.join("gws");
if primary.exists() {
return primary;
}

// Backward compat: fall back to OS-specific config dir for existing installs
// (e.g. ~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows).
let legacy = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("gws");
if legacy.exists() {
return legacy;
}

primary
crate::config::config_dir()
}

fn plain_credentials_path() -> PathBuf {
Expand Down
49 changes: 49 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::PathBuf;

/// Returns the gws configuration directory.
///
/// Prefers `~/.config/gws` for a consistent, cross-platform path.
/// Falls back to the OS-specific config directory (e.g. `~/Library/Application Support/gws`
/// on macOS) for backward compatibility with existing installs.
///
/// In tests, the `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` environment variable overrides the default.
pub fn config_dir() -> PathBuf {
#[cfg(test)]
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
return PathBuf::from(dir);
}

// Use ~/.config/gws on all platforms for a consistent, user-friendly path.
let primary = dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".config")
.join("gws");
if primary.exists() {
return primary;
}

// Backward compat: fall back to OS-specific config dir for existing installs
// (e.g. ~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows).
let legacy = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("gws");
if legacy.exists() {
return legacy;
}

primary
}
6 changes: 3 additions & 3 deletions src/credential_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "unknown-user".to_string());

let key_file = crate::auth_commands::config_dir().join(".encryption_key");
let key_file = crate::config::config_dir().join(".encryption_key");

let entry = Entry::new("gws-cli", &username);

Expand Down Expand Up @@ -218,7 +218,7 @@ pub fn decrypt(data: &[u8]) -> anyhow::Result<Vec<u8>> {

/// Returns the path for encrypted credentials.
pub fn encrypted_credentials_path() -> PathBuf {
crate::auth_commands::config_dir().join("credentials.enc")
crate::config::config_dir().join("credentials.enc")
}

/// Saves credentials JSON to an encrypted file.
Expand Down Expand Up @@ -280,7 +280,7 @@ pub fn load_encrypted() -> anyhow::Result<String> {
pub fn encrypted_credentials_path_for(account: &str) -> PathBuf {
let normalised = crate::accounts::normalize_email(account);
let b64 = crate::accounts::email_to_b64(&normalised);
crate::auth_commands::config_dir().join(format!("credentials.{b64}.enc"))
crate::config::config_dir().join(format!("credentials.{b64}.enc"))
}

/// Saves credentials JSON to a per-account encrypted file.
Expand Down
2 changes: 1 addition & 1 deletion src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ pub async fn fetch_discovery_document(
let version =
crate::validate::validate_api_identifier(version).map_err(|e| anyhow::anyhow!("{e}"))?;

let cache_dir = crate::auth_commands::config_dir().join("cache");
let cache_dir = crate::config::config_dir().join("cache");
std::fs::create_dir_all(&cache_dir)?;

let cache_file = cache_dir.join(format!("{service}_{version}.json"));
Expand Down
13 changes: 6 additions & 7 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async fn handle_json_response(
body_text: &str,
pagination: &PaginationConfig,
sanitize_template: Option<&str>,
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
sanitize_mode: &crate::sanitize::SanitizeMode,
output_format: &crate::formatter::OutputFormat,
pages_fetched: &mut u32,
page_token: &mut Option<String>,
Expand All @@ -219,15 +219,14 @@ async fn handle_json_response(
// Run Model Armor sanitization if --sanitize is enabled
if let Some(template) = sanitize_template {
let text_to_check = serde_json::to_string(&json_val).unwrap_or_default();
match crate::helpers::modelarmor::sanitize_text(template, &text_to_check).await {
match crate::sanitize::sanitize_text(template, &text_to_check).await {
Ok(result) => {
let is_match = result.filter_match_state == "MATCH_FOUND";
if is_match {
eprintln!("⚠️ Model Armor: prompt injection detected (filterMatchState: MATCH_FOUND)");
}

if is_match && *sanitize_mode == crate::helpers::modelarmor::SanitizeMode::Block
{
if is_match && *sanitize_mode == crate::sanitize::SanitizeMode::Block {
let blocked = serde_json::json!({
"error": "Content blocked by Model Armor",
"sanitizationResult": serde_json::to_value(&result).unwrap_or_default(),
Expand Down Expand Up @@ -365,7 +364,7 @@ pub async fn execute_method(
dry_run: bool,
pagination: &PaginationConfig,
sanitize_template: Option<&str>,
sanitize_mode: &crate::helpers::modelarmor::SanitizeMode,
sanitize_mode: &crate::sanitize::SanitizeMode,
output_format: &crate::formatter::OutputFormat,
capture_output: bool,
) -> Result<Option<Value>, GwsError> {
Expand Down Expand Up @@ -1651,7 +1650,7 @@ async fn test_execute_method_dry_run() {
let params_json = r#"{"fileId": "123"}"#;
let body_json = r#"{"name": "test.txt"}"#;

let sanitize_mode = crate::helpers::modelarmor::SanitizeMode::Warn;
let sanitize_mode = crate::sanitize::SanitizeMode::Warn;
let pagination = PaginationConfig::default();

let result = execute_method(
Expand Down Expand Up @@ -1696,7 +1695,7 @@ async fn test_execute_method_missing_path_param() {
..Default::default()
};

let sanitize_mode = crate::helpers::modelarmor::SanitizeMode::Warn;
let sanitize_mode = crate::sanitize::SanitizeMode::Warn;
let result = execute_method(
&doc,
&method,
Expand Down
Loading
Loading