docs: add Phase 4 Final Mile implementation plan
This commit is contained in:
parent
67ba1bfe4e
commit
c64c9635fa
1 changed files with 617 additions and 0 deletions
617
docs/superpowers/plans/2026-09-29-molotov-phase4-final-mile.md
Normal file
617
docs/superpowers/plans/2026-09-29-molotov-phase4-final-mile.md
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
# Molotov Phase 4: The Final Mile — End-to-End Orchestration, Cleaner, Region Routing & Interactive UI Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Complete the remaining user-facing and integration systems for Molotov: the end-to-end launch pipeline, cache/version cleaner tool, server region router, Discord Rich Presence (RPC) daemon, and interactive GUI/CLI wire-up.
|
||||
|
||||
**Architecture:** Connects `molotov-bootstrap`, `molotov-core`, `molotov-cli`, and `molotov-gui`. Scans and cleans stale versions from disk, passes region-routing parameters into engine flags, broadcasts playtime over Discord IPC socket (`/run/user/<uid>/discord-ipc-0`), and wires up the `egui` dashboard buttons to real operations.
|
||||
|
||||
**Tech Stack:** Rust 1.75+, `tokio`, `serde`, `serde_json`, `eframe` / `egui`, `clap`, `tracing`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` (Sections 4, 5, 6, 8)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton.
|
||||
- **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files.
|
||||
- **Atomic & Clean Operation**: Never leave partial files on disk; cleaning must never delete the active/current Roblox version.
|
||||
- **Safe Mode Fallback**: Automatically activates minimal flags if consecutive startup failures are detected.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Active Version Deletion Prevention**: Cleaner must never delete the currently active version directory.
|
||||
2. **Discord Socket Non-Blocking Failure**: If Discord is not running or socket is absent, RPC connection attempts must gracefully no-op without lagging the game or throwing panics.
|
||||
3. **Region Route Parameter Validation**: Invalid or malformed region codes must default to `Auto` without crashing.
|
||||
4. **GUI Background Threading**: Launching games or downloading updates from the GUI must not freeze the `egui` UI render loop.
|
||||
5. **Partial Download Purging**: Cleaner must cleanly purge `.part` files in `~/.cache/molotov/downloads/`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Real Disk & Cache Cleaner Engine
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-bootstrap/src/cleaner.rs`
|
||||
- Modify: `crates/molotov-bootstrap/src/lib.rs`
|
||||
- Test: `crates/molotov-bootstrap/tests/cleaner_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_bootstrap::cleaner::Cleaner`
|
||||
- `molotov_bootstrap::cleaner::CleanReport`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for version and cache cleanup**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/tests/cleaner_test.rs
|
||||
use molotov_bootstrap::cleaner::Cleaner;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_cleaner_purges_stale_versions_and_part_files() {
|
||||
let temp = tempdir().unwrap();
|
||||
let versions_dir = temp.path().join("versions");
|
||||
let downloads_dir = temp.path().join("downloads");
|
||||
std::fs::create_dir_all(&versions_dir).unwrap();
|
||||
std::fs::create_dir_all(&downloads_dir).unwrap();
|
||||
|
||||
// Create current version and stale version
|
||||
let current_ver = versions_dir.join("version-current");
|
||||
let stale_ver = versions_dir.join("version-stale-1");
|
||||
std::fs::create_dir_all(¤t_ver).unwrap();
|
||||
std::fs::create_dir_all(&stale_ver).unwrap();
|
||||
std::fs::write(current_ver.join("libroblox.so"), b"ACTIVE_LIB").unwrap();
|
||||
std::fs::write(stale_ver.join("libroblox.so"), b"STALE_LIB").unwrap();
|
||||
|
||||
// Create leftover .part file
|
||||
let part_file = downloads_dir.join("download.apk.part");
|
||||
std::fs::write(&part_file, b"DANGLING_PART").unwrap();
|
||||
|
||||
let cleaner = Cleaner::new(versions_dir, downloads_dir);
|
||||
let report = cleaner.clean_stale_data("version-current").unwrap();
|
||||
|
||||
assert_eq!(report.purged_versions.len(), 1);
|
||||
assert_eq!(report.purged_versions[0], "version-stale-1");
|
||||
assert_eq!(report.purged_parts_count, 1);
|
||||
assert!(report.reclaimed_bytes > 0);
|
||||
assert!(!stale_ver.exists(), "Stale version must be deleted");
|
||||
assert!(!part_file.exists(), "Dangling .part file must be deleted");
|
||||
assert!(current_ver.exists(), "Active version must NOT be deleted");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test cleaner_test`
|
||||
Expected: FAIL with unexported module `cleaner`.
|
||||
|
||||
- [ ] **Step 3: Implement Cleaner**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/src/cleaner.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use crate::error::MolotovError;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CleanReport {
|
||||
pub purged_versions: Vec<String>,
|
||||
pub purged_parts_count: usize,
|
||||
pub reclaimed_bytes: u64,
|
||||
}
|
||||
|
||||
pub struct Cleaner {
|
||||
versions_root: PathBuf,
|
||||
downloads_root: PathBuf,
|
||||
}
|
||||
|
||||
impl Cleaner {
|
||||
pub fn new(versions_root: PathBuf, downloads_root: PathBuf) -> Self {
|
||||
Self {
|
||||
versions_root,
|
||||
downloads_root,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_stale_data(&self, active_version_guid: &str) -> Result<CleanReport, MolotovError> {
|
||||
let mut report = CleanReport::default();
|
||||
|
||||
// 1. Scan versions directory
|
||||
if self.versions_root.exists() {
|
||||
for entry in std::fs::read_dir(&self.versions_root)? {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
if entry.file_type()?.is_dir() && file_name != active_version_guid {
|
||||
let dir_size = calculate_dir_size(&entry.path());
|
||||
std::fs::remove_dir_all(entry.path())?;
|
||||
report.purged_versions.push(file_name);
|
||||
report.reclaimed_bytes += dir_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Scan downloads directory for leftover .part files
|
||||
if self.downloads_root.exists() {
|
||||
for entry in std::fs::read_dir(&self.downloads_root)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) == Some("part") {
|
||||
let file_size = entry.metadata()?.len();
|
||||
std::fs::remove_file(path)?;
|
||||
report.purged_parts_count += 1;
|
||||
report.reclaimed_bytes += file_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_dir_size(path: &std::path::Path) -> u64 {
|
||||
let mut total = 0;
|
||||
if let Ok(entries) = std::fs::read_dir(path) {
|
||||
for entry in entries.flatten() {
|
||||
if let Ok(meta) = entry.metadata() {
|
||||
if meta.is_dir() {
|
||||
total += calculate_dir_size(&entry.path());
|
||||
} else {
|
||||
total += meta.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-bootstrap/src/lib.rs` to expose `pub mod cleaner;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test cleaner_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-bootstrap/
|
||||
git commit -m "feat(bootstrap): implement disk cache and version cleaner"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Preferred Server Region Selector & Network Configuration
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-bootstrap/src/region.rs`
|
||||
- Modify: `crates/molotov-bootstrap/src/lib.rs`
|
||||
- Test: `crates/molotov-bootstrap/tests/region_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_bootstrap::region::ServerRegion`
|
||||
- `molotov_bootstrap::region::ServerRegion::from_str()`
|
||||
- `molotov_bootstrap::region::ServerRegion::apply_to_fflags()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for region parsing and FastFlag routing**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/tests/region_test.rs
|
||||
use molotov_bootstrap::fflags::FastFlagManager;
|
||||
use molotov_bootstrap::region::ServerRegion;
|
||||
|
||||
#[test]
|
||||
fn test_server_region_parsing_and_flags() {
|
||||
assert_eq!(ServerRegion::from_code("eu"), ServerRegion::Europe);
|
||||
assert_eq!(ServerRegion::from_code("us-east"), ServerRegion::USEast);
|
||||
assert_eq!(ServerRegion::from_code("invalid_garbage"), ServerRegion::Auto);
|
||||
|
||||
let mut manager = FastFlagManager::new();
|
||||
ServerRegion::Europe.apply_to_fflags(&mut manager);
|
||||
|
||||
// Verify flag was applied
|
||||
assert_eq!(
|
||||
manager.active_profile().flags.get("FStringPreferredGameRegion").map(|v| format!("{:?}", v)),
|
||||
Some("Text(\"eu-central-1\")".to_string())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test region_test`
|
||||
Expected: FAIL with unexported module `region`.
|
||||
|
||||
- [ ] **Step 3: Implement ServerRegion**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/src/region.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use crate::fflags::{FastFlagManager, FlagValue};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum ServerRegion {
|
||||
#[default]
|
||||
Auto,
|
||||
USEast,
|
||||
USWest,
|
||||
Europe,
|
||||
Asia,
|
||||
Australia,
|
||||
SouthAmerica,
|
||||
}
|
||||
|
||||
impl ServerRegion {
|
||||
pub fn from_code(code: &str) -> Self {
|
||||
let clean = code.trim().to_lowercase();
|
||||
match clean.as_str() {
|
||||
"us-east" | "useast" | "virginia" => ServerRegion::USEast,
|
||||
"us-west" | "uswest" | "california" => ServerRegion::USWest,
|
||||
"eu" | "europe" | "frankfurt" | "london" => ServerRegion::Europe,
|
||||
"asia" | "tokyo" | "singapore" => ServerRegion::Asia,
|
||||
"aus" | "australia" | "sydney" => ServerRegion::Australia,
|
||||
"sa" | "south-america" | "brazil" => ServerRegion::SouthAmerica,
|
||||
_ => ServerRegion::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
ServerRegion::Auto => "Auto / Default",
|
||||
ServerRegion::USEast => "US East (Virginia)",
|
||||
ServerRegion::USWest => "US West (California)",
|
||||
ServerRegion::Europe => "Europe (Frankfurt / London)",
|
||||
ServerRegion::Asia => "Asia (Tokyo / Singapore)",
|
||||
ServerRegion::Australia => "Australia (Sydney)",
|
||||
ServerRegion::SouthAmerica => "South America (São Paulo)",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn region_identifier(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
ServerRegion::Auto => None,
|
||||
ServerRegion::USEast => Some("us-east-1"),
|
||||
ServerRegion::USWest => Some("us-west-1"),
|
||||
ServerRegion::Europe => Some("eu-central-1"),
|
||||
ServerRegion::Asia => Some("ap-northeast-1"),
|
||||
ServerRegion::Australia => Some("ap-southeast-2"),
|
||||
ServerRegion::SouthAmerica => Some("sa-east-1"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_to_fflags(&self, manager: &mut FastFlagManager) {
|
||||
if let Some(id) = self.region_identifier() {
|
||||
manager.set_flag("FStringPreferredGameRegion", FlagValue::Text(id.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-bootstrap/src/lib.rs` to expose `pub mod region;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test region_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-bootstrap/
|
||||
git commit -m "feat(bootstrap): implement server region selector and FastFlag network routing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Zero-Dependency Discord Rich Presence Client
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/discord.rs`
|
||||
- Modify: `crates/molotov-core/src/lib.rs`
|
||||
- Test: `crates/molotov-core/tests/discord_rpc_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_core::discord::DiscordRpc`
|
||||
- `molotov_core::discord::DiscordActivity`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for Discord IPC frame creation and non-blocking failure**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/discord_rpc_test.rs
|
||||
use molotov_core::discord::{DiscordActivity, DiscordRpc};
|
||||
|
||||
#[test]
|
||||
fn test_discord_rpc_payload_construction() {
|
||||
let activity = DiscordActivity {
|
||||
details: "Playing Brookhaven RP".to_string(),
|
||||
state: "In Game".to_string(),
|
||||
start_timestamp: 1690000000,
|
||||
};
|
||||
|
||||
let payload = activity.to_json_payload();
|
||||
assert!(payload.contains("Brookhaven RP"));
|
||||
assert!(payload.contains("In Game"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discord_rpc_connect_non_blocking_when_socket_missing() {
|
||||
let mut rpc = DiscordRpc::new("123456789012345678");
|
||||
// Connect attempt when Discord socket is absent must return Ok(false) or Err cleanly, never panic
|
||||
let connected = rpc.try_connect().unwrap_or(false);
|
||||
assert!(!connected);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test discord_rpc_test`
|
||||
Expected: FAIL with unexported module `discord`.
|
||||
|
||||
- [ ] **Step 3: Implement DiscordRpc**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/discord.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiscordActivity {
|
||||
pub details: String,
|
||||
pub state: String,
|
||||
pub start_timestamp: u64,
|
||||
}
|
||||
|
||||
impl DiscordActivity {
|
||||
pub fn to_json_payload(&self) -> String {
|
||||
format!(
|
||||
r#"{{"cmd":"SET_ACTIVITY","args":{{"pid":{},"activity":{{"details":"{}","state":"{}","timestamps":{{"start":{}}},"assets":{{"large_image":"roblox_logo","large_text":"Roblox on Linux (Molotov)"}}}}}},"nonce":"1"}}"#,
|
||||
std::process::id(),
|
||||
self.details,
|
||||
self.state,
|
||||
self.start_timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiscordRpc {
|
||||
client_id: String,
|
||||
stream: Option<UnixStream>,
|
||||
}
|
||||
|
||||
impl DiscordRpc {
|
||||
pub fn new(client_id: &str) -> Self {
|
||||
Self {
|
||||
client_id: client_id.to_string(),
|
||||
stream: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_connect(&mut self) -> Result<bool, String> {
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let socket_path = PathBuf::from(format!("/run/user/{}/discord-ipc-0", uid));
|
||||
|
||||
if !socket_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match UnixStream::connect(&socket_path) {
|
||||
Ok(s) => {
|
||||
self.stream = Some(s);
|
||||
tracing::info!("DiscordRpc: successfully connected to Discord IPC socket");
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("DiscordRpc: failed to connect to socket: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.stream.is_some()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/lib.rs` to expose `pub mod discord;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test discord_rpc_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(core): implement zero-dependency Discord Rich Presence IPC client"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Unified Launch Orchestrator
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-bootstrap/src/launcher.rs`
|
||||
- Modify: `crates/molotov-bootstrap/src/lib.rs`
|
||||
- Test: `crates/molotov-bootstrap/tests/launcher_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: All `molotov-bootstrap` modules (`cdn`, `apk`, `fflags`, `mods`, `sandbox`, `watchdog`)
|
||||
- Produces:
|
||||
- `molotov_bootstrap::launcher::Launcher`
|
||||
- `molotov_bootstrap::launcher::LaunchOptions`
|
||||
- `molotov_bootstrap::launcher::Launcher::prepare_launch()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for launcher orchestration flow**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/tests/launcher_test.rs
|
||||
use molotov_bootstrap::launcher::{LaunchOptions, Launcher};
|
||||
use molotov_bootstrap::region::ServerRegion;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_launcher_prepare_flow() {
|
||||
let temp = tempdir().unwrap();
|
||||
let root = temp.path().to_path_buf();
|
||||
|
||||
let launcher = Launcher::new(root);
|
||||
let options = LaunchOptions {
|
||||
place_id: Some(1818),
|
||||
safe_mode: false,
|
||||
region: ServerRegion::Europe,
|
||||
instance_id: "default".to_string(),
|
||||
};
|
||||
|
||||
let plan = launcher.prepare_launch(options).expect("Should prepare launch plan");
|
||||
assert!(plan.fflags_path.exists());
|
||||
assert_eq!(plan.place_id, Some(1818));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test launcher_test`
|
||||
Expected: FAIL with unexported module `launcher`.
|
||||
|
||||
- [ ] **Step 3: Implement Launcher**
|
||||
|
||||
```rust
|
||||
// crates/molotov-bootstrap/src/launcher.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use crate::error::MolotovError;
|
||||
use crate::fflags::{FastFlagManager, PresetProfile};
|
||||
use crate::region::ServerRegion;
|
||||
use crate::sandbox::{SandboxConfig, SandboxRunner};
|
||||
use crate::watchdog::{LaunchStatus, LaunchWatchdog};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchOptions {
|
||||
pub place_id: Option<u64>,
|
||||
pub safe_mode: bool,
|
||||
pub region: ServerRegion,
|
||||
pub instance_id: String,
|
||||
}
|
||||
|
||||
pub struct LaunchPlan {
|
||||
pub place_id: Option<u64>,
|
||||
pub fflags_path: PathBuf,
|
||||
pub sandbox_config: SandboxConfig,
|
||||
pub is_safe_mode: bool,
|
||||
}
|
||||
|
||||
pub struct Launcher {
|
||||
base_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Launcher {
|
||||
pub fn new(base_dir: PathBuf) -> Self {
|
||||
Self { base_dir }
|
||||
}
|
||||
|
||||
pub fn prepare_launch(&self, options: LaunchOptions) -> Result<LaunchPlan, MolotovError> {
|
||||
let state_file = self.base_dir.join("launch_state.json");
|
||||
let mut watchdog = LaunchWatchdog::new(state_file);
|
||||
|
||||
// Check if Safe Mode is required
|
||||
let activate_safe_mode = options.safe_mode || watchdog.status() == LaunchStatus::RequiresSafeMode;
|
||||
watchdog.record_start()?;
|
||||
|
||||
let mut fflag_mgr = FastFlagManager::new();
|
||||
if activate_safe_mode {
|
||||
tracing::warn!("Launcher: launching in SAFE MODE with minimal default flags");
|
||||
fflag_mgr.apply_preset(PresetProfile::Default);
|
||||
} else {
|
||||
fflag_mgr.apply_preset(PresetProfile::LowVram);
|
||||
options.region.apply_to_fflags(&mut fflag_mgr);
|
||||
}
|
||||
|
||||
let fflags_path = self.base_dir.join("ClientAppSettings.json");
|
||||
fflag_mgr.export_client_app_settings(&fflags_path)?;
|
||||
|
||||
let sandbox_config = SandboxConfig {
|
||||
instance_id: options.instance_id.clone(),
|
||||
data_dir: self.base_dir.join("instances").join(&options.instance_id).join("data"),
|
||||
config_dir: self.base_dir.join("instances").join(&options.instance_id).join("config"),
|
||||
nice_priority: -5,
|
||||
};
|
||||
|
||||
Ok(LaunchPlan {
|
||||
place_id: options.place_id,
|
||||
fflags_path,
|
||||
sandbox_config,
|
||||
is_safe_mode: activate_safe_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-bootstrap/src/lib.rs` to expose `pub mod launcher;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-bootstrap --test launcher_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-bootstrap/
|
||||
git commit -m "feat(bootstrap): implement unified Launch orchestrator"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Interactive GUI & CLI Wire-Up
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/molotov-cli/src/lib.rs`
|
||||
- Modify: `crates/molotov-cli/src/commands.rs`
|
||||
- Modify: `crates/molotov-gui/src/app.rs`
|
||||
- Test: `crates/molotov-cli/tests/cli_test.rs`
|
||||
- Test: `crates/molotov-gui/tests/gui_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_bootstrap::launcher::Launcher`, `molotov_bootstrap::cleaner::Cleaner`, `molotov_core::discord::DiscordRpc`
|
||||
- Produces: Live executable commands and GUI interactions
|
||||
|
||||
- [ ] **Step 1: Write test verifying CLI clean and launch execution**
|
||||
|
||||
```rust
|
||||
// In crates/molotov-cli/tests/cli_test.rs
|
||||
use clap::Parser;
|
||||
use molotov_cli::commands::Cli;
|
||||
|
||||
#[test]
|
||||
fn test_cli_parse_region_flag() {
|
||||
let parsed = Cli::try_parse_from(["molotov", "launch", "--region", "eu"]);
|
||||
assert!(parsed.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Implement CLI and GUI connections**
|
||||
|
||||
Update `crates/molotov-cli/src/commands.rs` to add `--region <REGION>` option to `Commands::Launch`.
|
||||
Update `crates/molotov-cli/src/lib.rs` to execute `Launcher::prepare_launch` and `Cleaner::clean_stale_data`.
|
||||
Update `crates/molotov-gui/src/app.rs` to invoke `Launcher`, `Cleaner`, and display region dropdown and Discord RPC status.
|
||||
|
||||
- [ ] **Step 3: Run tests to verify they pass**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-cli/ crates/molotov-gui/
|
||||
git commit -m "feat: wire up interactive GUI and CLI subcommands to launcher, cleaner, and region router"
|
||||
```
|
||||
|
||||
---
|
||||
Reference in a new issue