forked from didirus/AstralRinth
Merge branch 'master' into gui_search
This commit is contained in:
@@ -1,24 +1,52 @@
|
||||
use eyre::Result;
|
||||
use futures::TryFutureExt;
|
||||
use paris::*;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_futures::WithSubscriber;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
|
||||
mod subcommands;
|
||||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
mod subcommands;
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// The official Modrinth CLI
|
||||
pub struct Args {
|
||||
#[argh(subcommand)]
|
||||
pub subcommand: subcommands::SubCommand,
|
||||
pub subcommand: subcommands::Subcommand,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
let args = argh::from_env::<Args>();
|
||||
theseus::init().await?;
|
||||
|
||||
args.dispatch()
|
||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||
.and_then(|_| async { Ok(theseus::save().await?) })
|
||||
.await
|
||||
color_eyre::install()?;
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new("info"))?;
|
||||
|
||||
let format = fmt::layer()
|
||||
.without_time()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_target(false)
|
||||
.compact();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(format)
|
||||
.with(filter)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(
|
||||
async move {
|
||||
args.dispatch()
|
||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||
.await
|
||||
}
|
||||
.with_current_subscriber(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
use eyre::Result;
|
||||
|
||||
mod profile;
|
||||
mod user;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum SubCommand {
|
||||
pub enum Subcommand {
|
||||
Profile(profile::ProfileCommand),
|
||||
User(user::UserCommand),
|
||||
}
|
||||
|
||||
impl crate::Args {
|
||||
pub async fn dispatch(&self) -> Result<()> {
|
||||
match self.subcommand {
|
||||
SubCommand::Profile(ref cmd) => cmd.dispatch(self).await,
|
||||
}
|
||||
dispatch!(self.subcommand, (self) => {
|
||||
Subcommand::Profile,
|
||||
Subcommand::User
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
//! Profile management subcommand
|
||||
use crate::util::{
|
||||
confirm_async, prompt_async, select_async, table_path_display,
|
||||
confirm_async, prompt_async, select_async, table, table_path_display,
|
||||
};
|
||||
use daedalus::modded::LoaderVersion;
|
||||
use eyre::{ensure, Result};
|
||||
use futures::prelude::*;
|
||||
use paris::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tabled::{Table, Tabled};
|
||||
use theseus::{
|
||||
data::{profiles::PROFILE_JSON_PATH, Metadata, Profile, Profiles},
|
||||
launcher::ModLoader,
|
||||
};
|
||||
use tabled::Tabled;
|
||||
use theseus::prelude::*;
|
||||
use tokio::fs;
|
||||
use tokio_stream::{wrappers::ReadDirStream, StreamExt};
|
||||
use uuid::Uuid;
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "profile")]
|
||||
/// profile management
|
||||
/// manage Minecraft instances
|
||||
pub struct ProfileCommand {
|
||||
#[argh(subcommand)]
|
||||
action: ProfileSubcommand,
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum ProfileSubcommand {
|
||||
Add(ProfileAdd),
|
||||
@@ -33,7 +30,7 @@ pub enum ProfileSubcommand {
|
||||
Run(ProfileRun),
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "add")]
|
||||
/// add a new profile to Theseus
|
||||
pub struct ProfileAdd {
|
||||
@@ -54,23 +51,26 @@ impl ProfileAdd {
|
||||
);
|
||||
|
||||
let profile = self.profile.canonicalize()?;
|
||||
let json_path = profile.join(PROFILE_JSON_PATH);
|
||||
let json_path = profile.join("profile.json");
|
||||
|
||||
ensure!(
|
||||
json_path.exists(),
|
||||
"Profile json does not exist. Perhaps you wanted `profile init` or `profile fetch`?"
|
||||
);
|
||||
ensure!(
|
||||
Profiles::get().await.unwrap().0.get(&profile).is_none(),
|
||||
!profile::is_managed(&profile).await?,
|
||||
"Profile already managed by Theseus. If the contents of the profile are invalid or missing, the profile can be regenerated using `profile init` or `profile fetch`"
|
||||
);
|
||||
Profiles::insert_from(profile).await?;
|
||||
|
||||
profile::add_path(&profile).await?;
|
||||
State::sync().await?;
|
||||
success!("Profile added!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "init")]
|
||||
/// create a new profile and manage it with Theseus
|
||||
pub struct ProfileInit {
|
||||
@@ -106,13 +106,15 @@ impl ProfileInit {
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
// TODO: validate inputs from args early
|
||||
let state = State::get().await?;
|
||||
|
||||
if self.path.exists() {
|
||||
ensure!(
|
||||
self.path.is_dir(),
|
||||
"Attempted to create profile in something other than a folder!"
|
||||
);
|
||||
ensure!(
|
||||
!self.path.join(PROFILE_JSON_PATH).exists(),
|
||||
!self.path.join("profile.json").exists(),
|
||||
"Profile already exists! Perhaps you want `profile add` instead?"
|
||||
);
|
||||
if ReadDirStream::new(fs::read_dir(&self.path).await?)
|
||||
@@ -138,8 +140,6 @@ impl ProfileInit {
|
||||
&self.path.canonicalize()?.display()
|
||||
);
|
||||
|
||||
let metadata = Metadata::get().await?;
|
||||
|
||||
// TODO: abstract default prompting
|
||||
let name = match &self.name {
|
||||
Some(name) => name.clone(),
|
||||
@@ -157,7 +157,7 @@ impl ProfileInit {
|
||||
let game_version = match &self.game_version {
|
||||
Some(version) => version.clone(),
|
||||
None => {
|
||||
let default = &metadata.minecraft.latest.release;
|
||||
let default = &state.metadata.minecraft.latest.release;
|
||||
|
||||
prompt_async(
|
||||
String::from("Game version"),
|
||||
@@ -206,8 +206,8 @@ impl ProfileInit {
|
||||
};
|
||||
|
||||
let loader_data = match loader {
|
||||
ModLoader::Forge => &metadata.forge,
|
||||
ModLoader::Fabric => &metadata.fabric,
|
||||
ModLoader::Forge => &state.metadata.forge,
|
||||
ModLoader::Fabric => &state.metadata.fabric,
|
||||
_ => eyre::bail!("Could not get manifest for loader {loader}. This is a bug in the CLI!"),
|
||||
};
|
||||
|
||||
@@ -238,8 +238,6 @@ impl ProfileInit {
|
||||
.map(PathBuf::from),
|
||||
};
|
||||
|
||||
// We don't really care if the profile already is managed, as getting this far means that the user probably wanted to re-create a profile
|
||||
drop(metadata);
|
||||
let mut profile =
|
||||
Profile::new(name, game_version, self.path.clone()).await?;
|
||||
|
||||
@@ -251,8 +249,8 @@ impl ProfileInit {
|
||||
profile.with_loader(loader, Some(loader_version));
|
||||
}
|
||||
|
||||
Profiles::insert(profile).await?;
|
||||
Profiles::save().await?;
|
||||
profile::add(profile).await?;
|
||||
State::sync().await?;
|
||||
|
||||
success!(
|
||||
"Successfully created instance, it is now available to use with Theseus!"
|
||||
@@ -261,7 +259,7 @@ impl ProfileInit {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// list all managed profiles
|
||||
#[argh(subcommand, name = "list")]
|
||||
pub struct ProfileList {}
|
||||
@@ -294,25 +292,43 @@ impl<'a> From<&'a Profile> for ProfileRow<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Path> for ProfileRow<'a> {
|
||||
fn from(it: &'a Path) -> Self {
|
||||
Self {
|
||||
name: "?",
|
||||
path: it,
|
||||
game_version: "?",
|
||||
loader: &ModLoader::Vanilla,
|
||||
loader_version: "?",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileList {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
let profiles = Profiles::get().await?;
|
||||
let profiles = profiles.0.values().map(ProfileRow::from);
|
||||
let profiles = profile::list().await?;
|
||||
let rows = profiles.iter().map(|(path, prof)| {
|
||||
prof.as_ref().map_or_else(
|
||||
|| ProfileRow::from(path.as_path()),
|
||||
ProfileRow::from,
|
||||
)
|
||||
});
|
||||
|
||||
let table = Table::new(profiles).with(tabled::Style::psql()).with(
|
||||
let table = table(rows).with(
|
||||
tabled::Modify::new(tabled::Column(1..=1))
|
||||
.with(tabled::MaxWidth::wrapping(40)),
|
||||
);
|
||||
println!("{table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// unmanage a profile
|
||||
#[argh(subcommand, name = "remove")]
|
||||
pub struct ProfileRemove {
|
||||
@@ -329,10 +345,13 @@ impl ProfileRemove {
|
||||
) -> Result<()> {
|
||||
let profile = self.profile.canonicalize()?;
|
||||
info!("Removing profile {} from Theseus", self.profile.display());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
if Profiles::remove(&profile).await?.is_none() {
|
||||
if !profile::is_managed(&profile).await? {
|
||||
warn!("Profile was not managed by Theseus!");
|
||||
} else {
|
||||
profile::remove(&profile).await?;
|
||||
State::sync().await?;
|
||||
success!("Profile removed!");
|
||||
}
|
||||
} else {
|
||||
@@ -343,7 +362,7 @@ impl ProfileRemove {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs)]
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// run a profile
|
||||
#[argh(subcommand, name = "run")]
|
||||
pub struct ProfileRun {
|
||||
@@ -351,18 +370,9 @@ pub struct ProfileRun {
|
||||
/// the profile to run
|
||||
profile: PathBuf,
|
||||
|
||||
// TODO: auth
|
||||
#[argh(option, short = 't')]
|
||||
/// the Minecraft token to use for player login. Should be replaced by auth when that is a thing.
|
||||
token: String,
|
||||
|
||||
#[argh(option, short = 'n')]
|
||||
/// the uername to use for running the game
|
||||
name: String,
|
||||
|
||||
#[argh(option, short = 'i')]
|
||||
/// the account id to use for running the game
|
||||
id: Uuid,
|
||||
#[argh(option)]
|
||||
/// the user to authenticate with
|
||||
user: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl ProfileRun {
|
||||
@@ -372,24 +382,28 @@ impl ProfileRun {
|
||||
_largs: &ProfileCommand,
|
||||
) -> Result<()> {
|
||||
info!("Starting profile at path {}...", self.profile.display());
|
||||
let ref profiles = Profiles::get().await?.0;
|
||||
let path = self.profile.canonicalize()?;
|
||||
let profile = profiles
|
||||
.get(&path)
|
||||
.ok_or(
|
||||
eyre::eyre!(
|
||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)"
|
||||
)
|
||||
)?;
|
||||
|
||||
let credentials = theseus::launcher::Credentials {
|
||||
id: self.id.clone(),
|
||||
username: self.name.clone(),
|
||||
access_token: self.token.clone(),
|
||||
};
|
||||
ensure!(
|
||||
profile::is_managed(&path).await?,
|
||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
||||
);
|
||||
|
||||
let mut proc = profile.run(&credentials).await?;
|
||||
profile.wait_for(&mut proc).await?;
|
||||
let id = future::ready(self.user.ok_or(()))
|
||||
.or_else(|_| async move {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
settings.default_user
|
||||
.ok_or(eyre::eyre!(
|
||||
"Could not find any users, please add one using the `user add` command."
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
let credentials = auth::refresh(id, false).await?;
|
||||
|
||||
let mut proc = profile::run(&path, &credentials).await?;
|
||||
profile::wait_for(&mut proc).await?;
|
||||
|
||||
success!("Process exited successfully!");
|
||||
Ok(())
|
||||
@@ -397,14 +411,14 @@ impl ProfileRun {
|
||||
}
|
||||
|
||||
impl ProfileCommand {
|
||||
pub async fn dispatch(&self, args: &crate::Args) -> Result<()> {
|
||||
match &self.action {
|
||||
ProfileSubcommand::Add(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Init(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::List(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Remove(ref cmd) => cmd.run(args, self).await,
|
||||
ProfileSubcommand::Run(ref cmd) => cmd.run(args, self).await,
|
||||
}
|
||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||
dispatch!(&self.action, (args, self) => {
|
||||
ProfileSubcommand::Add,
|
||||
ProfileSubcommand::Init,
|
||||
ProfileSubcommand::List,
|
||||
ProfileSubcommand::Remove,
|
||||
ProfileSubcommand::Run
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
theseus_cli/src/subcommands/user.rs
Normal file
178
theseus_cli/src/subcommands/user.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
//! User management subcommand
|
||||
use crate::util::{confirm_async, table};
|
||||
use eyre::Result;
|
||||
use paris::*;
|
||||
use tabled::Tabled;
|
||||
use theseus::prelude::*;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand, name = "user")]
|
||||
/// manage Minecraft accounts
|
||||
pub struct UserCommand {
|
||||
#[argh(subcommand)]
|
||||
action: UserSubcommand,
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
#[argh(subcommand)]
|
||||
pub enum UserSubcommand {
|
||||
Add(UserAdd),
|
||||
List(UserList),
|
||||
Remove(UserRemove),
|
||||
SetDefault(UserDefault),
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// add a new user to Theseus
|
||||
#[argh(subcommand, name = "add")]
|
||||
pub struct UserAdd {
|
||||
#[argh(option)]
|
||||
/// the browser to authenticate using
|
||||
browser: Option<webbrowser::Browser>,
|
||||
}
|
||||
|
||||
impl UserAdd {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Adding new user account to Theseus");
|
||||
info!("A browser window will now open, follow the login flow there.");
|
||||
|
||||
let (tx, rx) = oneshot::channel::<url::Url>();
|
||||
let flow = tokio::spawn(auth::authenticate(tx));
|
||||
|
||||
let url = rx.await?;
|
||||
match self.browser {
|
||||
Some(browser) => webbrowser::open_browser(browser, url.as_str()),
|
||||
None => webbrowser::open(url.as_str()),
|
||||
}?;
|
||||
|
||||
let credentials = flow.await??;
|
||||
State::sync().await?;
|
||||
success!("Logged in user {}.", credentials.username);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// list all known users
|
||||
#[argh(subcommand, name = "list")]
|
||||
pub struct UserList {}
|
||||
|
||||
#[derive(Tabled)]
|
||||
struct UserRow<'a> {
|
||||
username: &'a str,
|
||||
id: uuid::Uuid,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
impl<'a> UserRow<'a> {
|
||||
pub fn from(
|
||||
credentials: &'a Credentials,
|
||||
default: Option<uuid::Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
username: &credentials.username,
|
||||
id: credentials.id,
|
||||
default: Some(credentials.id) == default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserList {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
let state = State::get().await?;
|
||||
let default = state.settings.read().await.default_user;
|
||||
|
||||
let users = auth::users().await?;
|
||||
let rows = users.iter().map(|user| UserRow::from(user, default));
|
||||
|
||||
let table = table(rows);
|
||||
println!("{table}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// remove a user
|
||||
#[argh(subcommand, name = "remove")]
|
||||
pub struct UserRemove {
|
||||
/// the user to remove
|
||||
#[argh(positional)]
|
||||
user: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl UserRemove {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Removing user {}", self.user.as_hyphenated());
|
||||
|
||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||
if !auth::has_user(self.user).await? {
|
||||
warn!("Profile was not managed by Theseus!");
|
||||
} else {
|
||||
auth::remove_user(self.user).await?;
|
||||
State::sync().await?;
|
||||
success!("User removed!");
|
||||
}
|
||||
} else {
|
||||
error!("Aborted!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(argh::FromArgs, Debug)]
|
||||
/// set the default user
|
||||
#[argh(subcommand, name = "set-default")]
|
||||
pub struct UserDefault {
|
||||
/// the user to set as default
|
||||
#[argh(positional)]
|
||||
user: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl UserDefault {
|
||||
pub async fn run(
|
||||
&self,
|
||||
_args: &crate::Args,
|
||||
_largs: &UserCommand,
|
||||
) -> Result<()> {
|
||||
info!("Setting user {} as default", self.user.as_hyphenated());
|
||||
|
||||
// TODO: settings API
|
||||
let state: std::sync::Arc<State> = State::get().await?;
|
||||
let mut settings = state.settings.write().await;
|
||||
|
||||
if settings.default_user == Some(self.user) {
|
||||
warn!("User is already the default!");
|
||||
} else {
|
||||
settings.default_user = Some(self.user);
|
||||
success!("User set as default!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl UserCommand {
|
||||
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||
dispatch!(&self.action, (args, self) => {
|
||||
UserSubcommand::Add,
|
||||
UserSubcommand::List,
|
||||
UserSubcommand::Remove,
|
||||
UserSubcommand::SetDefault
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
use eyre::Result;
|
||||
use std::{borrow::Cow, path::Path};
|
||||
use tabled::{Table, Tabled};
|
||||
|
||||
// TODO: make primarily async to avoid copies
|
||||
|
||||
@@ -56,7 +57,11 @@ pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
|
||||
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
||||
}
|
||||
|
||||
// Table display helpers
|
||||
// Table helpers
|
||||
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
|
||||
Table::new(rows).with(tabled::Style::psql())
|
||||
}
|
||||
|
||||
pub fn table_path_display(path: &Path) -> String {
|
||||
let mut res = path.display().to_string();
|
||||
|
||||
@@ -67,6 +72,20 @@ pub fn table_path_display(path: &Path) -> String {
|
||||
res
|
||||
}
|
||||
|
||||
// Dispatch macros
|
||||
macro_rules! dispatch {
|
||||
($on:expr, $args:tt => {$($option:path),+}) => {
|
||||
match $on {
|
||||
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
|
||||
}
|
||||
};
|
||||
|
||||
(@apply $cmd:expr => ($($args:expr),*)) => {{
|
||||
use tracing_futures::WithSubscriber;
|
||||
$cmd.run($($args),*).with_current_subscriber().await
|
||||
}};
|
||||
}
|
||||
|
||||
// Internal helpers
|
||||
fn print_prompt(prompt: &str) {
|
||||
println!(
|
||||
|
||||
Reference in New Issue
Block a user