#![allow(clippy::uninlined_format_args)]
mod api;
mod commands;
mod config;
mod error;
mod http;
mod interactive;
mod logging;
mod mcp;
mod output;
mod plugins;
mod shell;
mod storage;
use anyhow::Result;
use clap::{CommandFactory, Parser, Subcommand, error::ErrorKind};
use clap_complete::{Shell, generate};
use colored::Colorize;
use rustyline::Editor;
use rustyline::config::{CompletionType, Config as EditorConfig, EditMode};
use rustyline::error::ReadlineError;
use rustyline::history::DefaultHistory;
use std::io;
use api::{
AuthClient, DataManagementClient, DerivativeClient, DesignAutomationClient, IssuesClient,
OssClient, RealityCaptureClient, RfiClient, WebhooksClient,
};
use commands::{
AccCommands, AuthCommands, BucketCommands, ConfigCommands, DaCommands, DemoCommands,
FolderCommands, GenerateArgs, HubCommands, IssueCommands, ItemCommands, ObjectCommands,
PipelineCommands, PluginCommands, ProjectCommands, RealityCommands, RfiCommands,
TranslateCommands, WebhookCommands,
};
use config::Config;
use error::ExitCode;
use output::OutputFormat;
#[derive(Parser)]
#[command(name = "raps")]
#[command(author = "Dmytro Yemelianov <https://rapscli.xyz>")]
#[command(version = env!("CARGO_PKG_VERSION"))]
#[command(about = "🌼 RAPS (rapeseed) — Rust Autodesk Platform Services CLI", long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[arg(long, value_name = "FORMAT", global = true)]
output: Option<String>,
#[arg(long, global = true)]
no_color: bool,
#[arg(short, long, global = true)]
quiet: bool,
#[arg(short, long, global = true)]
verbose: bool,
#[arg(long, global = true)]
debug: bool,
#[arg(long, global = true)]
non_interactive: bool,
#[arg(long, global = true)]
yes: bool,
#[arg(long, value_name = "SECONDS", global = true)]
timeout: Option<u64>,
#[arg(long, value_name = "N", global = true)]
concurrency: Option<usize>,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(subcommand)]
Auth(AuthCommands),
#[command(subcommand)]
Bucket(BucketCommands),
#[command(subcommand)]
Object(ObjectCommands),
#[command(subcommand)]
Translate(TranslateCommands),
#[command(subcommand)]
Hub(HubCommands),
#[command(subcommand)]
Project(ProjectCommands),
#[command(subcommand)]
Folder(FolderCommands),
#[command(subcommand)]
Item(ItemCommands),
#[command(subcommand)]
Webhook(WebhookCommands),
#[command(subcommand)]
Da(DaCommands),
#[command(subcommand)]
Issue(IssueCommands),
#[command(subcommand)]
Acc(AccCommands),
#[command(subcommand)]
Rfi(RfiCommands),
#[command(subcommand)]
Reality(RealityCommands),
#[command(subcommand)]
Plugin(PluginCommands),
Generate(GenerateArgs),
#[command(subcommand)]
Demo(DemoCommands),
#[command(subcommand)]
Config(ConfigCommands),
#[command(subcommand)]
Pipeline(PipelineCommands),
Completions {
#[arg(value_enum)]
shell: Shell,
},
Shell,
Serve,
}
#[tokio::main]
async fn main() {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
let exit_code = match e.kind() {
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => 0,
_ => 2,
};
e.print().unwrap();
std::process::exit(exit_code);
}
};
logging::init(cli.no_color, cli.quiet, cli.verbose, cli.debug);
interactive::init(cli.non_interactive, cli.yes);
if let Err(err) = run(cli).await {
let exit_code = ExitCode::from_error(&err);
if !logging::quiet() {
eprintln!("{} {}", "Error:".red().bold(), err);
let mut source = err.source();
while let Some(cause) = source {
eprintln!(" {} {}", "Caused by:".dimmed(), cause);
source = cause.source();
}
}
exit_code.exit();
}
}
async fn run(cli: Cli) -> Result<()> {
if let Commands::Completions { shell } = &cli.command {
let mut cmd = Cli::command();
generate(*shell, &mut cmd, "raps", &mut io::stdout());
return Ok(());
}
if let Commands::Serve = &cli.command {
mcp::server::run_server()
.await
.map_err(|e| anyhow::anyhow!("{}", e))?;
return Ok(());
}
if let Commands::Config(_) = &cli.command {
let output_format = if let Some(format_str) = &cli.output {
Some(format_str.parse()?)
} else {
None
};
let output_format = OutputFormat::determine(output_format);
if let Commands::Config(cmd) = cli.command {
return cmd.execute(output_format).await;
}
unreachable!()
}
let output_format = if let Some(format_str) = &cli.output {
Some(format_str.parse()?)
} else {
None
};
let output_format = OutputFormat::determine(output_format);
if logging::verbose() || logging::debug() {
logging::log_verbose("🌼 RAPS CLI starting...");
}
let config = Config::from_env()?;
let http_config = http::HttpClientConfig::from_cli_and_env(cli.timeout);
if let Commands::Shell = cli.command {
println!("{}", "Welcome to the RAPS interactive shell!".bold());
println!("Type 'help' for a list of commands, 'exit' to quit.");
println!(
"{}",
"Use TAB for command completion, hints show required parameters.".dimmed()
);
println!();
let editor_config = EditorConfig::builder()
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.auto_add_history(true)
.tab_stop(4)
.build();
let helper = shell::RapsHelper::new();
let mut rl: Editor<shell::RapsHelper, DefaultHistory> = Editor::with_config(editor_config)?;
rl.set_helper(Some(helper));
let history_path = ".raps_history";
let _ = rl.load_history(history_path);
let prompt = "raps> ";
let colored_prompt = format!("\x1b[1;33m{}\x1b[0m", prompt);
loop {
let readline = rl.readline(&colored_prompt);
match readline {
Ok(line) => {
let _ = rl.add_history_entry(line.as_str());
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "exit" || line == "quit" {
break;
}
if line == "help" || line == "?" {
println!("{}", "Available commands:".bold());
println!(
" {:<16} Authentication (login, logout, status, test, whoami)",
"auth".cyan()
);
println!(
" {:<16} Bucket operations (list, create, get, delete)",
"bucket".cyan()
);
println!(
" {:<16} Object operations (list, upload, download, delete)",
"object".cyan()
);
println!(
" {:<16} Model Derivative (start, status, manifest, metadata)",
"translate".cyan()
);
println!(" {:<16} Hub operations (list, get)", "hub".cyan());
println!(" {:<16} Project operations (list, get)", "project".cyan());
println!(
" {:<16} Folder operations (list, get, create)",
"folder".cyan()
);
println!(" {:<16} Item operations (get, versions)", "item".cyan());
println!(
" {:<16} Webhook management (list, create, get, delete)",
"webhook".cyan()
);
println!(
" {:<16} Design Automation (engines, appbundles, activities)",
"da".cyan()
);
println!(
" {:<16} ACC/BIM 360 Issues (list, get, create)",
"issue".cyan()
);
println!(" {:<16} ACC RFIs (list, get)", "rfi".cyan());
println!(" {:<16} Configuration management", "config".cyan());
println!(" {:<16} Exit the shell", "exit".cyan());
println!();
println!("{}", "Tips:".bold());
println!(" • Press {} for command completion", "TAB".green());
println!(
" • {} hints show required parameters",
"Gray text".dimmed()
);
println!(
" • Use {} or {} for command help",
"<command> --help".green(),
"<command> -h".green()
);
continue;
}
let mut args = shlex::split(line).unwrap_or_default();
args.insert(0, "raps".to_string());
let sub_cli = match Cli::try_parse_from(&args) {
Ok(c) => c,
Err(e) => {
e.print().unwrap();
continue;
}
};
let sub_output_format = if let Some(format_str) = &sub_cli.output {
Some(format_str.parse()?)
} else {
None
};
let sub_output_format = OutputFormat::determine(sub_output_format);
let sub_http_config = http::HttpClientConfig::from_cli_and_env(sub_cli.timeout);
if let Err(err) = execute_command(
sub_cli.command,
&config,
&sub_http_config,
sub_output_format,
sub_cli.concurrency.unwrap_or(5),
)
.await
{
eprintln!("{} {}", "Error:".red().bold(), err);
let mut source = err.source();
while let Some(cause) = source {
eprintln!(" {} {}", "Caused by:".dimmed(), cause);
source = cause.source();
}
}
}
Err(ReadlineError::Interrupted) => {
println!("CTRL-C");
break;
}
Err(ReadlineError::Eof) => {
println!("CTRL-D");
break;
}
Err(err) => {
println!("Error: {:?}", err);
break;
}
}
}
rl.save_history(history_path).unwrap();
return Ok(());
}
execute_command(
cli.command,
&config,
&http_config,
output_format,
cli.concurrency.unwrap_or(5),
)
.await?;
Ok(())
}
async fn execute_command(
command: Commands,
config: &Config,
http_config: &http::HttpClientConfig,
output_format: OutputFormat,
concurrency: usize,
) -> Result<()> {
let get_auth_client =
|| -> AuthClient { AuthClient::new_with_http_config(config.clone(), http_config.clone()) };
let get_oss_client = || -> OssClient {
let auth = get_auth_client();
OssClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_derivative_client = || -> DerivativeClient {
let auth = get_auth_client();
DerivativeClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_dm_client = || -> DataManagementClient {
let auth = get_auth_client();
DataManagementClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_webhooks_client = || -> WebhooksClient {
let auth = get_auth_client();
WebhooksClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_da_client = || -> DesignAutomationClient {
let auth = get_auth_client();
DesignAutomationClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_issues_client = || -> IssuesClient {
let auth = get_auth_client();
IssuesClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
let get_rc_client = || -> RealityCaptureClient {
let auth = get_auth_client();
RealityCaptureClient::new_with_http_config(config.clone(), auth, http_config.clone())
};
match command {
Commands::Auth(cmd) => {
cmd.execute(&get_auth_client(), output_format).await?;
}
Commands::Bucket(cmd) => {
cmd.execute(&get_oss_client(), output_format).await?;
}
Commands::Object(cmd) => {
cmd.execute(&get_oss_client(), output_format).await?;
}
Commands::Translate(cmd) => {
cmd.execute(&get_derivative_client(), output_format).await?;
}
Commands::Hub(cmd) => {
cmd.execute(&get_dm_client(), output_format).await?;
}
Commands::Project(cmd) => {
cmd.execute(&get_dm_client(), output_format).await?;
}
Commands::Folder(cmd) => {
cmd.execute(&get_dm_client(), output_format).await?;
}
Commands::Item(cmd) => {
cmd.execute(&get_dm_client(), output_format).await?;
}
Commands::Webhook(cmd) => {
cmd.execute(&get_webhooks_client(), output_format).await?;
}
Commands::Da(cmd) => {
cmd.execute(&get_da_client(), output_format).await?;
}
Commands::Issue(cmd) => {
cmd.execute(&get_issues_client(), output_format).await?;
}
Commands::Acc(cmd) => {
let auth_client = get_auth_client();
let acc_client = api::AccClient::new(config.clone(), auth_client);
cmd.execute(&acc_client, output_format).await?;
}
Commands::Rfi(cmd) => {
let auth_client = get_auth_client();
let rfi_client =
RfiClient::new_with_http_config(config.clone(), auth_client, http_config.clone());
cmd.execute(&rfi_client, output_format).await?;
}
Commands::Reality(cmd) => {
cmd.execute(&get_rc_client(), output_format).await?;
}
Commands::Plugin(cmd) => {
cmd.execute(output_format)?;
}
Commands::Generate(args) => {
commands::generate::execute(args).await?;
}
Commands::Demo(cmd) => {
cmd.execute(concurrency).await?;
}
Commands::Config(_) => {
unreachable!()
}
Commands::Pipeline(cmd) => cmd.execute(output_format).await?,
Commands::Completions { .. } => {
unreachable!()
}
Commands::Shell => {
unreachable!()
}
Commands::Serve => {
unreachable!()
}
}
Ok(())
}