Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Decentraland Launcher — a cross-platform (macOS/Windows) desktop app built with **Tauri v2** that manages installation, updating, and launching of the Decentraland Explorer client. Rust backend, React/TypeScript frontend, Tauri IPC bridge.

## Build & Development Commands

```bash
npm install # Install all dependencies
npm run tauri dev # Run in development mode
npm run tauri build # Build for distribution
npm run format # Format JS/TS (Prettier) + check Rust formatting
npm run analyze # Run clippy on all three Rust crates with -D warnings
```

**Rust-only commands** (run from `core/`, `src-tauri/`, or `src-auto-auth/`):
```bash
cargo fmt # Format Rust code
cargo check # Type-check
cargo clippy --all-targets --all-features -- -D warnings
cargo test # Run tests for that crate
```

The pre-commit hook (`.githooks/pre-commit`) runs fmt, check, clippy, and test on all three Rust crates.

## Architecture

```
Frontend (React/TS) ←—Tauri IPC Channel—→ Backend (Rust)
src/ core/src/
components/Home/Home.tsx flow.rs (orchestration)
installs.rs → downloads.rs → compression.rs
types.rs (Status/Step enums serialized to TS)
```

**Three Rust crates** (no workspace — independent Cargo.toml files):
- **`core/`** (`dcl-launcher-core`) — Main business logic: launch flow orchestration, downloads, installation, S3 version fetching, analytics, error handling, deep linking, auto-auth
- **`src-tauri/`** (`Decentraland-Launcher`) — Tauri app shell: IPC command handlers (`launch`, `retry`), self-update logic, deep link setup
- **`src-auto-auth/`** — Platform-specific token extraction (macOS DMG xattr, Windows PE tail)

**Frontend** (`src/`): React + Vite. Main UI in `components/Home/Home.tsx`. TypeScript types in `components/Home/types.ts` mirror Rust enums in `core/src/types.rs` — these must stay in sync.

**Core flow** (`core/src/flow.rs`): Fetch → Download → Install → Launch, with retry logic and status reporting over Tauri channels.

## Rust Conventions

**Rust edition 2024** for `core` and `src-auto-auth`, **2021** for `src-tauri`. Minimum Rust version: **1.88.0**.

**Strict clippy enforcement** (see `core/src/lib.rs`):
- `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing, clippy::arithmetic_side_effects, clippy::todo, clippy::dbg_macro)]`
- `#![warn(clippy::all, clippy::pedantic, clippy::nursery)]`
- Use `?` operator and proper error handling instead of unwrap/expect/panic
- Use `.get()` for safe indexing instead of `[]`

**Error handling**: Custom `StepError` enum in `core/src/errors.rs` with error codes (E0000–E3002). Use `anyhow` for generic errors, `thiserror` for enum derives.

**Serde**: Use `#[serde(rename_all = "camelCase")]` for types crossing the IPC boundary to match JavaScript conventions.

**Platform-specific code**: Use `#[cfg(target_os = "macos")]` and `#[cfg(target_os = "windows")]` for platform-conditional logic.

## Commit Convention

Conventional Commits: `feat:`, `fix:`, `chore:`, `style:` with optional scopes like `(release)`, `(windows)`, `(macos)`, `(auto-auth)`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use style: convention. Where did you get that?

2 changes: 1 addition & 1 deletion core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions core/src/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ impl WorkflowStep<LaunchFlowState, ()> for AppLaunchStep {
let any_is_running = self.is_any_instance_running().await?;
let is_local_scene = deeplink.has_true_value(ARG_LOCAL_SCENE) || args.local_scene;

if !open_new_instance && any_is_running && !is_local_scene {
let result = if !open_new_instance && any_is_running && !is_local_scene {
channel.send(Status::State {
step: Step::DeeplinkOpening,
})?;
Expand Down Expand Up @@ -505,7 +505,12 @@ impl WorkflowStep<LaunchFlowState, ()> for AppLaunchStep {
.launch_explorer(Some(deeplink), None)
.await?;
StepResult::Ok(())
}
};

// Clear persisted deep link file after consumption
Protocol::clear_file();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you delete the file only if deeplink opens via an existing instance ? Why do you keep the file if a new explorer instance is opened?


result
}
None => {
//TODO passed version if specified manually from upper flow
Expand Down
4 changes: 4 additions & 0 deletions core/src/installs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ pub fn deeplink_bridge_path() -> PathBuf {
explorer_path().join("deeplink-bridge.json")
}

pub fn deeplink_state_path() -> PathBuf {
explorer_path().join("deeplink-state.txt")
}

// There is no point to recovery if the app failed to create working directory
#[allow(clippy::expect_used)]
fn get_app_base_path() -> PathBuf {
Expand Down
33 changes: 32 additions & 1 deletion core/src/protocols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::result::Result;
use std::sync::Mutex;
use url::form_urlencoded;

use log::{error, warn};
use log::{error, info, warn};

static PROTOCOL_STATE: Mutex<Option<DeepLink>> = Mutex::new(None);
const PROTOCOL_PREFIX: &str = "decentraland://";
Expand Down Expand Up @@ -122,6 +122,37 @@ impl Protocol {
}
}

pub fn save_to_file(deeplink: &DeepLink) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those implementation specific functioned don't need to be public. Let's proceed with public interface consume_deeplink() -> Option<DeepLink> and try_assign_value will write to file. Public API surface can be much simplier.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method should be named with try_ prefix based on it's intent. Since method doesn't return an explicit Result<T, E> that needs an explanation via naming.

let path = crate::installs::deeplink_state_path();
if let Err(e) = std::fs::write(&path, deeplink.original()) {
error!("Failed to persist deep link to file: {}", e);
} else {
info!("Persisted deep link to {}", path.display());
}
}

pub fn load_from_file() -> Option<String> {
let path = crate::installs::deeplink_state_path();
match std::fs::read_to_string(&path) {
Ok(content) if !content.is_empty() => {
info!("Loaded persisted deep link from {}", path.display());
Some(content)
}
Ok(_) | Err(_) => None,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't ignore errors and better to log them

}
}

pub fn clear_file() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method should be named with try_ prefix based on it's intent

let path = crate::installs::deeplink_state_path();
if path.exists() {
if let Err(e) = std::fs::remove_file(&path) {
error!("Failed to remove persisted deep link file: {}", e);
} else {
info!("Cleared persisted deep link file");
}
}
}

pub fn try_assign_value_from_vec(&self, value: &Vec<String>) {
for v in value {
if let Ok(deeplink) = DeepLink::new(v.to_owned()) {
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ async fn update_if_needed_and_restart(
.await?;

channel.send_silent(LauncherUpdate::InstallingUpdate.into());

// Persist deeplink to file BEFORE install (install may call exit(0) on Windows)
if let Some(deeplink) = Protocol::value() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be redundant with new API surface

Protocol::save_to_file(&deeplink);
}

update.install(content)?;
info!("update installed");

Expand Down Expand Up @@ -237,6 +243,13 @@ fn setup_deeplink(a: &App, protocol: &Protocol) {
let args: Vec<String> = AppEnvironment::raw_cmd_args().collect();
protocol.try_assign_value_from_vec(&args);

// Fallback: load from persisted file (survives Windows NSIS restart)
if Protocol::value().is_none() {
if let Some(saved) = Protocol::load_from_file() {
protocol.try_assign_value(saved);
}
}

#[cfg(target_os = "macos")]
{
let protocol = protocol.clone();
Expand Down
Loading