diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index bcd9480b81..a2615552ff 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,17 +1,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
index ecea560ce5..33af8a01da 100644
--- a/.idea/modules.xml
+++ b/.idea/modules.xml
@@ -1,15 +1,14 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/app/src/api/utils.rs b/apps/app/src/api/utils.rs
index a2b013ae3c..64dbfb0872 100644
--- a/apps/app/src/api/utils.rs
+++ b/apps/app/src/api/utils.rs
@@ -9,7 +9,10 @@ use theseus::{
use crate::api::{Result, TheseusSerializableError};
use dashmap::DashMap;
use std::path::{Path, PathBuf};
+use theseus::emit_warning;
use theseus::prelude::canonicalize;
+use theseus::profile::{self, QuickPlayType};
+use tracing;
use url::Url;
pub fn init() -> tauri::plugin::TauriPlugin {
@@ -162,6 +165,130 @@ pub async fn handle_command(command: String) -> Result<()> {
Ok(theseus::handler::parse_and_emit_command(&command).await?)
}
+/// Parse a Vec (as provided by std::env::args_os()) or Vec (as provided by tauri single-instance args)
+/// and return the value for the first `--launch-profile` occurrence, if any.
+pub fn parse_launch_profile_from_args>(
+ args: Vec,
+) -> Option {
+ let mut iter = args.into_iter();
+ // Skip executable name
+ let _ = iter.next();
+
+ while let Some(arg) = iter.next() {
+ let s = arg.as_ref().to_string_lossy();
+ if let Some(rest) = s.strip_prefix("--launch-profile=") {
+ let val = rest.to_string();
+ if !val.trim().is_empty() {
+ return Some(val);
+ }
+ } else if s == "--launch-profile" {
+ if let Some(next) = iter.next() {
+ let val = next.as_ref().to_string_lossy().to_string();
+ if !val.trim().is_empty() {
+ return Some(val);
+ }
+ } else {
+ // flag present but no value
+ return None;
+ }
+ }
+ }
+
+ None
+}
+
+/// Handle launching a profile by display name. Waits for state/profile readiness via underlying
+/// profile APIs and then re-uses the existing `profile::run` pipeline.
+pub async fn handle_launch_profile(profile_name: String) -> Result<()> {
+ tracing::info!(
+ "Requested auto-launch profile from CLI: '{}'",
+ profile_name
+ );
+
+ let name = profile_name.trim();
+ if name.is_empty() {
+ let _ = emit_warning("Empty profile name provided to --launch-profile")
+ .await;
+ tracing::warn!("Empty profile name provided to --launch-profile");
+ return Ok(());
+ }
+
+ // Retrieve profiles (this will await state readiness internally)
+ let profiles = match profile::list().await {
+ Ok(p) => p,
+ Err(e) => {
+ let msg = format!("Failed to read profiles for auto-launch: {e}");
+ let _ = emit_warning(&msg).await;
+ tracing::error!("{msg}");
+ return Ok(());
+ }
+ };
+
+ // Matching strategy: exact (case-sensitive), exact (case-insensitive), contains (case-insensitive)
+ let mut matches: Vec<_> =
+ profiles.iter().filter(|p| p.name == name).collect();
+
+ if matches.is_empty() {
+ let low = name.to_lowercase();
+ matches = profiles
+ .iter()
+ .filter(|p| p.name.to_lowercase() == low)
+ .collect();
+ }
+
+ if matches.is_empty() {
+ let low = name.to_lowercase();
+ matches = profiles
+ .iter()
+ .filter(|p| p.name.to_lowercase().contains(&low))
+ .collect();
+ }
+
+ if matches.is_empty() {
+ let msg = format!("Profile '{name}' not found");
+ let _ = emit_warning(&msg).await;
+ tracing::warn!("{msg}");
+ return Ok(());
+ }
+
+ if matches.len() > 1 {
+ // Ambiguous
+ let candidates = matches
+ .iter()
+ .map(|p| format!("{} ({})", p.name, p.path))
+ .collect::>()
+ .join(", ");
+ let msg = format!(
+ "Multiple profiles match '{name}'. Candidates: {candidates}"
+ );
+ let _ = emit_warning(&msg).await;
+ tracing::warn!("{msg}");
+ return Ok(());
+ }
+
+ let profile = matches.into_iter().next().unwrap();
+
+ tracing::info!(
+ "Auto-launch: launching profile '{}' (path='{}')",
+ profile.name,
+ profile.path
+ );
+
+ match profile::run(&profile.path, QuickPlayType::None).await {
+ Ok(_proc) => {
+ tracing::info!("Started launch for profile '{}'", profile.name)
+ }
+ Err(e) => {
+ let msg =
+ format!("Failed to launch profile '{}' : {e}", profile.name);
+ let _ = emit_warning(&msg).await;
+ tracing::error!("{msg}");
+ }
+ }
+
+ Ok(())
+}
+
// Remove when (and if) https://github.com/tauri-apps/tauri/issues/12022 is implemented
pub(crate) fn tauri_convert_file_src(path: &Path) -> Result {
#[cfg(any(windows, target_os = "android"))]
diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs
index c77a1ac730..9185f3822a 100644
--- a/apps/app/src/main.rs
+++ b/apps/app/src/main.rs
@@ -40,6 +40,23 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
app.fs_scope()
.allow_directory(state.directories.profiles_dir(), true)?;
+ // Check for CLI --launch-profile when the app finishes initializing state.
+ // We spawn the handler so this command does not block the initialize_state caller.
+ if let Some(profile_name) =
+ crate::api::utils::parse_launch_profile_from_args(
+ std::env::args_os().collect(),
+ )
+ {
+ let name = profile_name;
+ tracing::info!("Detected --launch-profile on startup: {}", name);
+ tauri::async_runtime::spawn(async move {
+ if let Err(e) = crate::api::utils::handle_launch_profile(name).await
+ {
+ tracing::error!("Auto-launch profile failed: {:?}", e);
+ }
+ });
+ }
+
Ok(())
}
@@ -151,7 +168,17 @@ fn main() {
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
- if let Some(payload) = args.get(1) {
+ // First, check if a second-instance invoked with --launch-profile
+ let args_vec = args.clone();
+ if let Some(profile_name) = api::utils::parse_launch_profile_from_args(args_vec) {
+ tracing::info!("Received single-instance --launch-profile request: {}", profile_name);
+ let name = profile_name;
+ tauri::async_runtime::spawn(async move {
+ if let Err(e) = api::utils::handle_launch_profile(name).await {
+ tracing::error!("Auto-launch profile (single-instance) failed: {:?}", e);
+ }
+ });
+ } else if let Some(payload) = args.get(1) {
tracing::info!("Handling deep link from arg {payload}");
let payload = payload.clone();
tauri::async_runtime::spawn(api::utils::handle_command(
diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json
index c1b9d08003..2633a33ccf 100644
--- a/apps/app/tauri.conf.json
+++ b/apps/app/tauri.conf.json
@@ -10,7 +10,7 @@
"active": true,
"category": "Game",
"copyright": "",
- "targets": "all",
+ "targets": ["nsis"],
"externalBin": [],
"icon": [
"icons/128x128.png",
diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md
index 681311eb9c..ceb2b988dc 120000
--- a/apps/frontend/AGENTS.md
+++ b/apps/frontend/AGENTS.md
@@ -1 +1 @@
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
diff --git a/packages/api-client/AGENTS.md b/packages/api-client/AGENTS.md
index 681311eb9c..ceb2b988dc 120000
--- a/packages/api-client/AGENTS.md
+++ b/packages/api-client/AGENTS.md
@@ -1 +1 @@
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md
diff --git a/packages/app-lib/src/lib.rs b/packages/app-lib/src/lib.rs
index 1bb282bc2c..509807b024 100644
--- a/packages/app-lib/src/lib.rs
+++ b/packages/app-lib/src/lib.rs
@@ -21,7 +21,7 @@ pub use api::*;
pub use error::*;
pub use event::{
EventState, LoadingBar, LoadingBarType, emit::emit_loading,
- emit::init_loading,
+ emit::emit_warning, emit::init_loading,
};
pub use logger::start_logger;
pub use state::State;
diff --git a/packages/ui/AGENTS.md b/packages/ui/AGENTS.md
index 681311eb9c..ceb2b988dc 120000
--- a/packages/ui/AGENTS.md
+++ b/packages/ui/AGENTS.md
@@ -1 +1 @@
-CLAUDE.md
\ No newline at end of file
+CLAUDE.md