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.
This commit is contained in:
iris committed 2026-08-28 17:33:47 -04:00
1 parent aa05ff9336
commit 2c925a679f
2 files changed
+8 -58

No files matched your search

+7 -57
View File
@@ -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
/// `~/<fallback>` 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<std::ffi::OsString>, 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
);
}
}