mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(tools): prefer xAI native Grok installer with npm fallback
Treat Grok like Claude/OpenCode: install via the official xAI installer (POSIX shell / Windows PowerShell), keep npm as fallback, discover ~/.grok/bin, and anchor updates to `grok update` only for native installs.
This commit is contained in:
+192
-28
@@ -441,6 +441,8 @@ const CLAUDE_INSTALL_UNIX: &str =
|
|||||||
"bash -c 'tmp=$(mktemp) && curl -fsSL https://claude.ai/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
|
"bash -c 'tmp=$(mktemp) && curl -fsSL https://claude.ai/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
|
||||||
const OPENCODE_INSTALL_UNIX: &str =
|
const OPENCODE_INSTALL_UNIX: &str =
|
||||||
"bash -c 'tmp=$(mktemp) && curl -fsSL https://opencode.ai/install -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
|
"bash -c 'tmp=$(mktemp) && curl -fsSL https://opencode.ai/install -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
|
||||||
|
const GROK_INSTALL_UNIX: &str =
|
||||||
|
"bash -c 'tmp=$(mktemp) && curl -fsSL https://x.ai/cli/install.sh -o $tmp && bash $tmp; status=$?; rm -f $tmp; exit $status'";
|
||||||
|
|
||||||
/// Hermes 官方安装器会自带/选择合适的 Python 运行时。不要再用
|
/// Hermes 官方安装器会自带/选择合适的 Python 运行时。不要再用
|
||||||
/// `python3 -m pip ... || python -m pip ...`:Hermes PyPI 包要求 Python >=3.11,
|
/// `python3 -m pip ... || python -m pip ...`:Hermes PyPI 包要求 Python >=3.11,
|
||||||
@@ -454,6 +456,8 @@ const HERMES_UPDATE_UNIX: &str =
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const HERMES_INSTALL_WINDOWS_SCRIPT: &str =
|
const HERMES_INSTALL_WINDOWS_SCRIPT: &str =
|
||||||
"irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex";
|
"irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex";
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const GROK_INSTALL_WINDOWS_SCRIPT: &str = "irm https://x.ai/cli/install.ps1 | iex";
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn powershell_encoded_command(script: &str) -> String {
|
fn powershell_encoded_command(script: &str) -> String {
|
||||||
@@ -474,6 +478,14 @@ fn hermes_install_windows_command() -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn grok_install_windows_command() -> String {
|
||||||
|
format!(
|
||||||
|
"powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand {}",
|
||||||
|
powershell_encoded_command(GROK_INSTALL_WINDOWS_SCRIPT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn hermes_update_windows_command() -> String {
|
fn hermes_update_windows_command() -> String {
|
||||||
// fallback 是 powershell.exe,不是 .cmd/.bat;这里不需要 `call`。PowerShell 的
|
// fallback 是 powershell.exe,不是 .cmd/.bat;这里不需要 `call`。PowerShell 的
|
||||||
@@ -502,7 +514,7 @@ fn npm_install_command_for(tool: &str) -> Option<&'static str> {
|
|||||||
|
|
||||||
fn official_update_args(tool: &str) -> Option<&'static str> {
|
fn official_update_args(tool: &str) -> Option<&'static str> {
|
||||||
match tool {
|
match tool {
|
||||||
"claude" | "codex" | "hermes" => Some("update"),
|
"claude" | "codex" | "grok" | "hermes" => Some("update"),
|
||||||
"openclaw" => Some("update --yes"),
|
"openclaw" => Some("update --yes"),
|
||||||
"opencode" => Some("upgrade"),
|
"opencode" => Some("upgrade"),
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -534,6 +546,20 @@ fn tool_action_shell_command_for_shell(
|
|||||||
action: ToolLifecycleAction,
|
action: ToolLifecycleAction,
|
||||||
shell: LifecycleCommandShell,
|
shell: LifecycleCommandShell,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
|
// xAI's primary Windows distribution is the native PowerShell installer.
|
||||||
|
// Keep npm as the network/policy fallback, matching the POSIX installer chain.
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if tool == "grok"
|
||||||
|
&& matches!(action, ToolLifecycleAction::Install)
|
||||||
|
&& matches!(shell, LifecycleCommandShell::WindowsBatch)
|
||||||
|
{
|
||||||
|
return Some(chain_update_commands(
|
||||||
|
grok_install_windows_command(),
|
||||||
|
npm_install_command_for(tool)?.to_string(),
|
||||||
|
shell,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if tool == "hermes" {
|
if tool == "hermes" {
|
||||||
return Some(
|
return Some(
|
||||||
match (action, shell) {
|
match (action, shell) {
|
||||||
@@ -1454,6 +1480,22 @@ fn opencode_extra_search_paths(
|
|||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grok's official installer writes the launcher to `$GROK_BIN_DIR` or, by
|
||||||
|
/// default, `~/.grok/bin`. Keep these ahead of generic npm/Node locations so
|
||||||
|
/// version probing and anchored updates can see the native distribution even
|
||||||
|
/// when the GUI process inherited a stale PATH.
|
||||||
|
fn grok_extra_search_paths(
|
||||||
|
home: &Path,
|
||||||
|
grok_bin_dir: Option<std::ffi::OsString>,
|
||||||
|
) -> Vec<std::path::PathBuf> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
push_env_single_dir(&mut paths, grok_bin_dir);
|
||||||
|
if !home.as_os_str().is_empty() {
|
||||||
|
push_unique_path(&mut paths, home.join(".grok").join("bin"));
|
||||||
|
}
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec<std::path::PathBuf> {
|
fn tool_executable_candidates(tool: &str, dir: &Path) -> Vec<std::path::PathBuf> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
@@ -1503,6 +1545,12 @@ fn build_tool_search_paths(tool: &str) -> Vec<std::path::PathBuf> {
|
|||||||
|
|
||||||
// 常见的安装路径(原生安装优先)
|
// 常见的安装路径(原生安装优先)
|
||||||
let mut search_paths: Vec<std::path::PathBuf> = Vec::new();
|
let mut search_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||||
|
if tool == "grok" {
|
||||||
|
let extra_paths = grok_extra_search_paths(&home, std::env::var_os("GROK_BIN_DIR"));
|
||||||
|
for path in extra_paths {
|
||||||
|
push_unique_path(&mut search_paths, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
if !home.as_os_str().is_empty() {
|
if !home.as_os_str().is_empty() {
|
||||||
push_unique_path(&mut search_paths, home.join(".local/bin"));
|
push_unique_path(&mut search_paths, home.join(".local/bin"));
|
||||||
push_unique_path(&mut search_paths, home.join(".npm-global/bin"));
|
push_unique_path(&mut search_paths, home.join(".npm-global/bin"));
|
||||||
@@ -1987,6 +2035,19 @@ fn brew_formula_from_path(real: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// xAI's native installer uses `~/.grok/bin` for its launchers and
|
||||||
|
/// `~/.grok/downloads/grok-<platform>` for the downloaded binary. The launcher
|
||||||
|
/// directory also covers the default Windows layout, where the executable is
|
||||||
|
/// copied instead of symlinked. Checking the real target additionally supports
|
||||||
|
/// a custom `$GROK_BIN_DIR` on POSIX, whose launcher still points into the
|
||||||
|
/// standard downloads directory.
|
||||||
|
fn is_grok_native_install(bin_path: &str, real_target: &str) -> bool {
|
||||||
|
[bin_path, real_target].iter().any(|path| {
|
||||||
|
let normalized = path.replace('\\', "/").to_ascii_lowercase();
|
||||||
|
normalized.contains("/.grok/bin/") || normalized.contains("/.grok/downloads/grok-")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 含空格才用 POSIX 单引号包一层,否则保持裸路径——命令展示更干净。
|
/// 含空格才用 POSIX 单引号包一层,否则保持裸路径——命令展示更干净。
|
||||||
/// claude / brew / volta / bun / npm 五个锚定分支共用,避免"含空格"判定漂移。
|
/// claude / brew / volta / bun / npm 五个锚定分支共用,避免"含空格"判定漂移。
|
||||||
///
|
///
|
||||||
@@ -2275,14 +2336,14 @@ fn package_manager_anchored_command_from_paths(
|
|||||||
/// 判定顺序(命中即返回):
|
/// 判定顺序(命中即返回):
|
||||||
/// ① Hermes → `<bin_path 绝对> update`;Hermes CLI 自己知道安装环境,避免 cc-switch
|
/// ① Hermes → `<bin_path 绝对> update`;Hermes CLI 自己知道安装环境,避免 cc-switch
|
||||||
/// 猜系统 `python3`/`python` 时撞上 Python 版本或 pyenv shim 问题。
|
/// 猜系统 `python3`/`python` 时撞上 Python 版本或 pyenv shim 问题。
|
||||||
/// ② Claude 原生安装器(`~/.local/share/claude/versions/`)→ `<bin_path 绝对> update`;
|
/// ② Claude / Grok 原生安装器 → `<bin_path 绝对> update`;
|
||||||
/// bin_path 指向 launcher,launcher 内部 dispatch update 子命令。它不归 npm 管,
|
/// bin_path 指向 launcher,launcher 内部 dispatch update 子命令。它不归 npm 管,
|
||||||
/// 且在 PATH 里比 nvm/homebrew 更靠前,用 npm 升级会装到别处且被原生那份遮蔽。
|
/// 且在 PATH 里比 nvm/homebrew 更靠前,用 npm 升级会装到别处且被原生那份遮蔽。
|
||||||
/// ③ Homebrew formula(真身在 `Cellar/<formula>/`)→ `<bin_path 同目录>/brew upgrade <formula>`;
|
/// ③ Homebrew formula(真身在 `Cellar/<formula>/`)→ `<bin_path 同目录>/brew upgrade <formula>`;
|
||||||
/// formula 由 Homebrew 拥有,避免 self-update 尝试改动包管理器管理的安装。
|
/// formula 由 Homebrew 拥有,避免 self-update 尝试改动包管理器管理的安装。
|
||||||
/// ④ 其余支持官方自升级的工具 → `<bin_path 绝对> update/upgrade || <原锚定包管理器命令>`;
|
/// ④ 其余支持官方自升级的工具 → `<bin_path 绝对> update/upgrade || <原锚定包管理器命令>`;
|
||||||
/// Codex 的 self-update 只在部分 release 可用,所以保留 npm/brew/bun/volta fallback。
|
/// Codex 的 self-update 只在部分 release 可用,所以保留 npm/brew/bun/volta fallback。
|
||||||
/// ⑤ 不支持官方自升级的 npm 全局包(例如 Gemini CLI / Grok Build) → 锚定到
|
/// ⑤ 不支持官方自升级的 npm 全局包(例如 Gemini CLI,以及非 native 的 Grok Build) → 锚定到
|
||||||
/// "那处 bin 目录的 npm"。
|
/// "那处 bin 目录的 npm"。
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) -> Option<String> {
|
fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) -> Option<String> {
|
||||||
@@ -2297,6 +2358,9 @@ fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) ->
|
|||||||
{
|
{
|
||||||
return anchored_official_update_command(tool, bin_path);
|
return anchored_official_update_command(tool, bin_path);
|
||||||
}
|
}
|
||||||
|
if tool == "grok" && is_grok_native_install(bin_path, real_target) {
|
||||||
|
return anchored_official_update_command(tool, bin_path);
|
||||||
|
}
|
||||||
let package_command = package_manager_anchored_command_from_paths(tool, bin_path, real_target);
|
let package_command = package_manager_anchored_command_from_paths(tool, bin_path, real_target);
|
||||||
if brew_formula_from_path(real_target).is_some() {
|
if brew_formula_from_path(real_target).is_some() {
|
||||||
return package_command;
|
return package_command;
|
||||||
@@ -2344,7 +2408,7 @@ fn package_manager_anchored_command_from_paths(tool: &str, bin_path: &str) -> Op
|
|||||||
|
|
||||||
/// Windows 版锚定命令生成。对平台确认可静默运行的工具优先使用官方 CLI 自升级;
|
/// Windows 版锚定命令生成。对平台确认可静默运行的工具优先使用官方 CLI 自升级;
|
||||||
/// 对 npm/Volta/pnpm 这类可确认写回位置的安装,再接一个包管理器 fallback。不存在 brew/bun/claude-native
|
/// 对 npm/Volta/pnpm 这类可确认写回位置的安装,再接一个包管理器 fallback。不存在 brew/bun/claude-native
|
||||||
/// (Windows 没 Homebrew、Bun for Windows 仍 preview、claude.ai/install.sh 是 bash 脚本)。
|
/// (Windows 没 Homebrew、Bun for Windows 仍 preview;Grok native 使用 PowerShell installer)。
|
||||||
/// Scoop/Chocolatey/winget/nvm-windows/MS Store node 都归 npm 类——它们都只是"如何装
|
/// Scoop/Chocolatey/winget/nvm-windows/MS Store node 都归 npm 类——它们都只是"如何装
|
||||||
/// node"的不同入口,全局包真正的 idiom 仍是 sibling `npm.cmd`。
|
/// node"的不同入口,全局包真正的 idiom 仍是 sibling `npm.cmd`。
|
||||||
///
|
///
|
||||||
@@ -2353,16 +2417,15 @@ fn package_manager_anchored_command_from_paths(tool: &str, bin_path: &str) -> Op
|
|||||||
/// 装 `.cmd`、Volta 装 `.exe`,纯字符串拼接无法消歧。这一平台差异**被刻意保留**:
|
/// 装 `.cmd`、Volta 装 `.exe`,纯字符串拼接无法消歧。这一平台差异**被刻意保留**:
|
||||||
/// 测试用 tempdir 隔离 fs,生产侧 TOCTOU 是 by design(见 `sibling_bin_with_ext` doc)。
|
/// 测试用 tempdir 隔离 fs,生产侧 TOCTOU 是 by design(见 `sibling_bin_with_ext` doc)。
|
||||||
///
|
///
|
||||||
/// `_real_target` 占位维持与 POSIX 版的签名对称——Windows 上未观测到需要真身路径
|
/// `real_target` 维持与 POSIX 版的签名对称,并辅助识别 Grok native。若未来加 Scoop
|
||||||
/// 区分的等价类(无 Cellar、无 claude-native installer)。若未来加 Scoop persist 锚定
|
/// persist 锚定(scoop 装的工具真身在 `<scoop_root>/persist/<app>/...`),也从这里取真身。
|
||||||
/// (scoop 装的工具真身在 `<scoop_root>/persist/<app>/...`),从这里启用 `_real_target`。
|
|
||||||
///
|
///
|
||||||
/// **关键不变量同 POSIX 版:返回的命令必须用绝对路径,不依赖 PATH**。Windows GUI
|
/// **关键不变量同 POSIX 版:返回的命令必须用绝对路径,不依赖 PATH**。Windows GUI
|
||||||
/// 进程 PATH 由 Service Control Manager / explorer.exe 给,通常不含用户 `%LOCALAPPDATA%`
|
/// 进程 PATH 由 Service Control Manager / explorer.exe 给,通常不含用户 `%LOCALAPPDATA%`
|
||||||
/// 下的 Volta/pnpm 路径;`$SHELL -lic` 的探测时 PATH 与执行时 PATH 不对称。
|
/// 下的 Volta/pnpm 路径;`$SHELL -lic` 的探测时 PATH 与执行时 PATH 不对称。
|
||||||
///
|
///
|
||||||
/// 判定顺序(命中即返回):
|
/// 判定顺序(命中即返回):
|
||||||
/// ① hermes → `<bin_path> update`;Hermes CLI 自己处理安装环境。
|
/// ① hermes / Grok native → `<bin_path> update`;CLI 自己处理安装环境。
|
||||||
/// ② 支持官方自升级且 Windows 可安全静默执行的工具 → `<bin_path> update/upgrade || call <包管理器 fallback>`。
|
/// ② 支持官方自升级且 Windows 可安全静默执行的工具 → `<bin_path> update/upgrade || call <包管理器 fallback>`。
|
||||||
/// ③ 其余 npm 工具 → sibling `npm.cmd`/`.exe` i -g <pkg>@latest。
|
/// ③ 其余 npm 工具 → sibling `npm.cmd`/`.exe` i -g <pkg>@latest。
|
||||||
///
|
///
|
||||||
@@ -2370,10 +2433,13 @@ fn package_manager_anchored_command_from_paths(tool: &str, bin_path: &str) -> Op
|
|||||||
/// 扩展名存在时,支持官方自升级的工具仍返回 `<bin_path> update/upgrade`,其余工具
|
/// 扩展名存在时,支持官方自升级的工具仍返回 `<bin_path> update/upgrade`,其余工具
|
||||||
/// 才返 None 让上游兜回静态命令、`anchored=false`。
|
/// 才返 None 让上游兜回静态命令、`anchored=false`。
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn anchored_command_from_paths(tool: &str, bin_path: &str, _real_target: &str) -> Option<String> {
|
fn anchored_command_from_paths(tool: &str, bin_path: &str, real_target: &str) -> Option<String> {
|
||||||
if tool == "hermes" {
|
if tool == "hermes" {
|
||||||
return anchored_official_update_command(tool, bin_path);
|
return anchored_official_update_command(tool, bin_path);
|
||||||
}
|
}
|
||||||
|
if tool == "grok" && is_grok_native_install(bin_path, real_target) {
|
||||||
|
return anchored_official_update_command(tool, bin_path);
|
||||||
|
}
|
||||||
let package_command = package_manager_anchored_command_from_paths(tool, bin_path);
|
let package_command = package_manager_anchored_command_from_paths(tool, bin_path);
|
||||||
if prefers_official_update(tool, LifecycleCommandShell::WindowsBatch) {
|
if prefers_official_update(tool, LifecycleCommandShell::WindowsBatch) {
|
||||||
let update = anchored_official_update_command(tool, bin_path)?;
|
let update = anchored_official_update_command(tool, bin_path)?;
|
||||||
@@ -2444,17 +2510,17 @@ fn static_fallback_command(tool: &str) -> String {
|
|||||||
///
|
///
|
||||||
/// 设计理由:
|
/// 设计理由:
|
||||||
/// - install 没有锚点可言(从无到有),但**有"上游推荐方式"这一事实** ——
|
/// - install 没有锚点可言(从无到有),但**有"上游推荐方式"这一事实** ——
|
||||||
/// Anthropic 和 SST(OpenCode)都已将自家 native installer 列为首推、把 npm 列为传统方式。
|
/// Anthropic、xAI 和 SST(OpenCode)都已将自家 native installer 列为首推、把 npm 列为替代方式。
|
||||||
/// 把这层认知补进来,让 install 表与 update 端的锚定决策树共用同一份"上游事实"。
|
/// 把这层认知补进来,让 install 表与 update 端的锚定决策树共用同一份"上游事实"。
|
||||||
/// - Hermes 使用官方 installer,避免用系统 Python/pip 安装时踩 Python >=3.11 与 pyenv
|
/// - Hermes 使用官方 installer,避免用系统 Python/pip 安装时踩 Python >=3.11 与 pyenv
|
||||||
/// `python` shim 问题;更新路径若能锚定已安装 CLI,则走 `<hermes> update`。
|
/// `python` shim 问题;更新路径若能锚定已安装 CLI,则走 `<hermes> update`。
|
||||||
/// **Hermes 没有 npm 包,install 端不享受 `||` 降级**——上游 installer 不可达就只能等。
|
/// **Hermes 没有 npm 包,install 端不享受 `||` 降级**——上游 installer 不可达就只能等。
|
||||||
/// - 对**有 npm 包**的工具(claude/opencode),短路链(POSIX `||`)保证官方脚本不可达/
|
/// - 对**有 npm 包**的工具(claude/grok/opencode),短路链(POSIX `||`)保证官方脚本不可达/
|
||||||
/// 防火墙拦截时仍能装上,降级到裸 `npm i -g`。官方脚本本身不用 pipe,
|
/// 防火墙拦截时仍能装上,降级到裸 `npm i -g`。官方脚本本身不用 pipe,
|
||||||
/// 所以这条路径在 WSL 的 `sh -c` 子 shell 中也不依赖外层 `pipefail`。
|
/// 所以这条路径在 WSL 的 `sh -c` 子 shell 中也不依赖外层 `pipefail`。
|
||||||
/// - Windows 原生不启用:claude.ai/install.sh、opencode.ai/install 都是 bash 脚本,
|
/// - Windows 上 Claude/OpenCode 原生不启用(对应 installer 都是 bash 脚本);Grok
|
||||||
/// Windows 原生继续走 `tool_action_shell_command` 的 npm/PowerShell 命令;WSL 作为
|
/// 使用官方 PowerShell installer,并同样保留 npm fallback。WSL 作为 Linux 环境
|
||||||
/// Linux 环境复用这套 POSIX 安装优先级。
|
/// 复用这套 POSIX 安装优先级。
|
||||||
fn installer_with_npm_fallback(installer: &str, tool: &str) -> String {
|
fn installer_with_npm_fallback(installer: &str, tool: &str) -> String {
|
||||||
match npm_install_command_for(tool) {
|
match npm_install_command_for(tool) {
|
||||||
Some(npm) => chain_update_commands(
|
Some(npm) => chain_update_commands(
|
||||||
@@ -2469,6 +2535,7 @@ fn installer_with_npm_fallback(installer: &str, tool: &str) -> String {
|
|||||||
fn posix_install_command_for(tool: &str) -> String {
|
fn posix_install_command_for(tool: &str) -> String {
|
||||||
match tool {
|
match tool {
|
||||||
"claude" => installer_with_npm_fallback(CLAUDE_INSTALL_UNIX, tool),
|
"claude" => installer_with_npm_fallback(CLAUDE_INSTALL_UNIX, tool),
|
||||||
|
"grok" => installer_with_npm_fallback(GROK_INSTALL_UNIX, tool),
|
||||||
"opencode" => installer_with_npm_fallback(OPENCODE_INSTALL_UNIX, tool),
|
"opencode" => installer_with_npm_fallback(OPENCODE_INSTALL_UNIX, tool),
|
||||||
"hermes" => HERMES_INSTALL_UNIX.to_string(),
|
"hermes" => HERMES_INSTALL_UNIX.to_string(),
|
||||||
_ => static_fallback_command_for(tool, ToolLifecycleAction::Install),
|
_ => static_fallback_command_for(tool, ToolLifecycleAction::Install),
|
||||||
@@ -3697,23 +3764,27 @@ mod tests {
|
|||||||
npm_install_command_for("grok"),
|
npm_install_command_for("grok"),
|
||||||
Some("npm i -g @xai-official/grok@latest")
|
Some("npm i -g @xai-official/grok@latest")
|
||||||
);
|
);
|
||||||
assert_eq!(official_update_args("grok"), None);
|
assert_eq!(official_update_args("grok"), Some("update"));
|
||||||
|
|
||||||
for shell in [
|
for action in [ToolLifecycleAction::Install, ToolLifecycleAction::Update] {
|
||||||
LifecycleCommandShell::Posix,
|
|
||||||
LifecycleCommandShell::WindowsBatch,
|
|
||||||
] {
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tool_action_shell_command_for_shell("grok", ToolLifecycleAction::Install, shell,)
|
tool_action_shell_command_for_shell("grok", action, LifecycleCommandShell::Posix)
|
||||||
.as_deref(),
|
|
||||||
Some("npm i -g @xai-official/grok@latest")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tool_action_shell_command_for_shell("grok", ToolLifecycleAction::Update, shell,)
|
|
||||||
.as_deref(),
|
.as_deref(),
|
||||||
Some("npm i -g @xai-official/grok@latest")
|
Some("npm i -g @xai-official/grok@latest")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static update remains package-manager based. Only a path positively
|
||||||
|
// identified as xAI's native install may run `grok update`.
|
||||||
|
assert_eq!(
|
||||||
|
tool_action_shell_command_for_shell(
|
||||||
|
"grok",
|
||||||
|
ToolLifecycleAction::Update,
|
||||||
|
LifecycleCommandShell::WindowsBatch,
|
||||||
|
)
|
||||||
|
.as_deref(),
|
||||||
|
Some("npm i -g @xai-official/grok@latest")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3971,6 +4042,35 @@ mod tests {
|
|||||||
assert_eq!(cmd.as_deref(), Some(expected.as_str()));
|
assert_eq!(cmd.as_deref(), Some(expected.as_str()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_native_windows_uses_self_update() {
|
||||||
|
let (_dir, _sub, bin_path) = setup_sibling(".grok/bin", "grok.exe", &["npm.cmd"]);
|
||||||
|
let cmd = anchored_command_from_paths("grok", &bin_path, &bin_path);
|
||||||
|
let expected = format!("{} update", expect_quoted_path(&bin_path));
|
||||||
|
assert_eq!(cmd.as_deref(), Some(expected.as_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_windows_install_prefers_powershell_with_npm_fallback() {
|
||||||
|
let install = static_fallback_command_for("grok", ToolLifecycleAction::Install);
|
||||||
|
let native = grok_install_windows_command();
|
||||||
|
assert!(
|
||||||
|
install.starts_with(&native),
|
||||||
|
"native installer first: {install}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
install.ends_with("|| call npm i -g @xai-official/grok@latest"),
|
||||||
|
"npm fallback should remain available: {install}"
|
||||||
|
);
|
||||||
|
let expected_encoded = powershell_encoded_command(GROK_INSTALL_WINDOWS_SCRIPT);
|
||||||
|
assert_eq!(
|
||||||
|
native
|
||||||
|
.split_once("-EncodedCommand ")
|
||||||
|
.map(|(_, encoded)| encoded),
|
||||||
|
Some(expected_encoded.as_str())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn windows_no_sibling_uses_cli_update_without_package_fallback() {
|
fn windows_no_sibling_uses_cli_update_without_package_fallback() {
|
||||||
// sibling 包管理器不存在(纯独立二进制)时,仍可锚定到 CLI 自身跑官方 update。
|
// sibling 包管理器不存在(纯独立二进制)时,仍可锚定到 CLI 自身跑官方 update。
|
||||||
@@ -4293,7 +4393,13 @@ mod tests {
|
|||||||
assert_eq!(codex, "npm i -g @openai/codex@latest");
|
assert_eq!(codex, "npm i -g @openai/codex@latest");
|
||||||
|
|
||||||
let grok = wsl_tool_action_shell_command("grok", ToolLifecycleAction::Install).unwrap();
|
let grok = wsl_tool_action_shell_command("grok", ToolLifecycleAction::Install).unwrap();
|
||||||
assert_eq!(grok, "npm i -g @xai-official/grok@latest");
|
assert!(
|
||||||
|
grok.starts_with(
|
||||||
|
"bash -c 'tmp=$(mktemp) && curl -fsSL https://x.ai/cli/install.sh "
|
||||||
|
) && grok.contains(" || npm i -g @xai-official/grok@latest"),
|
||||||
|
"WSL grok install should prefer native POSIX installer with npm fallback: {grok}"
|
||||||
|
);
|
||||||
|
assert!(!grok.contains("| bash"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4410,6 +4516,28 @@ mod tests {
|
|||||||
assert_eq!(cmd.as_deref(), Some("/Users/me/.local/bin/claude update"));
|
assert_eq!(cmd.as_deref(), Some("/Users/me/.local/bin/claude update"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_native_installer_uses_self_update() {
|
||||||
|
// ~/.grok/bin/grok is a launcher symlink into ~/.grok/downloads.
|
||||||
|
// Updating it through npm would create or mutate a different install.
|
||||||
|
let cmd = anchored_command_from_paths(
|
||||||
|
"grok",
|
||||||
|
"/Users/me/.grok/bin/grok",
|
||||||
|
"/Users/me/.grok/downloads/grok-macos-aarch64",
|
||||||
|
);
|
||||||
|
assert_eq!(cmd.as_deref(), Some("/Users/me/.grok/bin/grok update"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_custom_bin_dir_is_native_when_target_is_official_download() {
|
||||||
|
let cmd = anchored_command_from_paths(
|
||||||
|
"grok",
|
||||||
|
"/Users/me/bin/grok",
|
||||||
|
"/Users/me/.grok/downloads/grok-macos-aarch64",
|
||||||
|
);
|
||||||
|
assert_eq!(cmd.as_deref(), Some("/Users/me/bin/grok update"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gemini_homebrew_formula_uses_brew_upgrade() {
|
fn gemini_homebrew_formula_uses_brew_upgrade() {
|
||||||
// /opt/homebrew/bin/gemini → Cellar/gemini-cli/...:是 brew formula 而非 npm 全局包,
|
// /opt/homebrew/bin/gemini → Cellar/gemini-cli/...:是 brew formula 而非 npm 全局包,
|
||||||
@@ -4975,9 +5103,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grok_install_keeps_static_npm() {
|
fn grok_install_prefers_native_with_npm_fallback() {
|
||||||
let cmd = install_command_for("grok");
|
let cmd = install_command_for("grok");
|
||||||
assert_eq!(cmd, "npm i -g @xai-official/grok@latest");
|
assert!(
|
||||||
|
cmd.contains("https://x.ai/cli/install.sh"),
|
||||||
|
"should include official installer URL: {cmd}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cmd.contains("@xai-official/grok@latest"),
|
||||||
|
"should keep npm package as fallback: {cmd}"
|
||||||
|
);
|
||||||
|
let parts: Vec<&str> = cmd.split("||").collect();
|
||||||
|
assert_eq!(parts.len(), 2, "should be a two-step short-circuit chain");
|
||||||
|
assert!(parts[0].contains("install.sh"), "native first: {cmd}");
|
||||||
|
assert!(
|
||||||
|
!parts[0].contains('|'),
|
||||||
|
"native installer should avoid pipe: {cmd}"
|
||||||
|
);
|
||||||
|
assert!(parts[1].contains("npm i -g"), "npm second: {cmd}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5149,6 +5292,27 @@ mod tests {
|
|||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_extra_search_paths_prefers_override_then_default_native_dir() {
|
||||||
|
let home = PathBuf::from("/home/tester");
|
||||||
|
let paths =
|
||||||
|
grok_extra_search_paths(&home, Some(std::ffi::OsString::from("/custom/grok/bin")));
|
||||||
|
|
||||||
|
assert_eq!(paths[0], PathBuf::from("/custom/grok/bin"));
|
||||||
|
assert_eq!(paths[1], PathBuf::from("/home/tester/.grok/bin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_extra_search_paths_deduplicates_default_override() {
|
||||||
|
let home = PathBuf::from("/home/tester");
|
||||||
|
let paths = grok_extra_search_paths(
|
||||||
|
&home,
|
||||||
|
Some(std::ffi::OsString::from("/home/tester/.grok/bin")),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(paths, vec![PathBuf::from("/home/tester/.grok/bin")]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_path_env_search_paths_include_path_entries_and_dedupe() {
|
fn cli_path_env_search_paths_include_path_entries_and_dedupe() {
|
||||||
let temp = tempfile::tempdir().expect("temp dir should be created");
|
let temp = tempfile::tempdir().expect("temp dir should be created");
|
||||||
|
|||||||
Reference in New Issue
Block a user