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
62 changes: 52 additions & 10 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ impl Default for LauncherHooks {

#[tokio::main]
async fn main() -> Result<()> {
run_launcher().await
}

async fn run_launcher() -> Result<()> {
let Some(_guard) = acquire_single_instance_guard()? else {
return Ok(());
};
Expand All @@ -43,7 +47,13 @@ async fn main() -> Result<()> {
let _ = notify_manager_when_update_available().await;
});
let hooks = LauncherHooks::default();
let handle = launch_and_inject_with_hooks(options, &hooks).await?;
let handle = match launch_and_inject_with_hooks(options, &hooks).await {
Ok(handle) => handle,
Err(error) => {
let _ = open_manager_with_status_prompt();
return Err(error);
}
};
handle.wait_for_codex_exit().await?;
Ok(())
}
Expand Down Expand Up @@ -84,9 +94,23 @@ async fn notify_manager_when_update_available() -> anyhow::Result<bool> {
}

fn open_manager_with_update_prompt() -> anyhow::Result<()> {
open_manager_with_args(["--show-update"])
}

fn open_manager_with_status_prompt() -> anyhow::Result<()> {
open_manager_with_args(["--show-status"])
}

fn open_manager_with_args<I, S>(args: I) -> anyhow::Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
let manager_path = manager_exe_path();
let mut command = std::process::Command::new(&manager_path);
command.arg("--show-update");
for arg in args {
command.arg(arg);
}
#[cfg(windows)]
{
command.creation_flags(codex_plus_core::windows_create_no_window());
Expand Down Expand Up @@ -558,14 +582,7 @@ fn open_url(url: &str) -> anyhow::Result<()> {
}

fn manager_exe_path() -> PathBuf {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
let dir = exe.parent().unwrap_or_else(|| Path::new("."));
let suffix = if cfg!(windows) { ".exe" } else { "" };
dir.join(format!(
"{}{}",
codex_plus_core::install::MANAGER_BINARY,
suffix
))
codex_plus_core::install::companion_binary_path(codex_plus_core::install::MANAGER_BINARY)
}

fn default_user_script_manager() -> UserScriptManager {
Expand Down Expand Up @@ -640,6 +657,31 @@ mod tests {
.is_some_and(|name| name.contains(codex_plus_core::install::MANAGER_BINARY))
);
}

#[test]
fn installed_launcher_resolves_manager_app_executable() {
let launcher_exe = Path::new("/Applications/Codex++.app/Contents/MacOS/CodexPlusPlus");
let manager_path = codex_plus_core::install::option_or_current_exe_from_exe(
launcher_exe,
&None,
codex_plus_core::install::MANAGER_BINARY,
Some(Path::new("/Applications")),
);

assert_eq!(
manager_path,
PathBuf::from("/Applications/Codex++ 管理工具.app/Contents/MacOS/CodexPlusPlusManager")
);
}

#[test]
fn launcher_failure_prompt_opens_manager_status_view() {
let source = include_str!("main.rs");

assert!(source.contains("open_manager_with_status_prompt"));
assert!(source.contains("\"--show-status\""));
assert!(source.contains("launch_and_inject_with_hooks(options, &hooks).await"));
}
}

fn builtin_user_scripts_dir() -> PathBuf {
Expand Down
41 changes: 41 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub struct ScriptMarketPayload {
#[serde(rename_all = "camelCase")]
pub struct StartupPayload {
pub show_update: bool,
pub show_status: bool,
}

#[tauri::command]
Expand All @@ -238,6 +239,7 @@ pub fn startup_options() -> CommandResult<StartupPayload> {
"启动参数已读取。",
StartupPayload {
show_update: startup_should_show_update(),
show_status: startup_should_show_status(),
},
)
}
Expand All @@ -249,6 +251,13 @@ pub fn startup_should_show_update() -> bool {
)
}

pub fn startup_should_show_status() -> bool {
should_show_status(
std::env::args(),
std::env::var("CODEX_PLUS_SHOW_STATUS").ok().as_deref(),
)
}

fn should_show_update<I, S>(args: I, env_value: Option<&str>) -> bool
where
I: IntoIterator<Item = S>,
Expand All @@ -257,6 +266,14 @@ where
args.into_iter().any(|arg| arg.as_ref() == "--show-update") || env_value == Some("1")
}

fn should_show_status<I, S>(args: I, env_value: Option<&str>) -> bool
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
args.into_iter().any(|arg| arg.as_ref() == "--show-status") || env_value == Some("1")
}

#[tauri::command]
pub async fn load_overview() -> CommandResult<OverviewPayload> {
let payload = tauri::async_runtime::spawn_blocking(load_overview_payload).await;
Expand Down Expand Up @@ -2163,6 +2180,22 @@ mod tests {
assert!(result.payload.show_update);
}

#[test]
fn startup_options_honors_show_status_environment() {
unsafe {
std::env::set_var("CODEX_PLUS_SHOW_STATUS", "1");
}

let result = startup_options();

unsafe {
std::env::remove_var("CODEX_PLUS_SHOW_STATUS");
}

assert_eq!(result.status, "ok");
assert!(result.payload.show_status);
}

#[test]
fn startup_options_honors_show_update_argument() {
assert!(should_show_update(
Expand All @@ -2171,6 +2204,14 @@ mod tests {
));
}

#[test]
fn startup_options_honors_show_status_argument() {
assert!(should_show_status(
["codex-plus-plus-manager.exe", "--show-status"],
None
));
}

#[test]
fn overview_contains_expected_operational_fields() {
let result = tauri::async_runtime::block_on(load_overview());
Expand Down
3 changes: 3 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ pub fn run() {
return;
};
let show_update = commands::startup_should_show_update();
let show_status = commands::startup_should_show_status();
let run_result = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.setup(move |app| {
let url = if show_update {
"index.html?showUpdate=1"
} else if show_status {
"index.html?showStatus=1"
} else {
"index.html"
};
Expand Down
7 changes: 7 additions & 0 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ function syncMarketInstalledState(current: ScriptMarketResult | null, userScript

type StartupResult = CommandResult<{
showUpdate: boolean;
showStatus: boolean;
}>;

type Route = "overview" | "relay" | "context" | "enhance" | "userScripts" | "providerSync" | "recommendations" | "maintenance" | "about" | "settings";
Expand Down Expand Up @@ -1088,6 +1089,9 @@ export function App() {
if (startup?.showUpdate) {
setRoute("about");
void checkUpdate(false);
} else if (startup?.showStatus) {
setRoute("overview");
void checkUpdate(true);
} else {
void checkUpdate(true);
}
Expand Down Expand Up @@ -4386,5 +4390,8 @@ function loadInitialRoute(): Route {
if (params.get("showUpdate") === "1" || window.location.hash === "#about") {
return "about";
}
if (params.get("showStatus") === "1" || window.location.hash === "#overview") {
return "overview";
}
return "overview";
}
27 changes: 24 additions & 3 deletions crates/codex-plus-core/src/install/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::Path;

use super::{
InstallOptions, MANAGER_BINARY, MANAGER_NAME, MacosAppBundle, SILENT_BINARY, SILENT_NAME,
install_root_or_default, option_or_current_exe,
install_root_or_default, option_or_current_exe_from_exe,
};

pub fn build_app_bundle(options: &InstallOptions, manager: bool) -> MacosAppBundle {
Expand All @@ -23,19 +23,23 @@ pub fn build_app_bundle(options: &InstallOptions, manager: bool) -> MacosAppBund
} else {
SILENT_BINARY
};
let target = option_or_current_exe(
let current_exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("."));
let target = option_or_current_exe_from_exe(
&current_exe,
if manager {
&options.manager_path
} else {
&options.launcher_path
},
binary,
Some(&install_root),
);
let identifier_suffix = if manager { ".manager" } else { "" };
MacosAppBundle {
app_path: install_root.join(format!("{display_name}.app")),
info_plist: info_plist(display_name, executable_name, identifier_suffix),
launch_script: format!("#!/bin/sh\nexec \"{}\"\n", target.to_string_lossy()),
target_path: target,
}
}

Expand Down Expand Up @@ -77,14 +81,31 @@ fn write_bundle(bundle: &MacosAppBundle) -> anyhow::Result<()> {
fs::create_dir_all(&resources)?;
fs::write(contents.join("Info.plist"), &bundle.info_plist)?;
let executable = macos.join(executable_name_from_plist(&bundle.info_plist));
fs::write(&executable, &bundle.launch_script)?;
if executable != bundle.target_path {
fs::write(&executable, &bundle.launch_script)?;
}
let mut permissions = fs::metadata(&executable)?.permissions();
permissions.set_mode(0o755);
fs::set_permissions(executable, permissions)?;
copy_icon(&resources)?;
Ok(())
}

#[cfg(target_os = "macos")]
pub fn launch_script_writes_executable(bundle: &MacosAppBundle) -> bool {
let executable = bundle
.app_path
.join("Contents")
.join("MacOS")
.join(executable_name_from_plist(&bundle.info_plist));
executable != bundle.target_path
}

#[cfg(not(target_os = "macos"))]
pub fn launch_script_writes_executable(_bundle: &MacosAppBundle) -> bool {
true
}

#[cfg(target_os = "macos")]
fn copy_icon(resources: &Path) -> anyhow::Result<()> {
let source = std::env::current_exe()
Expand Down
54 changes: 44 additions & 10 deletions crates/codex-plus-core/src/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub struct MacosAppBundle {
pub app_path: PathBuf,
pub info_plist: String,
pub launch_script: String,
pub target_path: PathBuf,
}

impl ShortcutState {
Expand Down Expand Up @@ -111,6 +112,10 @@ pub fn build_macos_app_bundle(options: &InstallOptions, manager: bool) -> MacosA
macos::build_app_bundle(options, manager)
}

pub fn macos_launch_script_writes_executable(bundle: &MacosAppBundle) -> bool {
macos::launch_script_writes_executable(bundle)
}

pub fn remove_owned_data() -> std::io::Result<()> {
let dir = crate::paths::default_app_state_dir();
if dir.exists() {
Expand Down Expand Up @@ -217,11 +222,20 @@ fn entrypoint_candidates(root: &Option<PathBuf>, manager: bool) -> Vec<PathBuf>
}

pub fn option_or_current_exe(value: &Option<PathBuf>, binary: &str) -> PathBuf {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
option_or_current_exe_from_exe(&exe, value, binary, None)
}

pub fn option_or_current_exe_from_exe(
exe: &Path,
value: &Option<PathBuf>,
binary: &str,
install_root: Option<&Path>,
) -> PathBuf {
if let Some(value) = value {
return value.clone();
}
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
companion_binary_path_from_exe(&exe, binary)
companion_binary_path_from_exe_with_install_root(exe, binary, install_root)
}

pub fn companion_binary_path(binary: &str) -> PathBuf {
Expand All @@ -230,12 +244,20 @@ pub fn companion_binary_path(binary: &str) -> PathBuf {
}

pub fn companion_binary_path_from_exe(exe: &Path, binary: &str) -> PathBuf {
companion_binary_path_from_exe_with_install_root(exe, binary, None)
}

fn companion_binary_path_from_exe_with_install_root(
exe: &Path,
binary: &str,
install_root: Option<&Path>,
) -> PathBuf {
let dir = exe.parent().unwrap_or_else(|| Path::new("."));
let suffix = if cfg!(windows) { ".exe" } else { "" };
if let Some(app_binary) = macos_installed_app_binary_from_exe(exe, binary, install_root) {
return app_binary;
}
if binary == SILENT_BINARY {
if let Some(sibling_app_binary) = macos_silent_app_binary_from_exe(exe) {
return sibling_app_binary;
}
let same_bundle = dir.join(binary);
if same_bundle.exists() {
return same_bundle;
Expand All @@ -244,14 +266,26 @@ pub fn companion_binary_path_from_exe(exe: &Path, binary: &str) -> PathBuf {
dir.join(format!("{binary}{suffix}"))
}

fn macos_silent_app_binary_from_exe(exe: &Path) -> Option<PathBuf> {
macos_applications_dir_from_exe(exe).map(|applications_dir| {
fn macos_installed_app_binary_from_exe(
exe: &Path,
binary: &str,
install_root: Option<&Path>,
) -> Option<PathBuf> {
let applications_dir = install_root
.map(Path::to_path_buf)
.or_else(|| macos_applications_dir_from_exe(exe))?;
let (app_name, executable_name) = match binary {
SILENT_BINARY => (SILENT_NAME, "CodexPlusPlus"),
MANAGER_BINARY => (MANAGER_NAME, "CodexPlusPlusManager"),
_ => return None,
};
Some(
applications_dir
.join(format!("{SILENT_NAME}.app"))
.join(format!("{app_name}.app"))
.join("Contents")
.join("MacOS")
.join("CodexPlusPlus")
})
.join(executable_name),
)
}

fn macos_applications_dir_from_exe(exe: &Path) -> Option<PathBuf> {
Expand Down
Loading