File size: 4,263 Bytes
afa0cbf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | 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 workspace {display_workspace} in the Desktop app...");
open_url(&codex_new_thread_url(&workspace_path)).await?;
return Ok(());
}
eprintln!("Desktop app 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 the Desktop app, open workspace {display_workspace}.");
Ok(())
}
async fn codex_app_is_installed() -> anyhow::Result<bool> {
// This package identity is stable across Codex- and ChatGPT-branded builds.
let output = Command::new("powershell.exe")
.arg("-NoProfile")
.arg("-Command")
.arg(
"Get-StartApps | Where-Object AppID -Like 'OpenAI.Codex_*!App' | 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"
);
}
}
|