diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index c38fa566e..f16c440f6 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -673,6 +673,14 @@ const KIMI_INSTALL_UNIX: &str = #[cfg(target_os = "windows")] const KIMI_INSTALL_WINDOWS_SCRIPT: &str = "irm https://code.kimi.com/kimi-code/install.ps1 | iex"; +// Oh My Pi 的 npm 包只能由 bun 运行(入口是 `#!/usr/bin/env bun` 的 TypeScript)。官方 +// installer 有匹配架构的 bun 时走 bun,否则下载并校验预编译二进制到 `~/.local/bin`, +// 所以新装不要求本机先有 bun。 +const OMP_INSTALL_UNIX: &str = + "bash -c 'tmp=$(mktemp) && curl -fsSL https://omp.sh/install -o $tmp && sh $tmp; status=$?; rm -f $tmp; exit $status'"; +#[cfg(target_os = "windows")] +const OMP_INSTALL_WINDOWS_SCRIPT: &str = "irm https://omp.sh/install.ps1 | iex"; + /// Antigravity CLI 官方 installer:把原生二进制放进 `~/.local/bin`,再跑 `agy install` /// 把该目录加进 shell 配置。目标处已有 `agy` 时它只提示已安装、exit 0,所以只用于新装; /// 升级一律走 `agy update`(实测非 TTY 下照常检查并安装)。 @@ -743,6 +751,14 @@ fn kimi_install_windows_command() -> String { ) } +#[cfg(target_os = "windows")] +fn omp_install_windows_command() -> String { + format!( + "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {}", + powershell_encoded_command(OMP_INSTALL_WINDOWS_SCRIPT) + ) +} + /// 重跑官方 installer 升级「脚本安装」那一处。显式传 `MCODE_INSTALL_DIR`,保证写回 /// 探测到的那个目录,而不是 installer 的默认目录。 #[cfg(not(target_os = "windows"))] @@ -878,7 +894,6 @@ fn npm_install_command_for(tool: &str) -> Option<&'static str> { "mcode" => Some( "npm i -g @minimax-ai/code@latest --ignore-scripts=false --include=optional \"--allow-scripts=@minimax-ai/code,better-sqlite3\"", ), - "omp" => Some("bun install -g @oh-my-pi/pi-coding-agent@latest"), _ => None, } } @@ -905,12 +920,15 @@ fn npm_install_extra_args(tool: &str) -> &'static str { } } +// `omp update` 自己识别 brew / mise / nix / bun / npm / 预编译二进制并照原渠道升级, +// 非 TTY 下不询问,失败退出码非 0;Nix 安装它只打印提示就 exit 0,见 +// `anchored_command_from_paths` 的 Nix 门控。 // MiniMax Code 虽有 `mcode update`,却刻意不列在这里:它在 stdin/stdout 非 TTY 时只打印 // "No interactive confirmation is available" 就 exit 0、并不安装。静默 lifecycle 的 // `cmd.output()` 正是非 TTY,放进来会让 `mcode update || ` 的兜底永不触发。 fn official_update_args(tool: &str) -> Option<&'static str> { match tool { - "claude" | "codex" | "grok" | "hermes" | "agy" => Some("update"), + "claude" | "codex" | "grok" | "hermes" | "agy" | "omp" => Some("update"), "openclaw" => Some("update --yes"), "opencode" => Some("upgrade"), "kimi" => Some("upgrade --yes"), @@ -943,18 +961,21 @@ fn tool_action_shell_command_for_shell( action: ToolLifecycleAction, shell: LifecycleCommandShell, ) -> Option { - if tool == "kimi" { + if matches!(tool, "kimi" | "omp") { let install = match shell { - LifecycleCommandShell::Posix => KIMI_INSTALL_UNIX.to_string(), + LifecycleCommandShell::Posix if tool == "kimi" => KIMI_INSTALL_UNIX.to_string(), + LifecycleCommandShell::Posix => OMP_INSTALL_UNIX.to_string(), #[cfg(target_os = "windows")] - LifecycleCommandShell::WindowsBatch => kimi_install_windows_command(), + LifecycleCommandShell::WindowsBatch if tool == "kimi" => kimi_install_windows_command(), + #[cfg(target_os = "windows")] + LifecycleCommandShell::WindowsBatch => omp_install_windows_command(), #[cfg(not(target_os = "windows"))] LifecycleCommandShell::WindowsBatch => return None, }; return Some(match action { ToolLifecycleAction::Install => install, ToolLifecycleAction::Update => { - chain_update_commands("kimi upgrade --yes".into(), install, shell) + chain_update_commands(bare_official_update_command(tool)?, install, shell) } }); } @@ -2510,11 +2531,10 @@ fn build_tool_search_paths(tool: &str) -> Vec { } } if tool == "omp" { - // omp ships only as a Bun global package (`bun install -g - // @oh-my-pi/pi-coding-agent`), and Bun's global bin is not on the PATH - // a GUI process inherits — same reason OpenCode scans it. Without this, - // version probing, install enumeration and anchored updates report the - // tool as not installed whenever the launcher is only reachable there. + // A Bun global install (`bun install -g @oh-my-pi/pi-coding-agent`) puts + // the launcher in Bun's global bin, which is not on the PATH a GUI process + // inherits — same reason OpenCode scans it. The official installer's + // prebuilt binary (`~/.local/bin`) and Homebrew are covered below. if !home.as_os_str().is_empty() { push_unique_path(&mut search_paths, home.join(".bun").join("bin")); } @@ -3684,7 +3704,7 @@ fn grok_native_update_command(update: String) -> String { fn prefers_official_update(tool: &str, shell: LifecycleCommandShell) -> bool { match shell { LifecycleCommandShell::Posix => { - matches!(tool, "claude" | "opencode" | "openclaw" | "kimi") + matches!(tool, "claude" | "opencode" | "openclaw" | "kimi" | "omp") } LifecycleCommandShell::WindowsBatch => { matches!( @@ -3693,7 +3713,7 @@ fn prefers_official_update(tool: &str, shell: LifecycleCommandShell) -> bool { // 安装方式探测失败弹交互 prompt(spawn npm.cmd 没传 shell:true);静默 // lifecycle 没有 stdin 会挂死,Windows 先锚到包管理器路径,等上游修了 // 再把 opencode 加回这里。 - "claude" | "openclaw" | "kimi" + "claude" | "openclaw" | "kimi" | "omp" ) } } @@ -3774,7 +3794,12 @@ fn package_manager_anchored_command_from_paths( return Some(format!("{} upgrade {formula}", quote_path_if_spaced(&brew))); } let pkg = npm_package_for(tool)?; - match infer_install_source(Path::new(bin_path)) { + let source = infer_install_source(Path::new(bin_path)); + // omp 的 npm 包只能由 bun 运行:除 bun 外的 node 管理器装不出能跑的 omp。 + if tool == "omp" && source != "bun" { + return None; + } + match source { "volta" => { let volta = sibling_bin(bin_path, "volta")?; return Some(format!("{} install {pkg}", quote_path_if_spaced(&volta))); @@ -3860,6 +3885,10 @@ fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) -> return Some(codex_installer_update_command(&install)); } } + // Nix 安装 `omp update` 只提示去改 flake 就 exit 0,执行它会报假成功。 + if tool == "omp" && real_lower.starts_with("/nix/store/") { + return None; + } let package_command = package_manager_anchored_command_from_paths(tool, bin_path, real_target); if is_brew_managed_path(real_target) { return package_command; @@ -3889,6 +3918,10 @@ fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) -> #[cfg(target_os = "windows")] fn package_manager_anchored_command_from_paths(tool: &str, bin_path: &str) -> Option { + // omp 的 npm 包只能由 bun 运行,这里的 npm / Volta / pnpm 都装不出能跑的 omp。 + if tool == "omp" { + return None; + } let pkg = npm_package_for(tool)?; match infer_install_source(Path::new(bin_path)) { @@ -4617,6 +4650,7 @@ fn posix_install_command_for(tool: &str) -> String { "mcode" => installer_with_npm_fallback(MCODE_INSTALL_UNIX, tool), "hermes" => HERMES_INSTALL_UNIX.to_string(), "kimi" => KIMI_INSTALL_UNIX.to_string(), + "omp" => OMP_INSTALL_UNIX.to_string(), "agy" => AGY_INSTALL_UNIX.to_string(), _ => static_fallback_command_for(tool, ToolLifecycleAction::Install), } @@ -7160,17 +7194,25 @@ mod tests { // was not installed by npm..." and the batch header prints // `========== Unknown ==========`. assert_eq!(tool_display_name("omp"), "Oh My Pi"); - // Install and the version check must agree on one package: omp is - // distributed as @oh-my-pi/pi-coding-agent and installed with bun. + // The version check reads this package; npm cannot run it (the entry is + // a bun script), so there is no npm install command at all. assert_eq!(npm_package_for("omp"), Some("@oh-my-pi/pi-coding-agent")); - assert_eq!( - npm_install_command_for("omp"), - Some("bun install -g @oh-my-pi/pi-coding-agent@latest") - ); - // No updater contract is assumed; upgrades stay on the package-manager - // path (an unmanaged standalone binary is reported, not silently - // reinstalled through npm). - assert_eq!(official_update_args("omp"), None); + assert_eq!(npm_install_command_for("omp"), None); + // `omp update` routes every install source itself and never prompts. + assert_eq!(official_update_args("omp"), Some("update")); + #[cfg(not(target_os = "windows"))] + { + assert_eq!(install_command_for("omp"), OMP_INSTALL_UNIX); + assert_eq!( + tool_action_shell_command_for_shell( + "omp", + ToolLifecycleAction::Update, + LifecycleCommandShell::Posix + ) + .as_deref(), + Some(format!("omp update || {OMP_INSTALL_UNIX}").as_str()) + ); + } } #[test] @@ -8873,6 +8915,59 @@ mod tests { ); } + #[test] + fn omp_updates_through_its_own_updater_with_source_specific_fallbacks() { + // Homebrew owns its formula even though `omp update` would also delegate. + assert_eq!( + anchored_command_from_paths( + "omp", + "/opt/homebrew/bin/omp", + "/opt/homebrew/Cellar/omp/14.1.0/bin/omp", + ) + .as_deref(), + Some("/opt/homebrew/bin/brew upgrade omp") + ); + // A bun global keeps bun as the fallback. + assert_eq!( + anchored_command_from_paths( + "omp", + "/Users/me/.bun/bin/omp", + "/Users/me/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/src/cli.ts", + ) + .as_deref(), + Some("/Users/me/.bun/bin/omp update || /Users/me/.bun/bin/bun add -g @oh-my-pi/pi-coding-agent@latest") + ); + // The installer's prebuilt binary updates itself; no npm fallback is invented. + assert_eq!( + anchored_command_from_paths( + "omp", + "/Users/me/.local/bin/omp", + "/Users/me/.local/bin/omp", + ) + .as_deref(), + Some("/Users/me/.local/bin/omp update") + ); + // npm and other node managers cannot run omp, so they never become a fallback. + assert_eq!( + anchored_command_from_paths( + "omp", + "/Users/me/.nvm/versions/node/v22.0.0/bin/omp", + "/Users/me/.nvm/versions/node/v22.0.0/lib/node_modules/@oh-my-pi/pi-coding-agent/src/cli.ts", + ) + .as_deref(), + Some("/Users/me/.nvm/versions/node/v22.0.0/bin/omp update") + ); + // `omp update` exits 0 on Nix without updating: leave it to the user. + assert_eq!( + anchored_command_from_paths( + "omp", + "/Users/me/.nix-profile/bin/omp", + "/nix/store/abc-omp-14.1.0/bin/omp", + ), + None + ); + } + #[test] fn volta_path_with_space_is_quoted() { // volta 分支用 `/volta`,目录含空格时同样要 POSIX 引号包裹。 @@ -10104,9 +10199,8 @@ mod tests { #[test] fn omp_search_paths_include_the_bun_global_bin() { - // omp is distributed only as a Bun global package, and a GUI process does - // not inherit `~/.bun/bin`: without it the Apps page reports the tool as - // not installed and updates lose their anchor. + // A GUI process does not inherit `~/.bun/bin`: without it a Bun global + // omp is reported as not installed and updates lose their anchor. let home = dirs::home_dir().expect("home should resolve"); let bun_bin = home.join(".bun").join("bin"); assert!(build_tool_search_paths("omp").contains(&bun_bin)); diff --git a/src-tauri/src/commands/tool_uninstall.rs b/src-tauri/src/commands/tool_uninstall.rs index a80fc6b7b..0cdd93195 100644 --- a/src-tauri/src/commands/tool_uninstall.rs +++ b/src-tauri/src/commands/tool_uninstall.rs @@ -421,6 +421,8 @@ fn known_native_install(tool: &str, inst: &ToolInstallation) -> bool { "agy" => { normalized.ends_with("/.local/bin/agy") || normalized.ends_with("/agy/bin/agy.exe") } + // Official install.sh (`~/.local/bin`) / install.ps1 (`%LOCALAPPDATA%\omp`) targets. + "omp" => normalized.ends_with("/.local/bin/omp") || normalized.ends_with("/omp/omp.exe"), _ => false, } } @@ -977,6 +979,20 @@ mod tests { assert!(executable.exists()); } + #[test] + fn omp_installer_binary_is_a_known_native_install() { + let temp = tempfile::tempdir().unwrap(); + let bin = temp.path().join(".local/bin"); + std::fs::create_dir_all(&bin).unwrap(); + let binary = bin.join("omp"); + std::fs::write(&binary, [0xcf, 0xfa, 0xed, 0xfe]).unwrap(); + assert!(known_native_install("omp", &installation(&binary))); + // A same-named script elsewhere is never claimed. + let other = temp.path().join("omp"); + std::fs::write(&other, "#!/usr/bin/env bun\n").unwrap(); + assert!(!known_native_install("omp", &installation(&other))); + } + #[test] fn nearby_npm_package_does_not_claim_an_unrelated_executable() { let temp = tempfile::tempdir().unwrap(); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1a629184d..1bb5fe1a7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -4205,7 +4205,7 @@ "pi": "Installs with npm", "mcode": "Installs with the official script, falls back to npm", "agy": "Installs with the official script", - "omp": "Installs with Bun (requires Bun 1.3.14+)" + "omp": "Installs with the official script" }, "ompDiscoveryHelpTitle": "What Oh My Pi actually loads", "ompDiscoveryHelp": "Beyond its own ~/.omp/agent config, Oh My Pi discovers MCP servers, skills, commands, hooks, rules, and settings from other agents' directories (e.g. Claude's ~/.claude, Codex's ~/.codex). To keep what Oh My Pi actually loads in sync with the CC Switch UI, disable unneeded discovery sources via disabledProviders in ~/.omp/agent/config.yml (ids match the docs, e.g. claude, codex, gemini, opencode, github, agents).", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f69da39a5..833a2653e 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -4205,7 +4205,7 @@ "pi": "npm でインストール", "mcode": "公式インストールスクリプトで入れ、失敗したら npm を使います", "agy": "公式インストールスクリプトでインストール", - "omp": "Bun でインストール(Bun 1.3.14+ が必要)" + "omp": "公式インストールスクリプトでインストール" }, "ompDiscoveryHelpTitle": "Oh My Pi が実際に読み込む範囲", "ompDiscoveryHelp": "Oh My Pi は ~/.omp/agent 自身の設定に加え、他の Agent のディレクトリ(Claude の ~/.claude、Codex の ~/.codex など)からも MCP サーバー、スキル、コマンド、フック、ルール、設定を発見します。Oh My Pi の実際の読み込み内容を CC Switch の表示と一致させるには、~/.omp/agent/config.yml の disabledProviders で不要な発行元を無効化してください(id はドキュメント準拠:claude、codex、gemini、opencode、github、agents など)。", diff --git a/src/i18n/locales/tr.json b/src/i18n/locales/tr.json index e4847c531..e9b97bf4c 100644 --- a/src/i18n/locales/tr.json +++ b/src/i18n/locales/tr.json @@ -4113,7 +4113,7 @@ "hermes": "Resmi komut dosyasıyla yüklenir", "pi": "Npm ile yüklenir", "mcode": "Resmi komut dosyasıyla yüklenir, npm'ye geri döner", - "omp": "Bun ile yüklenir (Bun 1.3.14+ gerekir)" + "omp": "Resmi komut dosyasıyla yüklenir" }, "ompDiscoveryHelpTitle": "Oh My Pi'nin gerçekte yükledikleri", "ompDiscoveryHelp": "Oh My Pi, kendi ~/.omp/agent yapılandırmasının yanı sıra diğer ajanların dizinlerinden (ör. Claude'un ~/.claude, Codex'in ~/.codex) MCP sunucularını, becerileri, komutları, kancaları, kuralları ve ayarları da keşfeder. Oh My Pi'nin gerçekte yüklediklerini CC Switch arayüzüyle uyumlu tutmak için ~/.omp/agent/config.yml içinde disabledProviders ile gereksiz keşif kaynaklarını kapatın (kimlikler belgelerle aynıdır, ör. claude, codex, gemini, opencode, github, agents).", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 35caf97bf..1dfc6e250 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -4206,7 +4206,7 @@ "pi": "透過 npm 安裝", "mcode": "安裝時執行官方安裝腳本,失敗時改用 npm", "agy": "安裝時執行官方安裝腳本", - "omp": "透過 Bun 安裝(需 Bun 1.3.14+)" + "omp": "安裝時執行官方安裝腳本" }, "ompDiscoveryHelpTitle": "Oh My Pi 的實際載入範圍", "ompDiscoveryHelp": "Oh My Pi 除了 ~/.omp/agent 自己的設定,還會從其他 Agent 的目錄發現 MCP 伺服器、技能、命令、鉤子、規則和設定(如 Claude 的 ~/.claude、Codex 的 ~/.codex 等)。要讓 Oh My Pi 的實際生效內容與 CC Switch 介面一致,請在 ~/.omp/agent/config.yml 用 disabledProviders 停用不需要的發現來源(id 與文件一致,如 claude、codex、gemini、opencode、github、agents)。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 099f64528..f6c5b9992 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -4205,7 +4205,7 @@ "pi": "通过 npm 安装", "mcode": "安装时运行官方安装脚本,失败时改用 npm", "agy": "安装时运行官方安装脚本", - "omp": "通过 Bun 安装(需 Bun 1.3.14+)" + "omp": "安装时运行官方安装脚本" }, "ompDiscoveryHelpTitle": "Oh My Pi 的实际加载范围", "ompDiscoveryHelp": "Oh My Pi 除了 ~/.omp/agent 自己的配置,还会从其他 Agent 的目录发现 MCP 服务器、技能、命令、钩子、规则和设置(如 Claude 的 ~/.claude、Codex 的 ~/.codex 等)。要让 Oh My Pi 的实际生效内容与 CC Switch 界面一致,请在 ~/.omp/agent/config.yml 用 disabledProviders 关闭不需要的发现来源(id 与文档一致,如 claude、codex、gemini、opencode、github、agents)。",