From bfbcf03655e97231b72ea6169b414d44b29d064c Mon Sep 17 00:00:00 2001 From: Brandon Shippy Date: Tue, 18 Aug 2026 13:29:38 -0700 Subject: [PATCH 1/4] framework, search with CEL expression filter --- rust/crates/sift_cli/src/cli/mod.rs | 17 ++++++++ rust/crates/sift_cli/src/cmd/get/assets.rs | 51 ++++++++++++++++++++++ rust/crates/sift_cli/src/cmd/get/mod.rs | 1 + rust/crates/sift_cli/src/cmd/mod.rs | 1 + rust/crates/sift_cli/src/main.rs | 3 ++ rust/crates/sift_cli/src/util/tty.rs | 9 ++-- 6 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 rust/crates/sift_cli/src/cmd/get/assets.rs create mode 100644 rust/crates/sift_cli/src/cmd/get/mod.rs diff --git a/rust/crates/sift_cli/src/cli/mod.rs b/rust/crates/sift_cli/src/cli/mod.rs index e1a77899b5..00a7ee71fe 100644 --- a/rust/crates/sift_cli/src/cli/mod.rs +++ b/rust/crates/sift_cli/src/cli/mod.rs @@ -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,10 @@ impl DocArgs { "0.0.0.0:3000".parse().unwrap() } } + +#[derive(clap::Args)] +pub struct GetAssetArgs { + /// Filter option for filtering search + #[arg(long)] + pub filter: 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 0000000000..d8341cd497 --- /dev/null +++ b/rust/crates/sift_cli/src/cmd/get/assets.rs @@ -0,0 +1,51 @@ +use std::process::ExitCode; + +use anyhow::{Context as AnyhowContext, Result}; +use sift_rs::assets::v1::{ + ListAssetsRequest, ListAssetsResponse, asset_service_client::AssetServiceClient, +}; + +use crate::{ + cli::GetAssetArgs, + cmd::Context, + util::{api::create_grpc_channel, tty::Output}, +}; + +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: 50, + ..Default::default() + }) + .await + .context("failed to list assets")? + .into_inner(); + let mut output = Output::new(); + if assets.is_empty() { + output.line("no assets found"); + } else { + output.line(format_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 0a3064023c..c80a3d7012 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, Write}, +}; use anyhow::Result; use crossterm::style::Stylize; @@ -67,8 +70,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 } From 374b50f4b90aa3dd6d806bd6f8e00ba459491da4 Mon Sep 17 00:00:00 2001 From: Brandon Shippy Date: Tue, 18 Aug 2026 13:40:32 -0700 Subject: [PATCH 2/4] set pagination limit --- rust/crates/sift_cli/src/cli/mod.rs | 6 +++++- rust/crates/sift_cli/src/cmd/get/assets.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rust/crates/sift_cli/src/cli/mod.rs b/rust/crates/sift_cli/src/cli/mod.rs index 00a7ee71fe..b1af5a93a0 100644 --- a/rust/crates/sift_cli/src/cli/mod.rs +++ b/rust/crates/sift_cli/src/cli/mod.rs @@ -881,7 +881,11 @@ impl DocArgs { #[derive(clap::Args)] pub struct GetAssetArgs { - /// Filter option for filtering search + /// 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, } diff --git a/rust/crates/sift_cli/src/cmd/get/assets.rs b/rust/crates/sift_cli/src/cmd/get/assets.rs index d8341cd497..5874d82dfb 100644 --- a/rust/crates/sift_cli/src/cmd/get/assets.rs +++ b/rust/crates/sift_cli/src/cmd/get/assets.rs @@ -20,7 +20,7 @@ pub async fn run(ctx: Context, args: GetAssetArgs) -> Result { .list_assets(ListAssetsRequest { filter: args.filter.unwrap_or_default(), order_by: "modified_date desc".to_string(), - page_size: 50, + page_size: args.limit.unwrap_or_default(), ..Default::default() }) .await From efcff137d3bb262255857975e8e5d0e7132e9fd6 Mon Sep 17 00:00:00 2001 From: Brandon Shippy Date: Tue, 18 Aug 2026 14:08:49 -0700 Subject: [PATCH 3/4] hyperlink output --- rust/crates/sift_cli/src/cmd/get/assets.rs | 26 +++++++++++++-- rust/crates/sift_cli/src/util/explore_url.rs | 1 + rust/crates/sift_cli/src/util/tty.rs | 35 +++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/rust/crates/sift_cli/src/cmd/get/assets.rs b/rust/crates/sift_cli/src/cmd/get/assets.rs index 5874d82dfb..d86503a673 100644 --- a/rust/crates/sift_cli/src/cmd/get/assets.rs +++ b/rust/crates/sift_cli/src/cmd/get/assets.rs @@ -6,9 +6,15 @@ use sift_rs::assets::v1::{ }; use crate::{ + BIN_NAME, cli::GetAssetArgs, cmd::Context, - util::{api::create_grpc_channel, tty::Output}, + 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; @@ -26,10 +32,13 @@ pub async fn run(ctx: Context, args: GetAssetArgs) -> Result { .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(); if assets.is_empty() { output.line("no assets found"); } else { + let linkify = stdout_is_tty(); + output.line(format_args!( "{: Result { width = ASSET_ID_CHAR_LENGTH_WHITESPACE_LENGTH, )); for asset in &assets { + let name = match build_explore_url(app_uri, &asset.name, None) { + Some(url) if linkify => 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(); diff --git a/rust/crates/sift_cli/src/util/explore_url.rs b/rust/crates/sift_cli/src/util/explore_url.rs index bed2d177fa..9621aa3f70 100644 --- a/rust/crates/sift_cli/src/util/explore_url.rs +++ b/rust/crates/sift_cli/src/util/explore_url.rs @@ -72,6 +72,7 @@ pub fn build_explore_url( mod tests { use super::{build_explore_url, import_target}; + #[test] fn import_target_uses_the_configured_app_uri() { let target = import_target( diff --git a/rust/crates/sift_cli/src/util/tty.rs b/rust/crates/sift_cli/src/util/tty.rs index c80a3d7012..d877918500 100644 --- a/rust/crates/sift_cli/src/util/tty.rs +++ b/rust/crates/sift_cli/src/util/tty.rs @@ -1,6 +1,6 @@ use std::{ fmt::Display, - io::{self, Write}, + io::{self, IsTerminal, Write}, }; use anyhow::Result; @@ -59,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, @@ -104,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\\")); + } +} From d92890f47da11ab3bc0beba529631ea86ccbdf90 Mon Sep 17 00:00:00 2001 From: Brandon Shippy Date: Tue, 18 Aug 2026 14:27:44 -0700 Subject: [PATCH 4/4] output-format flag --- rust/crates/sift_cli/src/cli/mod.rs | 13 +++- rust/crates/sift_cli/src/cmd/get/assets.rs | 66 +++++++++++--------- rust/crates/sift_cli/src/util/explore_url.rs | 1 - 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/rust/crates/sift_cli/src/cli/mod.rs b/rust/crates/sift_cli/src/cli/mod.rs index b1af5a93a0..01684fdb45 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; @@ -879,6 +879,13 @@ impl DocArgs { } } +#[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 @@ -888,4 +895,8 @@ pub struct GetAssetArgs { /// 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 index d86503a673..affec4ae3b 100644 --- a/rust/crates/sift_cli/src/cmd/get/assets.rs +++ b/rust/crates/sift_cli/src/cmd/get/assets.rs @@ -7,7 +7,7 @@ use sift_rs::assets::v1::{ use crate::{ BIN_NAME, - cli::GetAssetArgs, + cli::{GetAssetArgs, OutputFormats}, cmd::Context, util::{ api::create_grpc_channel, @@ -34,36 +34,44 @@ pub async fn run(ctx: Context, args: GetAssetArgs) -> Result { .into_inner(); let app_uri = ctx.app_uri.as_deref().and_then(normalize_app_uri); let mut output = Output::new(); - 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!( - "{: { + output.line(serde_json::to_string_pretty(&assets).context("failed to encode assets")?); } - output.tip(match app_uri { - Some(host) => { - format!("View an asset in Explore at {host}/explore?method=single&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." + ), + }); } - None => format!( - "Run `{BIN_NAME} config update --app-uri ` for Explore links." - ), - }); + } } output.print(); diff --git a/rust/crates/sift_cli/src/util/explore_url.rs b/rust/crates/sift_cli/src/util/explore_url.rs index 9621aa3f70..bed2d177fa 100644 --- a/rust/crates/sift_cli/src/util/explore_url.rs +++ b/rust/crates/sift_cli/src/util/explore_url.rs @@ -72,7 +72,6 @@ pub fn build_explore_url( mod tests { use super::{build_explore_url, import_target}; - #[test] fn import_target_uses_the_configured_app_uri() { let target = import_target(