mod api;
mod commands;
mod models;
mod update_checker;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use api::RedashClient;
#[derive(Parser)]
#[command(name = "stmo-cli", version)]
#[command(about = "CLI tool for version controlling Redash queries and dashboards", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(about = "List all queries from Redash")]
Discover,
#[command(about = "Scaffold a new query/dashboard repository")]
Init,
#[command(about = "Fetch queries from Redash")]
Fetch {
#[arg(help = "Query IDs to fetch (e.g., 123 456 789)")]
query_ids: Vec<u64>,
#[arg(long, help = "Fetch all queries currently tracked in queries/ directory")]
all: bool,
},
#[command(about = "Deploy local changes to Redash (only changed queries by default)")]
Deploy {
#[arg(help = "Query IDs to deploy (e.g., 123 456 789)")]
query_ids: Vec<u64>,
#[arg(long, help = "Deploy all queries instead of only changed ones")]
all: bool,
},
#[command(about = "Execute a query and display results")]
Execute {
#[arg(help = "Query ID to execute (must be fetched locally first)")]
query_id: u64,
#[arg(long, help = "Query parameter in format: name=value (can be used multiple times)")]
param: Vec<String>,
#[arg(long, short = 'f', default_value = "json", help = "Output format: json or table")]
format: String,
#[arg(long, short = 'i', help = "Prompt for missing parameters interactively")]
interactive: bool,
#[arg(long, default_value = "300", help = "Timeout in seconds")]
timeout: u64,
#[arg(long, help = "Limit number of rows displayed (default: all)")]
limit: Option<usize>,
},
#[command(about = "List and explore data sources")]
DataSources {
#[arg(help = "Optional: Data source ID to inspect")]
data_source_id: Option<u64>,
#[arg(long, help = "Show table schema for the data source")]
schema: bool,
#[arg(long, help = "Force refresh schema from data source (slower but always up-to-date)")]
refresh: bool,
#[arg(long, short = 'f', default_value = "json", help = "Output format: json or table")]
format: String,
},
#[command(about = "Archive queries in Redash and remove local files")]
Archive {
#[arg(help = "Query IDs to archive (e.g., 123 456 789)")]
query_ids: Vec<u64>,
#[arg(long, help = "Remove local files for queries already archived in Redash")]
cleanup: bool,
},
#[command(about = "Restore archived queries")]
Unarchive {
#[arg(help = "Query IDs to unarchive (e.g., 123 456 789)")]
query_ids: Vec<u64>,
},
#[command(about = "Manage dashboards")]
Dashboards {
#[command(subcommand)]
command: DashboardCommands,
},
#[command(about = "Update stmo-cli to the latest version")]
Update,
}
#[derive(Subcommand)]
enum DashboardCommands {
#[command(about = "List all dashboards from Redash")]
Discover,
#[command(about = "Fetch dashboards from Redash")]
Fetch {
#[arg(help = "Dashboard slugs to fetch (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")]
slugs: Vec<String>,
},
#[command(about = "Deploy dashboard changes to Redash")]
Deploy {
#[arg(help = "Dashboard slugs to deploy (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")]
slugs: Vec<String>,
#[arg(long, help = "Deploy all tracked dashboards")]
all: bool,
},
#[command(about = "Archive dashboards in Redash and remove local files")]
Archive {
#[arg(help = "Dashboard slugs to archive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")]
slugs: Vec<String>,
},
#[command(about = "Restore archived dashboards")]
Unarchive {
#[arg(help = "Dashboard slugs to unarchive (e.g., firefox-desktop-on-steamos bug-2006698---ccov-build-regression)")]
slugs: Vec<String>,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if let Commands::Init = cli.command {
return commands::init::init();
}
if let Commands::Update = cli.command {
return commands::update::update();
}
update_checker::check_and_auto_update().await;
let api_key = std::env::var("REDASH_API_KEY")
.context("REDASH_API_KEY environment variable not set")?;
let base_url = std::env::var("REDASH_URL")
.unwrap_or_else(|_| "https://sql.telemetry.mozilla.org".to_string());
let client = RedashClient::new(base_url, &api_key)?;
match cli.command {
Commands::Discover => commands::discover::discover(&client).await?,
Commands::Init => unreachable!("Init handled above"),
Commands::Fetch { query_ids, all } => commands::fetch::fetch(&client, query_ids, all).await?,
Commands::Deploy { query_ids, all } => commands::deploy::deploy(&client, query_ids, all).await?,
Commands::Execute { query_id, param, format, interactive, timeout, limit } => {
let output_format = format.parse::<commands::OutputFormat>()
.context("Invalid output format")?;
let limit_rows = limit;
commands::execute::execute(
&client,
query_id,
param,
output_format,
interactive,
timeout,
limit_rows,
).await?;
}
Commands::DataSources { data_source_id, schema, refresh, format } => {
let output_format = format.parse::<commands::OutputFormat>()
.context("Invalid output format")?;
if let Some(id) = data_source_id {
commands::datasources::show_data_source(&client, id, schema, refresh, output_format).await?;
} else {
commands::datasources::list_data_sources(&client, output_format).await?;
}
}
Commands::Archive { query_ids, cleanup } => {
if cleanup {
commands::archive::cleanup(&client).await?;
} else if !query_ids.is_empty() {
commands::archive::archive(&client, query_ids).await?;
} else {
anyhow::bail!("No query IDs specified. Use specific query IDs or --cleanup flag.\n\nExamples:\n stmo-cli archive 123 456\n stmo-cli archive --cleanup");
}
}
Commands::Unarchive { query_ids } => {
if query_ids.is_empty() {
anyhow::bail!("No query IDs specified. Provide query IDs to unarchive.\n\nExample:\n stmo-cli unarchive 123 456");
}
commands::archive::unarchive(&client, query_ids).await?;
}
Commands::Dashboards { command } => match command {
DashboardCommands::Discover => commands::dashboards::discover(&client).await?,
DashboardCommands::Fetch { slugs } => commands::dashboards::fetch(&client, slugs.clone()).await?,
DashboardCommands::Deploy { slugs, all } => commands::dashboards::deploy(&client, slugs.clone(), all).await?,
DashboardCommands::Archive { slugs } => commands::dashboards::archive(&client, slugs.clone()).await?,
DashboardCommands::Unarchive { slugs } => commands::dashboards::unarchive(&client, slugs.clone()).await?,
},
Commands::Update => unreachable!("Update handled above"),
}
Ok(())
}