Codex Handbook
cli/src/desktop_app/windows.rs 130 lines
use anyhow::Context as _;use std::path::Path;use std::path::PathBuf;use tokio::process::Command;const CODEX_WINDOWS_INSTALLER_URL: &str =    "https://get.microsoft.com/installer/download/9PLM9XGG6VKS?cid=website_cta_psi";const CODEX_MICROSOFT_STORE_WEB_URL: &str = "https://apps.microsoft.com/detail/9plm9xgg6vks";pub async fn run_windows_app_open_or_install(    workspace: PathBuf,    download_url_override: Option<String>,) -> anyhow::Result<()> {    let workspace_path = workspace.display().to_string();    let display_workspace = display_workspace_path(&workspace);    if codex_app_is_installed().await? {        eprintln!("Opening Codex Desktop workspace {display_workspace}...");        open_url(&codex_new_thread_url(&workspace_path)).await?;        return Ok(());    }    eprintln!("Codex Desktop not found; opening Windows installer...");    let download_url = download_url_override        .as_deref()        .unwrap_or(CODEX_WINDOWS_INSTALLER_URL);    if open_url(download_url).await.is_err() && download_url_override.is_none() {        open_url(CODEX_MICROSOFT_STORE_WEB_URL).await?;    }    eprintln!("After installing Codex Desktop, open workspace {display_workspace}.");    Ok(())}async fn codex_app_is_installed() -> anyhow::Result<bool> {    let output = Command::new("powershell.exe")        .arg("-NoProfile")        .arg("-Command")        .arg("Get-StartApps -Name 'Codex' | Select-Object -First 1 -ExpandProperty AppID")        .output()        .await        .context("failed to invoke `powershell.exe`")?;    if !output.status.success() {        return Ok(false);    }    Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())}async fn open_url(url: &str) -> anyhow::Result<()> {    let status = Command::new("powershell.exe")        .arg("-NoProfile")        .arg("-Command")        .arg("& { param($target) Start-Process -FilePath $target }")        .arg(url)        .status()        .await        .with_context(|| format!("failed to open {url}"))?;    if status.success() {        Ok(())    } else {        anyhow::bail!("failed to open {url} with {status}");    }}fn codex_new_thread_url(workspace: &str) -> String {    let mut serializer = url::form_urlencoded::Serializer::new(String::new());    serializer.append_pair("path", workspace);    let query = serializer.finish();    format!("codex://threads/new?{query}")}fn display_workspace_path(workspace: &Path) -> String {    let path = workspace.display().to_string();    if let Some(path) = path.strip_prefix(r"\\?\UNC\") {        format!(r"\\{path}")    } else if let Some(path) = path.strip_prefix(r"\\?\") {        path.to_string()    } else {        path    }}#[cfg(test)]mod tests {    use super::codex_new_thread_url;    use super::display_workspace_path;    use pretty_assertions::assert_eq;    use std::path::Path;    #[test]    fn display_workspace_path_removes_windows_extended_prefix() {        assert_eq!(            display_workspace_path(Path::new(r"\\?\C:\Users\fcoury\code\codex")),            r"C:\Users\fcoury\code\codex"        );    }    #[test]    fn display_workspace_path_preserves_unc_prefix() {        assert_eq!(            display_workspace_path(Path::new(r"\\?\UNC\server\share\codex")),            r"\\server\share\codex"        );    }    #[test]    fn display_workspace_path_leaves_regular_paths_unchanged() {        assert_eq!(            display_workspace_path(Path::new(r"C:\Users\fcoury\code\codex")),            r"C:\Users\fcoury\code\codex"        );    }    #[test]    fn codex_new_thread_url_encodes_windows_workspace_path() {        assert_eq!(            codex_new_thread_url(r"C:\Users\akuma\repos\koba"),            r"codex://threads/new?path=C%3A%5CUsers%5Cakuma%5Crepos%5Ckoba"        );    }    #[test]    fn codex_new_thread_url_preserves_verbatim_workspace_path() {        assert_eq!(            codex_new_thread_url(r"\\?\C:\Users\akuma\repos\koba"),            r"codex://threads/new?path=%5C%5C%3F%5CC%3A%5CUsers%5Cakuma%5Crepos%5Ckoba"        );    }}