Skip to content
Merged
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
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ clap = { workspace = true }
clap_complete = "4"
anyhow = { workspace = true }
colored = { workspace = true }
owo-colors = "4"
indicatif = { workspace = true }
dialoguer = { workspace = true }
tabled = { workspace = true }
Expand Down
24 changes: 15 additions & 9 deletions crates/cli/src/commands/auth.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! `prism login` and `prism logout` — Manage API credentials for hosted services.

use clap::{Args, Subcommand};
use anyhow::{Result, anyhow};
use colored::Colorize;
use clap::{Args, Subcommand};
use dialoguer::Select;
use rpassword::prompt_password;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -72,18 +71,23 @@ async fn login(
config_path: Option<String>,
output_format: &str,
) -> Result<()> {
let palette = crate::output::theme::ColorPalette::default();

// Determine provider name
let provider = match provider_param {
Some(p) => p,
None => select_provider_interactive()?,
};

// Prompt for API key securely
let prompt = format!("Enter your API key for {}: ", provider.green());
let prompt = format!(
"Enter your API key for {}: ",
palette.success_text(&provider)
);
let api_key = prompt_password(&prompt)?;

if api_key.trim().is_empty() {
eprintln!("{}", "API key cannot be empty.".red());
eprintln!("{}", palette.error_text("API key cannot be empty."));
std::process::exit(1);
}

Expand All @@ -102,11 +106,11 @@ async fn login(
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
println!("✓ Credentials for {} saved.", provider.green());
println!("✓ Credentials for {} saved.", palette.success_text(&provider));
}
}
Err(e) => {
eprintln!("{} {}", "Error:".red(), e);
eprintln!("{} {}", palette.error_text("Error:"), e);
std::process::exit(1);
}
}
Expand All @@ -120,6 +124,8 @@ async fn logout(
config_path: Option<String>,
output_format: &str,
) -> Result<()> {
let palette = crate::output::theme::ColorPalette::default();

// Determine provider name
let provider = match provider_param {
Some(p) => p,
Expand All @@ -141,7 +147,7 @@ async fn logout(
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
println!("✓ Credentials for {} removed.", provider.green());
println!("✓ Credentials for {} removed.", palette.success_text(&provider));
}
}
Ok(false) => {
Expand All @@ -157,11 +163,11 @@ async fn logout(
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
println!("No credentials found for {}.", provider.yellow());
println!("No credentials found for {}.", palette.warning_text(&provider));
}
}
Err(e) => {
eprintln!("{} {}", "Error:".red(), e);
eprintln!("{} {}", palette.error_text("Error:"), e);
std::process::exit(1);
}
}
Expand Down
33 changes: 20 additions & 13 deletions crates/cli/src/commands/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use anyhow::Result;
use colored::Colorize;
use directories::ProjectDirs;
use crate::output::theme::ColorPalette;

// ─── Args ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -42,11 +42,12 @@ impl Status {
}
}

fn label(&self) -> colored::ColoredString {
fn label(&self) -> String {
let palette = ColorPalette::default();
match self {
Self::Ok => " OK ".green().bold(),
Self::Warning(_) => " WARN ".yellow().bold(),
Self::Error(_) => " ERROR ".red().bold(),
Self::Ok => palette.success_text(" OK "),
Self::Warning(_) => palette.warning_text(" WARN "),
Self::Error(_) => palette.error_text(" ERROR "),
}
}
}
Expand Down Expand Up @@ -264,21 +265,26 @@ fn dir_size_mib(path: &PathBuf) -> Result<u64> {
// ─── Report ───────────────────────────────────────────────────────────────────

fn print_report(checks: &[Check], quiet: bool) {
let palette = ColorPalette::default();
let sep = "─".repeat(58);
println!("\n {}", "Prism Diagnostic Report".bold());
println!(" {}\n", sep.dimmed());
println!("\n {}", palette.accent_text("Prism Diagnostic Report"));
println!(" {}\n", palette.muted_text(&sep));

for check in checks {
if quiet && check.status.is_ok() {
continue;
}
println!(" [{}] {}", check.status.label(), check.name);
if let Some(detail) = check.status.detail() {
println!(" {} {}", "└─".dimmed(), detail.dimmed());
println!(
" {} {}",
palette.muted_text("└─"),
palette.muted_text(detail)
);
}
}

println!("\n {}", sep.dimmed());
println!("\n {}", palette.muted_text(&sep));

let warnings = checks
.iter()
Expand All @@ -287,20 +293,21 @@ fn print_report(checks: &[Check], quiet: bool) {
let errors = checks.iter().filter(|c| c.status.is_error()).count();

if errors == 0 && warnings == 0 {
println!(" {}\n", "All checks passed.".green().bold());
println!(" {}\n", palette.success_text("All checks passed."));
} else {
println!(
" {} warning(s), {} error(s).\n",
warnings.to_string().yellow(),
errors.to_string().red()
palette.warning_text(&warnings.to_string()),
palette.error_text(&errors.to_string())
);
}
}

// ─── Entry point ──────────────────────────────────────────────────────────────

pub async fn run(args: DiagnosticArgs) -> Result<()> {
println!("{}", "Running diagnostics…".dimmed());
let palette = ColorPalette::default();
println!("{}", palette.muted_text("Running diagnostics..."));

let mut checks: Vec<Check> = Vec::new();

Expand Down
7 changes: 7 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ struct Cli {
/// Suppress non-essential output.
#[arg(long, short, global = true)]
quiet: bool,

/// Disable ANSI colors in terminal output.
#[arg(long, global = true)]
no_color: bool,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -128,10 +132,13 @@ async fn main() -> anyhow::Result<()> {
output = %cli.output,
network_arg = %cli.network,
verbose = cli.verbose,
no_color = cli.no_color,
config_loaded = loaded_config.is_some(),
"CLI arguments parsed"
);

output::theme::set_color_enabled(!cli.no_color);

let mut network = prism_core::network::config::resolve_network(&cli.network);
if let Some(ref rpc_url) = cli.rpc_url {
network.rpc_url = rpc_url.clone();
Expand Down
15 changes: 9 additions & 6 deletions crates/cli/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod compact;
pub mod human;
pub mod json;
pub mod renderers;
pub mod theme;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Expand Down Expand Up @@ -69,8 +70,9 @@ pub fn print_resource_profile(
renderers::BudgetBar::new("Memory", profile.total_memory, profile.memory_limit)
.render()
);
let palette = theme::ColorPalette::default();
for warning in &profile.warnings {
println!("{} {warning}", colored::Colorize::yellow("⚠"));
println!("{} {warning}", palette.warning_text("⚠"));
}
println!();
print!("{}", renderers::render_heatmap(profile));
Expand All @@ -84,13 +86,14 @@ pub fn print_state_diff(diff: &StateDiff, output_format: &str) -> anyhow::Result
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(diff)?),
OutputFormat::Short => println!("{}", format_state_diff_summary(diff)),
OutputFormat::Human => {
println!("{}", colored::Colorize::bold("State Diff"));
let palette = theme::ColorPalette::default();
println!("{}", palette.accent_text("State Diff"));
for entry in &diff.entries {
let symbol = match entry.change_type {
DiffChangeType::Created => colored::Colorize::green("+"),
DiffChangeType::Deleted => colored::Colorize::red("-"),
DiffChangeType::Updated => colored::Colorize::yellow("~"),
DiffChangeType::Unchanged => colored::Colorize::dimmed(" "),
DiffChangeType::Created => palette.success_text("+"),
DiffChangeType::Deleted => palette.error_text("-"),
DiffChangeType::Updated => palette.warning_text("~"),
DiffChangeType::Unchanged => palette.muted_text(" "),
};
println!("{symbol} {}", entry.key);
}
Expand Down
35 changes: 20 additions & 15 deletions crates/cli/src/output/renderers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

#![allow(dead_code)]

use colored::Colorize;
use prism_core::types::report::TransactionContext;
use prism_core::types::trace::ResourceProfile;
use tabled::{Table, Tabled};
use crate::output::theme::ColorPalette;

const BAR_WIDTH: usize = 10;
const HEAT_BLOCKS: [&str; 4] = ["░", "▒", "▓", "█"];
Expand All @@ -31,8 +31,9 @@ impl<'a> SectionHeader<'a> {
let border = format!("+{}+", "-".repeat(inner.chars().count()));
let middle = format!("|{}|", inner);

let border = border.cyan().bold().to_string();
let middle = middle.white().bold().to_string();
let palette = ColorPalette::default();
let border = palette.metadata_text(&border);
let middle = palette.accent_text(&middle);

format!("{}\n{}\n{}", border, middle, border)
}
Expand Down Expand Up @@ -61,12 +62,13 @@ impl BudgetBar {
let empty = BAR_WIDTH.saturating_sub(filled);
let bar_str = format!("{}{}", "█".repeat(filled), "░".repeat(empty));

let palette = ColorPalette::default();
let colored_bar = if pct >= 0.9 {
bar_str.red().bold().to_string()
palette.error_text(&bar_str)
} else if pct >= 0.7 {
bar_str.yellow().to_string()
palette.warning_text(&bar_str)
} else {
bar_str.green().to_string()
palette.success_text(&bar_str)
};

format!(
Expand Down Expand Up @@ -95,24 +97,26 @@ fn heat_cell(intensity: f64) -> String {
let empty = BAR_WIDTH.saturating_sub(filled);
let cell = format!("{}{}", block.repeat(filled), "░".repeat(empty));

let palette = ColorPalette::default();
if intensity >= 0.75 {
cell.red().bold().to_string()
palette.error_text(&cell)
} else if intensity >= 0.5 {
cell.yellow().to_string()
palette.warning_text(&cell)
} else if intensity >= 0.25 {
cell.cyan().to_string()
palette.metadata_text(&cell)
} else {
cell.dimmed().to_string()
palette.muted_text(&cell)
}
}

/// Render a resource heatmap grid from a `ResourceProfile`.
pub fn render_heatmap(profile: &ResourceProfile) -> String {
if profile.hotspots.is_empty() {
let palette = ColorPalette::default();
return format!(
"{}\n {}\n",
render_section_header("Resource Heatmap"),
"No hotspot data available.".dimmed()
palette.muted_text("No hotspot data available.")
);
}

Expand Down Expand Up @@ -186,12 +190,13 @@ pub fn render_heatmap(profile: &ResourceProfile) -> String {
}

out.push('\n');
let palette = ColorPalette::default();
out.push_str(&format!(
" Legend: {} cold {} low {} medium {} hot\n",
"░░░░░░░░░░".dimmed(),
"▒▒▒▒▒▒▒▒▒▒".cyan(),
"▓▓▓▓▓▓▓▓▓▓".yellow(),
"██████████".red().bold(),
palette.muted_text("░░░░░░░░░░"),
palette.metadata_text("▒▒▒▒▒▒▒▒▒▒"),
palette.warning_text("▓▓▓▓▓▓▓▓▓▓"),
palette.error_text("██████████"),
));

out
Expand Down
Loading