#![doc = include_str!("../README.md")]
pub mod commands;
pub mod cli;
pub mod ui;
pub mod utils;
pub mod logger;
use anyhow::Result;
use utils::app_state::AppState;
use utils::license as license_utils;
use std::path::Path;
#[cfg(not(feature = "test-exposed"))]
use anyhow::anyhow;
#[cfg(not(feature = "test-exposed"))]
use std::env;
#[cfg(not(feature = "test-exposed"))]
use std::fs;
#[cfg(not(feature = "test-exposed"))]
fn load_license_token_from_env_or_file(state_path: &Path) -> Option<String> {
if let Ok(tok) = env::var("CLEANSH_LICENSE") {
return Some(tok);
}
if let Some(parent) = state_path.parent() {
let license_file = parent.join("license.token");
if license_file.exists() {
if let Ok(s) = fs::read_to_string(&license_file) {
return Some(s.trim().to_string());
}
}
}
None
}
#[cfg(not(feature = "test-exposed"))]
fn license_url() -> String {
env::var("CLEANSH_LICENSE_URL").unwrap_or_else(|_| "https://your-site.example/upgrade".to_string())
}
#[cfg(not(feature = "test-exposed"))]
fn require_license_for_feature(feature: &str, state_path: &Path, app_state: &mut AppState, theme_map: &ui::theme::ThemeMap) -> Result<license_utils::LicenseToken> {
let tok = load_license_token_from_env_or_file(state_path)
.ok_or_else(|| anyhow!("No license provided"))?;
let parsed = match license_utils::parse_and_verify_compact(&tok) {
Ok(p) => p,
Err(e) => {
commands::cleansh::error_msg(format!("License validation failed: {}. Visit {}", e, license_url()), theme_map);
std::process::exit(2);
}
};
let fp = parsed.fingerprint();
if app_state.is_license_consumed(&fp) {
commands::cleansh::error_msg(format!("License appears fully consumed. Visit {}", license_url()), theme_map);
std::process::exit(2);
}
let feature_entry = parsed.payload.features.get(feature)
.or_else(|| parsed.payload.features.get("*"));
match feature_entry {
Some(opt_limit) => {
if let Some(limit) = opt_limit {
let used = app_state.get_license_feature_usage(&fp, feature);
if used >= *limit {
commands::cleansh::error_msg(format!("No remaining uses for feature '{}' on this license (used {}/{}). Visit {}", feature, used, limit, license_url()), theme_map);
std::process::exit(2);
} else {
commands::cleansh::info_msg(format!("License validated — '{}' unlocked. Expires: {}. Usage for '{}': {}/{}", feature, parsed.payload.expires_at, feature, used, limit), theme_map);
}
} else {
commands::cleansh::info_msg(format!("License validated — '{}' unlocked (unlimited). Expires: {}", feature, parsed.payload.expires_at), theme_map);
}
}
None => {
commands::cleansh::error_msg(format!("This license does not grant access to feature '{}'. Visit {}", feature, license_url()), theme_map);
std::process::exit(2);
}
}
Ok(parsed)
}
pub fn consume_license_post_success(token: &license_utils::LicenseToken, feature: &str, app_state: &mut AppState, state_path: &Path, theme_map: &ui::theme::ThemeMap) {
let fp = token.fingerprint();
let used_before = app_state.get_license_feature_usage(&fp, feature);
app_state.increment_license_feature_usage(&fp, feature);
let used_after = used_before.saturating_add(1);
if let Some(opt_limit) = token.payload.features.get(feature).cloned().flatten() {
if used_after >= opt_limit {
let mut all_exhausted = true;
for (feat_name, feat_limit_opt) in &token.payload.features {
if let Some(limit) = feat_limit_opt {
let used = app_state.get_license_feature_usage(&fp, feat_name.as_str());
if used < *limit {
all_exhausted = false;
break;
}
}
}
if all_exhausted {
app_state.mark_license_consumed(&fp);
}
}
}
if let Err(e) = app_state.save(state_path) {
commands::cleansh::warn_msg(format!("Failed to persist app state after license usage: {}", e), theme_map);
} else {
commands::cleansh::info_msg(format!("Recorded license usage for feature '{}'. (fingerprint: {})", feature, fp), theme_map);
}
}
pub fn check_license_for_feature(
feature: &str,
state_path: &Path,
app_state: &mut AppState,
theme_map: &ui::theme::ThemeMap,
) -> Result<Option<license_utils::LicenseToken>> {
#[cfg(feature = "test-exposed")]
{
commands::cleansh::info_msg("License check bypassed in test mode.", theme_map);
let _ = (feature, state_path, app_state); Ok(None)
}
#[cfg(not(feature = "test-exposed"))]
{
let token = require_license_for_feature(feature, state_path, app_state, theme_map)?;
Ok(Some(token))
}
}
#[cfg(any(test, feature = "test-exposed"))]
pub mod test_exposed {
pub mod config {
pub use cleansh_core::config::{
MAX_PATTERN_LENGTH,
RedactionConfig,
RedactionRule,
RedactionSummaryItem,
RuleConfigNotFoundError,
merge_rules,
};
}
pub mod sanitizer {
pub use cleansh_core::{
CompiledRule,
CompiledRules,
compile_rules,
};
}
pub mod redaction_match {
pub use cleansh_core::redaction_match::{
RedactionMatch,
redact_sensitive,
};
}
pub mod validators {
pub use cleansh_core::validators::{
is_valid_ssn_programmatically,
is_valid_uk_nino_programmatically,
};
}
pub mod commands {
pub use crate::commands::cleansh::{run_cleansh_opts, sanitize_single_line};
pub use crate::commands::stats::run_stats_command;
pub use crate::commands::uninstall::run_uninstall_command;
}
pub mod ui {
pub use crate::ui::diff_viewer;
pub use crate::ui::output_format;
pub use crate::ui::redaction_summary;
pub use crate::ui::theme;
pub use crate::ui::verify_ui;
pub use crate::ui::sync_ui;
}
pub mod utils {
pub use crate::utils::app_state::*;
pub use crate::utils::platform::*;
pub use crate::utils::clipboard::*;
pub use crate::utils::license::*;
}
pub mod logger {
pub use crate::logger::*;
}
}