fix(omp): install and update Oh My Pi without assuming Bun

omp also ships as a prebuilt binary (official installer, Homebrew), and
its npm package only runs under Bun. Install now runs the official
installer, which uses Bun when present and the verified binary otherwise.
Updates go through `omp update`, which routes every install source
itself: Homebrew keeps `brew upgrade`, Bun keeps `bun add -g` as the
fallback, Nix is left to the user, and npm is never offered. The
installer's binary is now recognized for uninstall.
This commit is contained in:
Jason authored and Jason Young committed 2026-10-24 15:30:32 +08:00
1 parent 9bacd1d172
commit d9422e7d6a
7 files changed
+142 -32

No files matched your search

+121 -27
View File
@@ -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 || <fallback>` 的兜底永不触发。
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<String> {
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<std::path::PathBuf> {
}
}
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<String> {
// 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 {
// <path> 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 分支用 `<dir>/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));
+16
View File
@@ -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();
+1 -1
View File
@@ -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).",
+1 -1
View File
@@ -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 など)。",
+1 -1
View File
@@ -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).",
+1 -1
View File
@@ -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)。",
+1 -1
View File
@@ -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)。",