From 264ea7900d2f2606693b7cf01ddf95cd4bcc8a59 Mon Sep 17 00:00:00 2001
From: amnesiaof
Date: Thu, 18 Jun 2026 20:09:59 +0300
Subject: [PATCH 1/4] feat(app): auto-update linked modpacks before launch and
in background
---
.../ui/instance_settings/GeneralSettings.vue | 37 +++
apps/app-frontend/src/helpers/profile.ts | 5 +
apps/app-frontend/src/helpers/types.d.ts | 1 +
.../app-frontend/src/locales/en-US/index.json | 6 +
apps/app/build.rs | 1 +
apps/app/src/api/profile.rs | 18 ++
apps/app/src/main.rs | 13 ++
apps/frontend/AGENTS.md | 1 -
packages/api-client/AGENTS.md | 2 +-
...63a1388ef538e4b80680bce4b668b182b4f42.json | 12 -
...d0494058373b484ca8ac44a02edb7a4cc444.json} | 10 +-
...fa00083022e0a4b0437a3e613c84e76f2d5c.json} | 10 +-
...b207c848c1d4cf7c30243b5c2cfd6a6f11597.json | 12 +
...20260618000000_add-auto-update-modpack.sql | 1 +
packages/app-lib/src/api/profile/create.rs | 1 +
packages/app-lib/src/api/profile/mod.rs | 142 ++++++++++++
.../app-lib/src/state/legacy_converter.rs | 1 +
packages/app-lib/src/state/profiles.rs | 18 +-
packages/assets/generated-icons.ts | 217 +++++++++---------
19 files changed, 377 insertions(+), 131 deletions(-)
delete mode 100644 packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
rename packages/app-lib/.sqlx/{query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json => query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json} (93%)
rename packages/app-lib/.sqlx/{query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json => query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json} (93%)
create mode 100644 packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
create mode 100644 packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
index e30bead729..85a2df3500 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
@@ -9,6 +9,7 @@ import {
injectNotificationManager,
OverflowMenu,
StyledInput,
+ Toggle,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -44,6 +45,21 @@ const releaseChannelDisabledItems = computed(() =>
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
)
+const autoUpdateModpack = ref(instance.value.auto_update_modpack ?? false)
+
+const hasLinkedModpack = computed(() => !!instance.value.linked_data)
+
+watch(autoUpdateModpack, async (value) => {
+ await edit(instance.value.path, { auto_update_modpack: value }).catch(handleError)
+})
+
+watch(
+ () => instance.value.auto_update_modpack,
+ (val) => {
+ autoUpdateModpack.value = val ?? false
+ },
+)
+
const newCategoryInput = ref('')
const installing = computed(() => instance.value.install_stage !== 'installed')
@@ -263,6 +279,15 @@ const messages = defineMessages({
id: 'instance.settings.tabs.general.update-channel.select',
defaultMessage: 'Select update channel',
},
+ autoUpdateModpack: {
+ id: 'instance.settings.tabs.general.auto-update-modpack',
+ defaultMessage: 'Auto-update modpack',
+ },
+ autoUpdateModpackDescription: {
+ id: 'instance.settings.tabs.general.auto-update-modpack.description',
+ defaultMessage:
+ 'Automatically update this modpack to the latest version. Updates are checked before launching and periodically in the background.',
+ },
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
@@ -409,6 +434,18 @@ const messages = defineMessages({
+
+
+
+ {{ formatMessage(messages.autoUpdateModpack) }}
+
+
+ {{ formatMessage(messages.autoUpdateModpackDescription) }}
+
+
+
+
+
{{ formatMessage(messages.deleteInstance) }}
diff --git a/apps/app-frontend/src/helpers/profile.ts b/apps/app-frontend/src/helpers/profile.ts
index be3b62c6ef..d4ffcd4707 100644
--- a/apps/app-frontend/src/helpers/profile.ts
+++ b/apps/app-frontend/src/helpers/profile.ts
@@ -242,6 +242,11 @@ export async function update_managed_modrinth_version(
})
}
+// Auto-update all modpacks with auto_update_modpack enabled
+export async function auto_update_all_modpacks(): Promise {
+ return await invoke('plugin:profile|profile_auto_update_all_modpacks')
+}
+
// Repair a managed Modrinth profile
export async function update_repair_modrinth(path: string): Promise {
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index 143997702f..f659f2d9cc 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -31,6 +31,7 @@ export type GameInstance = {
force_fullscreen?: boolean
game_resolution?: [number, number]
hooks: Hooks
+ auto_update_modpack?: boolean
}
type InstallStage =
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index cf98dc3a76..cb3cb7e424 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -665,6 +665,12 @@
"instance.settings.tabs.general": {
"message": "General"
},
+ "instance.settings.tabs.general.auto-update-modpack": {
+ "message": "Auto-update modpack"
+ },
+ "instance.settings.tabs.general.auto-update-modpack.description": {
+ "message": "Automatically update this modpack to the latest version. Updates are checked before launching and periodically in the background."
+ },
"instance.settings.tabs.general.delete": {
"message": "Delete instance"
},
diff --git a/apps/app/build.rs b/apps/app/build.rs
index 1f78fcce47..58f683bdff 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -196,6 +196,7 @@ fn main() {
"profile_edit_icon",
"profile_export_mrpack",
"profile_get_pack_export_candidates",
+ "profile_auto_update_all_modpacks",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs
index ab41af5887..cf8ab54e0e 100644
--- a/apps/app/src/api/profile.rs
+++ b/apps/app/src/api/profile.rs
@@ -43,6 +43,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
profile_edit_icon,
profile_export_mrpack,
profile_get_pack_export_candidates,
+ profile_auto_update_all_modpacks,
])
.build()
}
@@ -432,6 +433,20 @@ pub struct EditProfile {
)]
pub game_resolution: Option
+
+
+
+ {{ formatMessage(messages.autoUpdateModpack) }}
+
+
+ {{ formatMessage(messages.autoUpdateModpackDescription) }}
+
+
+
+
+
{{ formatMessage(messages.deleteInstance) }}
diff --git a/apps/app-frontend/src/helpers/profile.ts b/apps/app-frontend/src/helpers/profile.ts
index be3b62c6ef..d4ffcd4707 100644
--- a/apps/app-frontend/src/helpers/profile.ts
+++ b/apps/app-frontend/src/helpers/profile.ts
@@ -242,6 +242,11 @@ export async function update_managed_modrinth_version(
})
}
+// Auto-update all modpacks with auto_update_modpack enabled
+export async function auto_update_all_modpacks(): Promise {
+ return await invoke('plugin:profile|profile_auto_update_all_modpacks')
+}
+
// Repair a managed Modrinth profile
export async function update_repair_modrinth(path: string): Promise {
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index 143997702f..f659f2d9cc 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -31,6 +31,7 @@ export type GameInstance = {
force_fullscreen?: boolean
game_resolution?: [number, number]
hooks: Hooks
+ auto_update_modpack?: boolean
}
type InstallStage =
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index cf98dc3a76..cb3cb7e424 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -665,6 +665,12 @@
"instance.settings.tabs.general": {
"message": "General"
},
+ "instance.settings.tabs.general.auto-update-modpack": {
+ "message": "Auto-update modpack"
+ },
+ "instance.settings.tabs.general.auto-update-modpack.description": {
+ "message": "Automatically update this modpack to the latest version. Updates are checked before launching and periodically in the background."
+ },
"instance.settings.tabs.general.delete": {
"message": "Delete instance"
},
diff --git a/apps/app/build.rs b/apps/app/build.rs
index 1f78fcce47..58f683bdff 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -196,6 +196,7 @@ fn main() {
"profile_edit_icon",
"profile_export_mrpack",
"profile_get_pack_export_candidates",
+ "profile_auto_update_all_modpacks",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs
index ab41af5887..cf8ab54e0e 100644
--- a/apps/app/src/api/profile.rs
+++ b/apps/app/src/api/profile.rs
@@ -43,6 +43,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
profile_edit_icon,
profile_export_mrpack,
profile_get_pack_export_candidates,
+ profile_auto_update_all_modpacks,
])
.build()
}
@@ -432,6 +433,20 @@ pub struct EditProfile {
)]
pub game_resolution: Option>,
pub hooks: Option,
+
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ with = "serde_with::rust::double_option"
+ )]
+ pub auto_update_modpack: Option>,
+}
+
+// Auto-update all modpacks with auto_update_modpack enabled
+// invoke('plugin:profile|profile_auto_update_all_modpacks')
+#[tauri::command]
+pub async fn profile_auto_update_all_modpacks() -> Result<()> {
+ Ok(profile::auto_update_all_modpacks().await?)
}
// Edits a profile
@@ -480,6 +495,9 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
if let Some(hooks) = edit_profile.hooks.clone() {
prof.hooks = hooks;
}
+ if let Some(auto_update_modpack) = edit_profile.auto_update_modpack {
+ prof.auto_update_modpack = auto_update_modpack;
+ }
prof.modified = chrono::Utc::now();
diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs
index 33d2da8796..8674e87da6 100644
--- a/apps/app/src/main.rs
+++ b/apps/app/src/main.rs
@@ -226,6 +226,19 @@ fn main() {
tracing::warn!("Failed to set window shadow: {e}");
}
+ // Background task: auto-update modpacks every 60 minutes
+ tauri::async_runtime::spawn(async move {
+ // Wait for state to initialize before first check
+ tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+ loop {
+ tracing::debug!("Running background modpack auto-update check");
+ if let Err(e) = theseus::profile::auto_update_all_modpacks().await {
+ tracing::warn!("Background modpack auto-update failed: {e}");
+ }
+ tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
+ }
+ });
+
Ok(())
});
diff --git a/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json b/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
deleted file mode 100644
index acc106f94d..0000000000
--- a/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 29
- },
- "nullable": []
- },
- "hash": "22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42"
-}
diff --git a/packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json b/packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
similarity index 93%
rename from packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json
rename to packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
index 6f16a301a4..47a2543c4f 100644
--- a/packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json
+++ b/packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
+ "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n auto_update_modpack\n FROM profiles\n WHERE 1=$1",
"describe": {
"columns": [
{
@@ -147,6 +147,11 @@
"name": "override_hook_post_exit",
"ordinal": 28,
"type_info": "Text"
+ },
+ {
+ "name": "auto_update_modpack",
+ "ordinal": 29,
+ "type_info": "Integer"
}
],
"parameters": {
@@ -181,8 +186,9 @@
true,
true,
true,
+ true,
true
]
},
- "hash": "be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5"
+ "hash": "6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444"
}
diff --git a/packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json b/packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
similarity index 93%
rename from packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json
rename to packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
index 775055ea41..2e1bf890b2 100644
--- a/packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json
+++ b/packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
+ "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n auto_update_modpack\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
"describe": {
"columns": [
{
@@ -147,6 +147,11 @@
"name": "override_hook_post_exit",
"ordinal": 28,
"type_info": "Text"
+ },
+ {
+ "name": "auto_update_modpack",
+ "ordinal": 29,
+ "type_info": "Integer"
}
],
"parameters": {
@@ -181,8 +186,9 @@
true,
true,
true,
+ true,
true
]
},
- "hash": "de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97"
+ "hash": "df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c"
}
diff --git a/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json b/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
new file mode 100644
index 0000000000..30621991bc
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version,\n auto_update_modpack\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29,\n $30\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29,\n auto_update_modpack = $30\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 30
+ },
+ "nullable": []
+ },
+ "hash": "f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597"
+}
diff --git a/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql b/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
new file mode 100644
index 0000000000..a6835f5056
--- /dev/null
+++ b/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
@@ -0,0 +1 @@
+ALTER TABLE profiles ADD COLUMN auto_update_modpack INTEGER NULL;
diff --git a/packages/app-lib/src/api/profile/create.rs b/packages/app-lib/src/api/profile/create.rs
index c1a1491c1b..21d8f31548 100644
--- a/packages/app-lib/src/api/profile/create.rs
+++ b/packages/app-lib/src/api/profile/create.rs
@@ -102,6 +102,7 @@ pub async fn profile_create(
wrapper: None,
post_exit: None,
},
+ auto_update_modpack: None,
};
let result = async {
diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs
index a7f2f113ac..b58ba3d537 100644
--- a/packages/app-lib/src/api/profile/mod.rs
+++ b/packages/app-lib/src/api/profile/mod.rs
@@ -819,6 +819,37 @@ async fn run_credentials(
))
})?;
+ // Auto-update modpack if enabled and an update is available
+ if profile.auto_update_modpack == Some(true)
+ && profile.linked_data.is_some()
+ && let Ok(Some(info)) = crate::state::get_linked_modpack_info(
+ &profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await
+ && info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ if let Err(e) = update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await
+ {
+ tracing::warn!(
+ "Auto-update of modpack '{}' failed: {e}",
+ profile.name,
+ );
+ }
+ }
+
let pre_launch_hooks = profile
.hooks
.pre_launch
@@ -1178,6 +1209,117 @@ pub async fn add_all_recursive_folder_paths(
Ok(())
}
+/// Check for a modpack update for a single profile and apply it if auto-update is enabled.
+/// Returns `true` if an update was applied.
+#[tracing::instrument]
+pub async fn auto_update_modpack_if_needed(path: &str) -> crate::Result {
+ let state = State::get().await?;
+
+ let Some(profile) = get(path).await? else {
+ return Ok(false);
+ };
+
+ if profile.auto_update_modpack != Some(true)
+ || profile.linked_data.is_none()
+ {
+ return Ok(false);
+ }
+
+ let Some(info) = crate::state::get_linked_modpack_info(
+ &profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await?
+ else {
+ return Ok(false);
+ };
+
+ if info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await?;
+ return Ok(true);
+ }
+
+ Ok(false)
+}
+
+/// Check all profiles for modpack updates and apply them automatically
+/// where `auto_update_modpack` is enabled.
+#[tracing::instrument]
+pub async fn auto_update_all_modpacks() -> crate::Result<()> {
+ let state = State::get().await?;
+ let profiles = Profile::get_all(&state.pool).await?;
+
+ for profile in &profiles {
+ if profile.auto_update_modpack != Some(true)
+ || profile.linked_data.is_none()
+ {
+ continue;
+ }
+
+ // Skip if the profile is currently running
+ let is_running = state
+ .process_manager
+ .get_all()
+ .iter()
+ .any(|p| p.profile_path == profile.path);
+
+ if is_running {
+ tracing::debug!(
+ "Skipping auto-update for '{}' — profile is running",
+ profile.name,
+ );
+ continue;
+ }
+
+ let Ok(Some(info)) = crate::state::get_linked_modpack_info(
+ profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await
+ else {
+ continue;
+ };
+
+ if info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Background auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ if let Err(e) = update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await
+ {
+ tracing::warn!(
+ "Background auto-update of modpack '{}' failed: {e}",
+ profile.name,
+ );
+ }
+ }
+ }
+
+ Ok(())
+}
+
pub fn sanitize_profile_name(input: &str) -> String {
input.replace(
['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>', '!'],
diff --git a/packages/app-lib/src/state/legacy_converter.rs b/packages/app-lib/src/state/legacy_converter.rs
index 9404c6ae0f..e765cf267c 100644
--- a/packages/app-lib/src/state/legacy_converter.rs
+++ b/packages/app-lib/src/state/legacy_converter.rs
@@ -376,6 +376,7 @@ where
.and_then(|x| x.wrapper.clone()),
post_exit: profile.hooks.and_then(|x| x.post_exit),
},
+ auto_update_modpack: None,
}
.upsert(exec)
.await?;
diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs
index c30412af22..c2e5bd11a4 100644
--- a/packages/app-lib/src/state/profiles.rs
+++ b/packages/app-lib/src/state/profiles.rs
@@ -57,6 +57,7 @@ pub struct Profile {
pub force_fullscreen: Option,
pub game_resolution: Option,
pub hooks: Hooks,
+ pub auto_update_modpack: Option,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
@@ -315,6 +316,7 @@ struct ProfileQueryResult {
override_hook_post_exit: Option,
protocol_version: Option,
launcher_feature_version: String,
+ auto_update_modpack: Option,
}
impl TryFrom for Profile {
@@ -387,6 +389,7 @@ impl TryFrom for Profile {
wrapper: x.override_hook_wrapper,
post_exit: x.override_hook_post_exit,
},
+ auto_update_modpack: x.auto_update_modpack.map(|x| x == 1),
})
}
}
@@ -406,7 +409,8 @@ macro_rules! select_profiles_with_predicate {
override_java_path,
json(override_extra_launch_args) as "override_extra_launch_args!: serde_json::Value", json(override_custom_env_vars) as "override_custom_env_vars!: serde_json::Value",
override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,
- override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit
+ override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,
+ auto_update_modpack
FROM profiles
"#
+ $predicate,
@@ -527,7 +531,8 @@ impl Profile {
override_java_path, override_extra_launch_args, override_custom_env_vars,
override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,
override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,
- protocol_version, launcher_feature_version
+ protocol_version, launcher_feature_version,
+ auto_update_modpack
)
VALUES (
$1, $2, $3, $4,
@@ -539,7 +544,8 @@ impl Profile {
$18, jsonb($19), jsonb($20),
$21, $22, $23, $24,
$25, $26, $27,
- $28, $29
+ $28, $29,
+ $30
)
ON CONFLICT (path) DO UPDATE SET
install_stage = $2,
@@ -577,7 +583,8 @@ impl Profile {
override_hook_post_exit = $27,
protocol_version = $28,
- launcher_feature_version = $29
+ launcher_feature_version = $29,
+ auto_update_modpack = $30
",
self.path,
install_stage,
@@ -607,7 +614,8 @@ impl Profile {
self.hooks.wrapper,
self.hooks.post_exit,
self.protocol_version,
- launcher_feature_version
+ launcher_feature_version,
+ self.auto_update_modpack
)
.execute(exec)
.await?;
From 166c53bcbe18b9655bc45eb8984acba155d1b6fb Mon Sep 17 00:00:00 2001
From: amnesiaof
Date: Thu, 18 Jun 2026 20:21:37 +0300
Subject: [PATCH 3/4] revert
---
apps/frontend/AGENTS.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md
index e69de29bb2..ceb2b988dc 120000
--- a/apps/frontend/AGENTS.md
+++ b/apps/frontend/AGENTS.md
@@ -0,0 +1 @@
+CLAUDE.md
From a5385ff44bb70419996eed5e2bf296674ea3452c Mon Sep 17 00:00:00 2001
From: amnesiaof
Date: Thu, 18 Jun 2026 20:26:47 +0300
Subject: [PATCH 4/4] feat(app): auto-update linked modpacks before launch and
in background
---
.../ui/instance_settings/GeneralSettings.vue | 37 +++++
apps/app-frontend/src/helpers/profile.ts | 5 +
apps/app-frontend/src/helpers/types.d.ts | 1 +
.../app-frontend/src/locales/en-US/index.json | 6 +
apps/app/build.rs | 1 +
apps/app/src/api/profile.rs | 18 +++
apps/app/src/main.rs | 13 ++
...63a1388ef538e4b80680bce4b668b182b4f42.json | 12 --
...d0494058373b484ca8ac44a02edb7a4cc444.json} | 10 +-
...fa00083022e0a4b0437a3e613c84e76f2d5c.json} | 10 +-
...b207c848c1d4cf7c30243b5c2cfd6a6f11597.json | 12 ++
...20260618000000_add-auto-update-modpack.sql | 1 +
packages/app-lib/src/api/profile/create.rs | 1 +
packages/app-lib/src/api/profile/mod.rs | 142 ++++++++++++++++++
.../app-lib/src/state/legacy_converter.rs | 1 +
packages/app-lib/src/state/profiles.rs | 18 ++-
16 files changed, 267 insertions(+), 21 deletions(-)
delete mode 100644 packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
rename packages/app-lib/.sqlx/{query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json => query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json} (93%)
rename packages/app-lib/.sqlx/{query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json => query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json} (93%)
create mode 100644 packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
create mode 100644 packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
diff --git a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
index e30bead729..85a2df3500 100644
--- a/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
+++ b/apps/app-frontend/src/components/ui/instance_settings/GeneralSettings.vue
@@ -9,6 +9,7 @@ import {
injectNotificationManager,
OverflowMenu,
StyledInput,
+ Toggle,
useVIntl,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
@@ -44,6 +45,21 @@ const releaseChannelDisabledItems = computed(() =>
savingReleaseChannel.value ? [...releaseChannelOptions] : [],
)
+const autoUpdateModpack = ref(instance.value.auto_update_modpack ?? false)
+
+const hasLinkedModpack = computed(() => !!instance.value.linked_data)
+
+watch(autoUpdateModpack, async (value) => {
+ await edit(instance.value.path, { auto_update_modpack: value }).catch(handleError)
+})
+
+watch(
+ () => instance.value.auto_update_modpack,
+ (val) => {
+ autoUpdateModpack.value = val ?? false
+ },
+)
+
const newCategoryInput = ref('')
const installing = computed(() => instance.value.install_stage !== 'installed')
@@ -263,6 +279,15 @@ const messages = defineMessages({
id: 'instance.settings.tabs.general.update-channel.select',
defaultMessage: 'Select update channel',
},
+ autoUpdateModpack: {
+ id: 'instance.settings.tabs.general.auto-update-modpack',
+ defaultMessage: 'Auto-update modpack',
+ },
+ autoUpdateModpackDescription: {
+ id: 'instance.settings.tabs.general.auto-update-modpack.description',
+ defaultMessage:
+ 'Automatically update this modpack to the latest version. Updates are checked before launching and periodically in the background.',
+ },
deleteInstance: {
id: 'instance.settings.tabs.general.delete',
defaultMessage: 'Delete instance',
@@ -409,6 +434,18 @@ const messages = defineMessages({
+
+
+
+ {{ formatMessage(messages.autoUpdateModpack) }}
+
+
+ {{ formatMessage(messages.autoUpdateModpackDescription) }}
+
+
+
+
+
{{ formatMessage(messages.deleteInstance) }}
diff --git a/apps/app-frontend/src/helpers/profile.ts b/apps/app-frontend/src/helpers/profile.ts
index be3b62c6ef..d4ffcd4707 100644
--- a/apps/app-frontend/src/helpers/profile.ts
+++ b/apps/app-frontend/src/helpers/profile.ts
@@ -242,6 +242,11 @@ export async function update_managed_modrinth_version(
})
}
+// Auto-update all modpacks with auto_update_modpack enabled
+export async function auto_update_all_modpacks(): Promise {
+ return await invoke('plugin:profile|profile_auto_update_all_modpacks')
+}
+
// Repair a managed Modrinth profile
export async function update_repair_modrinth(path: string): Promise {
return await invoke('plugin:profile|profile_repair_managed_modrinth', { path })
diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts
index 143997702f..f659f2d9cc 100644
--- a/apps/app-frontend/src/helpers/types.d.ts
+++ b/apps/app-frontend/src/helpers/types.d.ts
@@ -31,6 +31,7 @@ export type GameInstance = {
force_fullscreen?: boolean
game_resolution?: [number, number]
hooks: Hooks
+ auto_update_modpack?: boolean
}
type InstallStage =
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index cf98dc3a76..cb3cb7e424 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -665,6 +665,12 @@
"instance.settings.tabs.general": {
"message": "General"
},
+ "instance.settings.tabs.general.auto-update-modpack": {
+ "message": "Auto-update modpack"
+ },
+ "instance.settings.tabs.general.auto-update-modpack.description": {
+ "message": "Automatically update this modpack to the latest version. Updates are checked before launching and periodically in the background."
+ },
"instance.settings.tabs.general.delete": {
"message": "Delete instance"
},
diff --git a/apps/app/build.rs b/apps/app/build.rs
index 1f78fcce47..58f683bdff 100644
--- a/apps/app/build.rs
+++ b/apps/app/build.rs
@@ -196,6 +196,7 @@ fn main() {
"profile_edit_icon",
"profile_export_mrpack",
"profile_get_pack_export_candidates",
+ "profile_auto_update_all_modpacks",
])
.default_permission(
DefaultPermissionRule::AllowAllCommands,
diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs
index ab41af5887..cf8ab54e0e 100644
--- a/apps/app/src/api/profile.rs
+++ b/apps/app/src/api/profile.rs
@@ -43,6 +43,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
profile_edit_icon,
profile_export_mrpack,
profile_get_pack_export_candidates,
+ profile_auto_update_all_modpacks,
])
.build()
}
@@ -432,6 +433,20 @@ pub struct EditProfile {
)]
pub game_resolution: Option>,
pub hooks: Option,
+
+ #[serde(
+ default,
+ skip_serializing_if = "Option::is_none",
+ with = "serde_with::rust::double_option"
+ )]
+ pub auto_update_modpack: Option>,
+}
+
+// Auto-update all modpacks with auto_update_modpack enabled
+// invoke('plugin:profile|profile_auto_update_all_modpacks')
+#[tauri::command]
+pub async fn profile_auto_update_all_modpacks() -> Result<()> {
+ Ok(profile::auto_update_all_modpacks().await?)
}
// Edits a profile
@@ -480,6 +495,9 @@ pub async fn profile_edit(path: &str, edit_profile: EditProfile) -> Result<()> {
if let Some(hooks) = edit_profile.hooks.clone() {
prof.hooks = hooks;
}
+ if let Some(auto_update_modpack) = edit_profile.auto_update_modpack {
+ prof.auto_update_modpack = auto_update_modpack;
+ }
prof.modified = chrono::Utc::now();
diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs
index 33d2da8796..8674e87da6 100644
--- a/apps/app/src/main.rs
+++ b/apps/app/src/main.rs
@@ -226,6 +226,19 @@ fn main() {
tracing::warn!("Failed to set window shadow: {e}");
}
+ // Background task: auto-update modpacks every 60 minutes
+ tauri::async_runtime::spawn(async move {
+ // Wait for state to initialize before first check
+ tokio::time::sleep(std::time::Duration::from_secs(30)).await;
+ loop {
+ tracing::debug!("Running background modpack auto-update check");
+ if let Err(e) = theseus::profile::auto_update_all_modpacks().await {
+ tracing::warn!("Background modpack auto-update failed: {e}");
+ }
+ tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
+ }
+ });
+
Ok(())
});
diff --git a/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json b/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
deleted file mode 100644
index acc106f94d..0000000000
--- a/packages/app-lib/.sqlx/query-22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "SQLite",
- "query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Right": 29
- },
- "nullable": []
- },
- "hash": "22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42"
-}
diff --git a/packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json b/packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
similarity index 93%
rename from packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json
rename to packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
index 6f16a301a4..47a2543c4f 100644
--- a/packages/app-lib/.sqlx/query-be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5.json
+++ b/packages/app-lib/.sqlx/query-6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE 1=$1",
+ "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n auto_update_modpack\n FROM profiles\n WHERE 1=$1",
"describe": {
"columns": [
{
@@ -147,6 +147,11 @@
"name": "override_hook_post_exit",
"ordinal": 28,
"type_info": "Text"
+ },
+ {
+ "name": "auto_update_modpack",
+ "ordinal": 29,
+ "type_info": "Integer"
}
],
"parameters": {
@@ -181,8 +186,9 @@
true,
true,
true,
+ true,
true
]
},
- "hash": "be21bfd7c36fd8c69dfffe0299eeb7e57590dc75b721dd823d00572f428d0bc5"
+ "hash": "6641176d0102e8219a42d2207a06d0494058373b484ca8ac44a02edb7a4cc444"
}
diff --git a/packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json b/packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
similarity index 93%
rename from packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json
rename to packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
index 775055ea41..2e1bf890b2 100644
--- a/packages/app-lib/.sqlx/query-de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97.json
+++ b/packages/app-lib/.sqlx/query-df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c.json
@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
- "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
+ "query": "\n SELECT\n path, install_stage, launcher_feature_version, name, icon_path,\n game_version, protocol_version, mod_loader, mod_loader_version,\n json(groups) as \"groups!: serde_json::Value\",\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path,\n json(override_extra_launch_args) as \"override_extra_launch_args!: serde_json::Value\", json(override_custom_env_vars) as \"override_custom_env_vars!: serde_json::Value\",\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n auto_update_modpack\n FROM profiles\n WHERE path IN (SELECT value FROM json_each($1))",
"describe": {
"columns": [
{
@@ -147,6 +147,11 @@
"name": "override_hook_post_exit",
"ordinal": 28,
"type_info": "Text"
+ },
+ {
+ "name": "auto_update_modpack",
+ "ordinal": 29,
+ "type_info": "Integer"
}
],
"parameters": {
@@ -181,8 +186,9 @@
true,
true,
true,
+ true,
true
]
},
- "hash": "de1887a95766303d16ddcad7fe7f34e0a64101ead329bcc88e55e31b32578a97"
+ "hash": "df575bd90e6725b75cce9366ff27fa00083022e0a4b0437a3e613c84e76f2d5c"
}
diff --git a/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json b/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
new file mode 100644
index 0000000000..30621991bc
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version,\n auto_update_modpack\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29,\n $30\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29,\n auto_update_modpack = $30\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 30
+ },
+ "nullable": []
+ },
+ "hash": "f3fd719bd016b84c314631fd256b207c848c1d4cf7c30243b5c2cfd6a6f11597"
+}
diff --git a/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql b/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
new file mode 100644
index 0000000000..a6835f5056
--- /dev/null
+++ b/packages/app-lib/migrations/20260618000000_add-auto-update-modpack.sql
@@ -0,0 +1 @@
+ALTER TABLE profiles ADD COLUMN auto_update_modpack INTEGER NULL;
diff --git a/packages/app-lib/src/api/profile/create.rs b/packages/app-lib/src/api/profile/create.rs
index c1a1491c1b..21d8f31548 100644
--- a/packages/app-lib/src/api/profile/create.rs
+++ b/packages/app-lib/src/api/profile/create.rs
@@ -102,6 +102,7 @@ pub async fn profile_create(
wrapper: None,
post_exit: None,
},
+ auto_update_modpack: None,
};
let result = async {
diff --git a/packages/app-lib/src/api/profile/mod.rs b/packages/app-lib/src/api/profile/mod.rs
index a7f2f113ac..b58ba3d537 100644
--- a/packages/app-lib/src/api/profile/mod.rs
+++ b/packages/app-lib/src/api/profile/mod.rs
@@ -819,6 +819,37 @@ async fn run_credentials(
))
})?;
+ // Auto-update modpack if enabled and an update is available
+ if profile.auto_update_modpack == Some(true)
+ && profile.linked_data.is_some()
+ && let Ok(Some(info)) = crate::state::get_linked_modpack_info(
+ &profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await
+ && info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ if let Err(e) = update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await
+ {
+ tracing::warn!(
+ "Auto-update of modpack '{}' failed: {e}",
+ profile.name,
+ );
+ }
+ }
+
let pre_launch_hooks = profile
.hooks
.pre_launch
@@ -1178,6 +1209,117 @@ pub async fn add_all_recursive_folder_paths(
Ok(())
}
+/// Check for a modpack update for a single profile and apply it if auto-update is enabled.
+/// Returns `true` if an update was applied.
+#[tracing::instrument]
+pub async fn auto_update_modpack_if_needed(path: &str) -> crate::Result {
+ let state = State::get().await?;
+
+ let Some(profile) = get(path).await? else {
+ return Ok(false);
+ };
+
+ if profile.auto_update_modpack != Some(true)
+ || profile.linked_data.is_none()
+ {
+ return Ok(false);
+ }
+
+ let Some(info) = crate::state::get_linked_modpack_info(
+ &profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await?
+ else {
+ return Ok(false);
+ };
+
+ if info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await?;
+ return Ok(true);
+ }
+
+ Ok(false)
+}
+
+/// Check all profiles for modpack updates and apply them automatically
+/// where `auto_update_modpack` is enabled.
+#[tracing::instrument]
+pub async fn auto_update_all_modpacks() -> crate::Result<()> {
+ let state = State::get().await?;
+ let profiles = Profile::get_all(&state.pool).await?;
+
+ for profile in &profiles {
+ if profile.auto_update_modpack != Some(true)
+ || profile.linked_data.is_none()
+ {
+ continue;
+ }
+
+ // Skip if the profile is currently running
+ let is_running = state
+ .process_manager
+ .get_all()
+ .iter()
+ .any(|p| p.profile_path == profile.path);
+
+ if is_running {
+ tracing::debug!(
+ "Skipping auto-update for '{}' — profile is running",
+ profile.name,
+ );
+ continue;
+ }
+
+ let Ok(Some(info)) = crate::state::get_linked_modpack_info(
+ profile,
+ None,
+ &state.pool,
+ &state.api_semaphore,
+ )
+ .await
+ else {
+ continue;
+ };
+
+ if info.has_update
+ && let Some(update_version_id) = &info.update_version_id
+ {
+ tracing::info!(
+ "Background auto-updating modpack '{}' to version {}",
+ profile.name,
+ update_version_id,
+ );
+ if let Err(e) = update::update_managed_modrinth_version(
+ &profile.path,
+ update_version_id,
+ )
+ .await
+ {
+ tracing::warn!(
+ "Background auto-update of modpack '{}' failed: {e}",
+ profile.name,
+ );
+ }
+ }
+ }
+
+ Ok(())
+}
+
pub fn sanitize_profile_name(input: &str) -> String {
input.replace(
['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>', '!'],
diff --git a/packages/app-lib/src/state/legacy_converter.rs b/packages/app-lib/src/state/legacy_converter.rs
index 9404c6ae0f..e765cf267c 100644
--- a/packages/app-lib/src/state/legacy_converter.rs
+++ b/packages/app-lib/src/state/legacy_converter.rs
@@ -376,6 +376,7 @@ where
.and_then(|x| x.wrapper.clone()),
post_exit: profile.hooks.and_then(|x| x.post_exit),
},
+ auto_update_modpack: None,
}
.upsert(exec)
.await?;
diff --git a/packages/app-lib/src/state/profiles.rs b/packages/app-lib/src/state/profiles.rs
index c30412af22..c2e5bd11a4 100644
--- a/packages/app-lib/src/state/profiles.rs
+++ b/packages/app-lib/src/state/profiles.rs
@@ -57,6 +57,7 @@ pub struct Profile {
pub force_fullscreen: Option,
pub game_resolution: Option,
pub hooks: Hooks,
+ pub auto_update_modpack: Option,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, PartialEq)]
@@ -315,6 +316,7 @@ struct ProfileQueryResult {
override_hook_post_exit: Option,
protocol_version: Option,
launcher_feature_version: String,
+ auto_update_modpack: Option,
}
impl TryFrom for Profile {
@@ -387,6 +389,7 @@ impl TryFrom for Profile {
wrapper: x.override_hook_wrapper,
post_exit: x.override_hook_post_exit,
},
+ auto_update_modpack: x.auto_update_modpack.map(|x| x == 1),
})
}
}
@@ -406,7 +409,8 @@ macro_rules! select_profiles_with_predicate {
override_java_path,
json(override_extra_launch_args) as "override_extra_launch_args!: serde_json::Value", json(override_custom_env_vars) as "override_custom_env_vars!: serde_json::Value",
override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,
- override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit
+ override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,
+ auto_update_modpack
FROM profiles
"#
+ $predicate,
@@ -527,7 +531,8 @@ impl Profile {
override_java_path, override_extra_launch_args, override_custom_env_vars,
override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,
override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,
- protocol_version, launcher_feature_version
+ protocol_version, launcher_feature_version,
+ auto_update_modpack
)
VALUES (
$1, $2, $3, $4,
@@ -539,7 +544,8 @@ impl Profile {
$18, jsonb($19), jsonb($20),
$21, $22, $23, $24,
$25, $26, $27,
- $28, $29
+ $28, $29,
+ $30
)
ON CONFLICT (path) DO UPDATE SET
install_stage = $2,
@@ -577,7 +583,8 @@ impl Profile {
override_hook_post_exit = $27,
protocol_version = $28,
- launcher_feature_version = $29
+ launcher_feature_version = $29,
+ auto_update_modpack = $30
",
self.path,
install_stage,
@@ -607,7 +614,8 @@ impl Profile {
self.hooks.wrapper,
self.hooks.post_exit,
self.protocol_version,
- launcher_feature_version
+ launcher_feature_version,
+ self.auto_update_modpack
)
.execute(exec)
.await?;