feat: begin designing cli

This commit is contained in:
DecDuck
2026-01-06 16:11:06 +07:00
parent aa21a779ff
commit 4e32c38948
6 changed files with 1660 additions and 49 deletions
+1512 -18
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -4,6 +4,15 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.54", features = ["derive"] }
console = "0.16.2"
dialoguer = "0.12.0"
droplet-rs = { git = "https://github.com/Drop-OSS/droplet-rs.git", version = "0.11.1" }
indicatif = "0.18.3"
reqwest = { version = "0.13.1", features = ["json"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.148"
tokio = { version = "1.48.0", features = ["macros"] }
url = "2.5.8"
webbrowser = "1.0.6"
+33
View File
@@ -0,0 +1,33 @@
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Specify data file path
#[arg(short, long)]
pub data: Option<String>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Configures a new Drop server
Configure {
/// Endpoint of the Drop server
url: String,
/// API token for non-interactive configuration.
#[arg(short, long)]
token: Option<String>
},
/// Uploads new game version to depot
Upload {
/// Path of new version
path: bool,
/// ID of game to attach to
game_id: String,
/// Version ID to attach to
version_id: String,
},
}
+79
View File
@@ -0,0 +1,79 @@
use std::sync::LazyLock;
use anyhow::{Result, anyhow};
use dialoguer::{Confirm, Input, theme::ColorfulTheme};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use url::Url;
const TOKEN_CREATE_PAYLOAD: &str =
"eyJuYW1lIjoiZG93bnBvdXIgKGNsaSkiLCJhY2xzIjpbImRlcG90Om5ldyJdfQ==";
static CLIENT: LazyLock<Client> = LazyLock::new(|| reqwest::Client::new());
const REQUIRED_ACLS: [&str; 1] = ["depot:new"];
pub async fn interactive_configure(url: String) -> Result<()> {
let base_url = Url::parse(&url)?;
let mut token_create_url = base_url.join("/admin/settings/tokens")?;
{
let mut query = token_create_url.query_pairs_mut();
query.append_pair("payload", TOKEN_CREATE_PAYLOAD);
};
let confirm = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"Open \"{}\" in your default browser?",
token_create_url.as_str()
))
.interact()?;
if !confirm {
return Err(anyhow!("User cancelled action"));
}
webbrowser::open(token_create_url.as_str())?;
let token: String = Input::with_theme(&ColorfulTheme::default())
.with_prompt("API token")
.interact_text()?;
validate_configuration(url, token).await?;
Ok(())
}
pub async fn validate_configuration(url: String, token: String) -> Result<()> {
let base_url = Url::parse(&url)?;
let token_check_url = base_url.join("/api/v1/token")?;
let acl_check = CLIENT
.get(token_check_url)
.bearer_auth(token)
.send()
.await?;
if !acl_check.status().is_success() {
return Err(anyhow!(
"ACL check failed with response code: {}",
acl_check.status()
));
}
let acls: Vec<String> = acl_check.json().await?;
for acl in REQUIRED_ACLS {
if !acls.contains(&acl.to_string()) {
return Err(anyhow!("Token missing {} acl", acl));
}
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct ServerConfiguration {
pub endpoint: String,
pub token: String,
}
+1
View File
@@ -0,0 +1 @@
pub mod configure;
+26 -31
View File
@@ -1,40 +1,35 @@
use std::{env, path::PathBuf};
use std::{env, path::PathBuf, sync::LazyLock};
use anyhow::Result;
use clap::Parser;
use droplet_rs::manifest::generate_manifest_rusty;
use indicatif::{ProgressBar, ProgressStyle};
use tokio::runtime::Handle;
use crate::{cli::{Cli, Commands}, commands::configure::interactive_configure};
mod cli;
mod commands;
pub static CLI: LazyLock<Cli> = LazyLock::new(|| Cli::parse());
#[tokio::main]
async fn main() {
let threads = Handle::current().metrics().num_workers();
println!("using {} workers", threads);
let args: Vec<String> = env::args().collect();
let path = &args[1];
async fn main() -> Result<()> {
let path = PathBuf::from(path);
if !path.exists() {
panic!("{} does not exist", path.display());
}
println!("using path {}", path.display());
let progress_bar = ProgressBar::new(100_00).with_style(
ProgressStyle::template(
ProgressStyle::default_bar(),
"[{elapsed_precise}] [ETA {eta}] {wide_bar} {percent_precise}%",
)
.unwrap(),
);
let manifest = generate_manifest_rusty(
&path,
|progress| {
let progress_int = (progress * 100.0f32).round() as u64;
progress_bar.set_position(progress_int);
match &CLI.command {
Commands::Configure { url, token } => {
if let Some(token) = token {
todo!()
} else {
interactive_configure(url.to_string()).await?;
}
},
|log| {
progress_bar.println(log);
},
)
.await
.expect("failed to generate manifest");
Commands::Upload {
path,
game_id,
version_id,
} => todo!(),
};
Ok(())
}