diff --git a/rust/crates/sift_cli/src/cli/mod.rs b/rust/crates/sift_cli/src/cli/mod.rs index e1a77899b..01684fdb4 100644 --- a/rust/crates/sift_cli/src/cli/mod.rs +++ b/rust/crates/sift_cli/src/cli/mod.rs @@ -1,4 +1,4 @@ -use clap::{Parser, Subcommand, crate_version}; +use clap::{Parser, Subcommand, ValueEnum, crate_version}; use clap_complete::Shell; use parquet::{ChannelMode, ComplexTypesMode}; pub mod hdf5; @@ -51,6 +51,10 @@ pub enum Cmd { #[command(subcommand)] Export(ExportCmd), + /// Get commands to discover and pull data from Sift + #[command(subcommand)] + Get(GetCmd), + /// Ping the Sift API to verify credentials and connectivity Ping, @@ -311,6 +315,12 @@ pub enum ImportCmd { Backup(BackupArgs), } +#[derive(Subcommand)] +pub enum GetCmd { + /// Get assets + Asset(GetAssetArgs), +} + #[derive(Subcommand)] pub enum ConfigCmd { /// Display the contents of the current config file @@ -868,3 +878,25 @@ impl DocArgs { "0.0.0.0:3000".parse().unwrap() } } + +#[derive(Clone, Copy, ValueEnum, Debug, PartialEq, Eq)] +#[value(rename_all = "lowercase")] +pub enum OutputFormats { + Text, + Json, +} + +#[derive(clap::Args)] +pub struct GetAssetArgs { + /// Filter option for filtering search with CEL expression + #[arg(long)] + pub filter: Option, + + /// Caps returned results to set number + #[arg(long, default_value = "50")] + pub limit: Option, + + /// Determines the output format + #[arg(long, value_enum)] + pub output_format: Option, +} diff --git a/rust/crates/sift_cli/src/cmd/get/assets.rs b/rust/crates/sift_cli/src/cmd/get/assets.rs new file mode 100644 index 000000000..affec4ae3 --- /dev/null +++ b/rust/crates/sift_cli/src/cmd/get/assets.rs @@ -0,0 +1,79 @@ +use std::process::ExitCode; + +use anyhow::{Context as AnyhowContext, Result}; +use sift_rs::assets::v1::{ + ListAssetsRequest, ListAssetsResponse, asset_service_client::AssetServiceClient, +}; + +use crate::{ + BIN_NAME, + cli::{GetAssetArgs, OutputFormats}, + cmd::Context, + util::{ + api::create_grpc_channel, + app_uri::normalize_app_uri, + explore_url::build_explore_url, + tty::{Output, hyperlink, link_style, stdout_is_tty}, + }, +}; + +const ASSET_ID_CHAR_LENGTH_WHITESPACE_LENGTH: usize = 38; + +pub async fn run(ctx: Context, args: GetAssetArgs) -> Result { + let grpc_channel = create_grpc_channel(&ctx)?; + + let ListAssetsResponse { assets, .. } = AssetServiceClient::new(grpc_channel) + .list_assets(ListAssetsRequest { + filter: args.filter.unwrap_or_default(), + order_by: "modified_date desc".to_string(), + page_size: args.limit.unwrap_or_default(), + ..Default::default() + }) + .await + .context("failed to list assets")? + .into_inner(); + let app_uri = ctx.app_uri.as_deref().and_then(normalize_app_uri); + let mut output = Output::new(); + match args.output_format { + Some(OutputFormats::Json) => { + output.line(serde_json::to_string_pretty(&assets).context("failed to encode assets")?); + } + Some(OutputFormats::Text) | None => { + if assets.is_empty() { + output.line("no assets found"); + } else { + let linkify = stdout_is_tty(); + + output.line(format_args!( + "{: hyperlink(&link_style(&asset.name), &url), + _ => asset.name.clone(), + }; + output.line(format_args!( + "{: format!( + "View an asset in Explore at {host}/explore?method=single&assets=" + ), + None => format!( + "Run `{BIN_NAME} config update --app-uri ` for Explore \ + links." + ), + }); + } + } + } + output.print(); + + Ok(ExitCode::SUCCESS) +} diff --git a/rust/crates/sift_cli/src/cmd/get/mod.rs b/rust/crates/sift_cli/src/cmd/get/mod.rs new file mode 100644 index 000000000..5d8c80bf4 --- /dev/null +++ b/rust/crates/sift_cli/src/cmd/get/mod.rs @@ -0,0 +1 @@ +pub mod assets; diff --git a/rust/crates/sift_cli/src/cmd/mod.rs b/rust/crates/sift_cli/src/cmd/mod.rs index 29c77d4eb..c897e2eba 100644 --- a/rust/crates/sift_cli/src/cmd/mod.rs +++ b/rust/crates/sift_cli/src/cmd/mod.rs @@ -9,6 +9,7 @@ pub mod agent; pub mod config; pub mod doc; pub mod export; +pub mod get; pub mod import; pub mod install; pub mod mcp; diff --git a/rust/crates/sift_cli/src/main.rs b/rust/crates/sift_cli/src/main.rs index c3fa86627..44dac8bed 100644 --- a/rust/crates/sift_cli/src/main.rs +++ b/rust/crates/sift_cli/src/main.rs @@ -154,6 +154,9 @@ fn run(clargs: cli::Args) -> Result { cli::ExportCmd::Run(args) => run_future(cmd::export::run(ctx, args)), cli::ExportCmd::Asset(args) => run_future(cmd::export::asset(ctx, args)), }, + Cmd::Get(cmd) => match cmd { + cli::GetCmd::Asset(args) => run_future(cmd::get::assets::run(ctx, args)), + }, Cmd::Ping => run_future(cmd::ping::run(ctx)), _ => Ok(ExitCode::SUCCESS), } diff --git a/rust/crates/sift_cli/src/util/tty.rs b/rust/crates/sift_cli/src/util/tty.rs index 0a3064023..d87791850 100644 --- a/rust/crates/sift_cli/src/util/tty.rs +++ b/rust/crates/sift_cli/src/util/tty.rs @@ -1,4 +1,7 @@ -use std::io::{self, Write}; +use std::{ + fmt::Display, + io::{self, IsTerminal, Write}, +}; use anyhow::Result; use crossterm::style::Stylize; @@ -56,6 +59,18 @@ impl PromptUser { } } +pub fn stdout_is_tty() -> bool { + io::stdout().is_terminal() +} + +pub fn hyperlink(text: &str, url: &str) -> String { + format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\") +} + +pub fn link_style(text: &str) -> String { + text.cyan().underlined().to_string() +} + #[derive(Default)] pub struct Output { lines: Vec, @@ -67,8 +82,8 @@ impl Output { Self::default() } - pub fn line>(&mut self, txt: S) -> &mut Self { - self.lines.push(txt.into()); + pub fn line(&mut self, txt: S) -> &mut Self { + self.lines.push(txt.to_string()); self } @@ -101,3 +116,24 @@ impl Output { eprintln!("{}: {out}", "error".red()) } } + +#[cfg(test)] +mod tests { + use super::hyperlink; + + #[test] + fn hyperlink_wraps_text_in_an_osc_8_sequence() { + assert_eq!( + hyperlink("engine", "https://app.siftstack.com/explore?assets=engine"), + "\x1b]8;;https://app.siftstack.com/explore?assets=engine\x1b\\engine\x1b]8;;\x1b\\" + ); + } + + #[test] + fn hyperlink_leaves_the_visible_width_unchanged() { + let link = hyperlink("engine", "https://app.siftstack.com"); + assert!(link.contains("engine")); + assert!(link.starts_with("\x1b]8;;")); + assert!(link.ends_with("\x1b]8;;\x1b\\")); + } +}