From 2c925a679f84301fec61a108cfdfee4a99fc9f09 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 28 Aug 2026 17:33:47 -0400 Subject: [PATCH] Take XDG resolution from the shared crate too Sixth and last of the modules that were the link rather than this product. main.rs loses config_home, data_home and xdg_dir, and its test module with them -- it held one test, which moved to the crate that now holds the code. The helpers gained a `product` parameter, matching certs::ensure and netif::wg_address, which is what keeps two products' state apart while resolving it identically. Verified by running it: with only XDG_CONFIG_HOME and XDG_DATA_HOME set and no --config or --data-dir, the server puts its certificates in $XDG_CONFIG_HOME/ai-app/certs and its sessions in $XDG_DATA_HOME/ai-app/sessions, and still prints an aiapp:// enrollment URI. 35 tests here and 19 in the crate, clippy silent, rustfmt clean. --- server/src/main.rs | 64 +++++----------------------------------------- wg-app-link | 2 +- 2 files changed, 8 insertions(+), 58 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 744d895..29df202 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -33,40 +33,13 @@ use tokio::signal::unix::{SignalKind, signal}; use wg_app_link::enroll; use wg_app_link::netif::{self, WG_INTERFACE}; +use wg_app_link::xdg::{config_home, data_home}; use config::TokenEntry; use session::SessionManager; const DEFAULT_PORT: u16 = 8443; -/// `$XDG_CONFIG_HOME/ai-app`, or `~/.config/ai-app`. Holds `config.ron` -/// and `certs/`. -fn config_home() -> PathBuf { - xdg_dir(std::env::var_os("XDG_CONFIG_HOME"), ".config") -} - -/// `$XDG_DATA_HOME/ai-app`, or `~/.local/share/ai-app`. Holds the session -/// directories: transcripts, attachments, produced images. -fn data_home() -> PathBuf { - xdg_dir(std::env::var_os("XDG_DATA_HOME"), ".local/share") -} - -/// This app's directory under `base` -- the XDG variable's value, if it -/// was set to an absolute path as the spec requires -- or under -/// `~/` otherwise. Takes the value rather than reading the -/// environment itself so the rule is testable without mutating a -/// process-wide variable other threads may be reading. -fn xdg_dir(base: Option, fallback: &str) -> PathBuf { - base.map(PathBuf::from) - .filter(|path| path.is_absolute()) - .unwrap_or_else(|| { - std::env::home_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(fallback) - }) - .join("ai-app") -} - /// Serves AI coding sessions (Claude Code, llama.cpp) to the phone app. #[derive(Parser)] struct Args { @@ -121,16 +94,16 @@ async fn main() -> Result<()> { let config_path = args .config - .unwrap_or_else(|| config_home().join("config.ron")); + .unwrap_or_else(|| config_home("ai-app").join("config.ron")); let data_dir = args .data_dir - .unwrap_or_else(|| data_home().join("sessions")); + .unwrap_or_else(|| data_home("ai-app").join("sessions")); // Beside the session data rather than under it: models outlive every // session and are shared by all of them, so deleting a session must // never take a multi-gigabyte download with it. let models_dir = args .models_dir - .unwrap_or_else(|| data_home().join("models")); + .unwrap_or_else(|| data_home("ai-app").join("models")); let models = Arc::new(models::ModelStore::new(models_dir.clone())); let manager = Arc::new( SessionManager::new(config_path.clone(), data_dir, models_dir.clone()) @@ -163,7 +136,9 @@ async fn main() -> Result<()> { // also what the phone app embeds at build time, so they need to be // obtainable on a machine whose tunnel isn't up yet. The leaf is // reissued on every start, so once wg0 exists the next start covers it. - let certs_dir = args.certs.unwrap_or_else(|| config_home().join("certs")); + let certs_dir = args + .certs + .unwrap_or_else(|| config_home("ai-app").join("certs")); let certificates = wg_app_link::certs::ensure("ai-app", &certs_dir, &netif::local_addresses()) .with_context(|| format!("failed to prepare certificates in {}", certs_dir.display()))?; if certificates.ca_is_new { @@ -247,28 +222,3 @@ async fn main() -> Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn xdg_dirs_respect_the_environment_and_are_namespaced() { - let home_fallback = xdg_dir(None, ".config"); - assert!(home_fallback.ends_with("ai-app")); - assert!(home_fallback.parent().expect("parent").ends_with(".config")); - - assert_eq!( - xdg_dir(Some("/somewhere".into()), ".config"), - PathBuf::from("/somewhere/ai-app"), - ); - - // Relative values are ignored per the spec, rather than resolving - // against whatever the working directory happens to be -- so a - // relative setting lands on the same path as no setting at all. - assert_eq!( - xdg_dir(Some("relative/path".into()), ".config"), - home_fallback - ); - } -} diff --git a/wg-app-link b/wg-app-link index 841a3a8..73a32cb 160000 --- a/wg-app-link +++ b/wg-app-link @@ -1 +1 @@ -Subproject commit 841a3a8372d62b1069dd0b54e0056e66e7f2410f +Subproject commit 73a32cb89795b79913e65cb92362612cd109b22c