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
34 changes: 33 additions & 1 deletion rust/crates/sift_cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will be using these in a lot of other get subcommands so this should be a reusable struct that we flatten

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// Filter option for filtering search with CEL expression
#[arg(long)]
pub filter: Option<String>,

/// Caps returned results to set number
#[arg(long, default_value = "50")]
pub limit: Option<u32>,

/// Determines the output format
#[arg(long, value_enum)]
pub output_format: Option<OutputFormats>,
}
79 changes: 79 additions & 0 deletions rust/crates/sift_cli/src/cmd/get/assets.rs
Original file line number Diff line number Diff line change
@@ -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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use an ASCII table crate instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


pub async fn run(ctx: Context, args: GetAssetArgs) -> Result<ExitCode> {
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(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be exposed as an option

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!(
"{:<width$}{}",
"ID",
"Name",
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!(
"{:<width$}{name}",
asset.asset_id,
width = ASSET_ID_CHAR_LENGTH_WHITESPACE_LENGTH,
));
}
output.tip(match app_uri {
Some(host) => format!(
"View an asset in Explore at {host}/explore?method=single&assets=<NAME>"
),
None => format!(
"Run `{BIN_NAME} config update --app-uri <SIFT_WEB_ORIGIN>` for Explore \
links."
),
});
}
}
}
output.print();

Ok(ExitCode::SUCCESS)
}
1 change: 1 addition & 0 deletions rust/crates/sift_cli/src/cmd/get/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod assets;
1 change: 1 addition & 0 deletions rust/crates/sift_cli/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/sift_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ fn run(clargs: cli::Args) -> Result<ExitCode> {
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),
}
Expand Down
42 changes: 39 additions & 3 deletions rust/crates/sift_cli/src/util/tty.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::io::{self, Write};
use std::{
fmt::Display,
io::{self, IsTerminal, Write},
};

use anyhow::Result;
use crossterm::style::Stylize;
Expand Down Expand Up @@ -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<String>,
Expand All @@ -67,8 +82,8 @@ impl Output {
Self::default()
}

pub fn line<S: Into<String>>(&mut self, txt: S) -> &mut Self {
self.lines.push(txt.into());
pub fn line<S: Display>(&mut self, txt: S) -> &mut Self {
self.lines.push(txt.to_string());
self
}

Expand Down Expand Up @@ -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\\"));
}
}
Loading