Files
my-pi/update.sh
T
云服务部-叶林立 dec48dab6d feat: 新增系统提示词封面,并修正工具结果投影与常驻工具
系统提示词封面(新增能力):

- extensions/system-prompt-cover.ts:读取 ~/.pi/agent/SYSTEM_COVER.md,
  在 before_agent_start 返回 { systemPrompt },用文本拼接替换 Pi 默认
  系统提示词的开头,同时保留 tools / rules / docs / skills /
  project_context / cwd 全部分区及其原始顺序。
- 必须走 forceSystemPrompt 投影路径。customPrompt 选项会连带抑制
  tools / rules / docs 三个分区,而回填只能追加到末尾,无法恢复原顺序;
  且 forceSystemPrompt 优先级更高,二者同时存在时 customPrompt 会被
  静默忽略,因此 handler 不得写 customPrompt。
- 分割边界要求独占一行的裸标签:<project_instructions path="..."> 嵌套在
  <project_context> 内,只有裸标签才是分区边界。
- 注册在 tool-routing 之前,否则提示词已被压平,改 systemPromptOptions
  不再影响本轮请求。
- scripts/deploy-system-prompt-cover.mjs:同目录临时文件 + rename 原子
  覆盖,不保留备份;install.sh 从脚本目录、update.sh 从升级后组合包
  定位 helper,禁止回退到可能陈旧的脚本目录。
- 封面缺失、不可读或空白时静默 no-op,Pi 行为保持不变;封面每轮重新
  读取,修改后在下一轮生效,不影响已结束的历史对话。

pi-compaction 投影修正:

- 仅在「摘要 + Source 行」严格短于原文时才替换工具结果。此前命中即
  无条件替换,小结果反而被包壳放大;未替换的结果完整保留原始 content
  parts(含非文本部分)。
- deterministic.maxStubTokens 由 256 提升到 1024。

pi-tool-search 常驻工具:

- alwaysEnabled 增加 context_tree_query:恢复被投影的工具证据不应依赖
  一次 tool_search 激活,常驻同时保持请求前缀稳定。
- BUNDLE_TOOL_SEARCH_DEFAULTS_VERSION 由 3 提升到 4;
  scripts/migrate-tool-search-settings.mjs 新增 version-3 快照常量,
  使其与 version-2 一样可被完整精确匹配并迁移。
- 权限策略为 context_tree_query 显式 allow:它只读扩展自己的 segment
  索引、不触碰工作区,常驻后不应每轮触发 AutoReview 往返。该种子仅在
  全新安装时部署,已存在的用户 policy.jsonc 不会被覆盖。

文档与测试同步:

- AGENTS.md:新增 system-prompt-cover 开发约定,修正加载顺序、
  bundleDefaultsVersion、可迁移旧快照集合与常驻工具范围的描述。
- pi-tool-search 的 README / CHANGELOG 同步 version 4 与新增常驻工具。
- tests/system-prompt-cover.test.ts:覆盖顺序保持、尾部逐字节不变、
  嵌套 project_instructions 不误判、部署幂等与覆盖、缺源时非零退出。
- 修正 pi-tool-search/test/config.test.ts 与
  tests/tool-search-settings-migration.test.ts 中写死的 version 3 与
  工具列表断言。
- update-git-clean-recovery / tool-search-settings-migration 的 mock
  组合包补拷 scripts/,与真实发布包结构一致,避免 helper 定位失败。

测试:根 61/61、pi-compaction 36/36 + tsc、pi-tool-search 16/16 + tsc、
pi-permission-system 2577/2577;sh -n 与 git diff --check 通过。
2026-10-15 18:02:03 -07:00

491 lines
17 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -u
SCRIPT_DIR=$(CDPATH=; cd "$(dirname "$0")" && pwd) || exit 1
MANAGED_CONFIG_HELPERS=$SCRIPT_DIR/scripts/managed-config.sh
if [ ! -f "$MANAGED_CONFIG_HELPERS" ]; then
printf '错误: 找不到受管配置辅助脚本:%s\n' "$MANAGED_CONFIG_HELPERS" >&2
exit 1
fi
# shellcheck source=scripts/managed-config.sh
. "$MANAGED_CONFIG_HELPERS"
PACKAGE_SOURCE=${PI_PACKAGE_SOURCE:-git:git@bitbucket.org:siakitem/my-pi.git}
POWERLEVEL10K_PRESET_FILE=$SCRIPT_DIR/config/p10k.zsh
CODEGRAPH_INSTALL_URL=https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh
CODEGRAPH_LATEST_RELEASE_URL=https://github.com/colbymchenry/codegraph/releases/latest
KITTY_INSTALL_URL=https://sw.kovidgoyal.net/kitty/installer.sh
KITTY_VERSION_URL=https://sw.kovidgoyal.net/kitty/current-version.txt
update_failed=0
brew_metadata_refreshed=0
say() {
printf '%s\n' "$*"
}
warn() {
printf '警告: %s\n' "$*" >&2
}
record_update_failure() {
warn "$1 升级或配置同步失败;其余已安装组件仍会继续处理。"
update_failed=1
}
select_updated_bundle_config() {
updated_bundle_dir=
case $PACKAGE_SOURCE in
/*)
updated_bundle_dir=$PACKAGE_SOURCE
;;
./*|../*)
updated_bundle_dir=$(CDPATH=; cd "$PACKAGE_SOURCE" 2>/dev/null && pwd) || updated_bundle_dir=
;;
git:git@bitbucket.org:siakitem/my-pi.git)
updated_bundle_dir=${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/git/bitbucket.org/siakitem/my-pi
;;
esac
if [ -n "$updated_bundle_dir" ] && [ -f "$updated_bundle_dir/config/p10k.zsh" ]; then
POWERLEVEL10K_PRESET_FILE=$updated_bundle_dir/config/p10k.zsh
elif [ ! -f "$POWERLEVEL10K_PRESET_FILE" ]; then
warn "无法定位升级后的组合包配置,找不到 ${POWERLEVEL10K_PRESET_FILE}。"
return 1
else
warn "无法从当前 PI_PACKAGE_SOURCE 推导升级后的包目录,将使用脚本目录中的配置。"
fi
}
recover_incomplete_bundle_update() {
recovery_applicable=0
case $PACKAGE_SOURCE in
git:git@bitbucket.org:siakitem/my-pi.git) ;;
*) return 1 ;;
esac
agent_dir=${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}
recovery_target=$agent_dir/git/bitbucket.org/siakitem/my-pi
recovery_marker=$agent_dir/git/bitbucket.org/siakitem/.my-pi.pi-update-incomplete
if [ ! -f "$recovery_marker" ] || [ -L "$recovery_marker" ]; then
return 1
fi
if ! command -v git >/dev/null 2>&1 || [ ! -d "$recovery_target/.git" ]; then
warn "检测到 Pi Git 升级中断标记,但无法验证已安装 checkout,已跳过自动恢复。"
return 1
fi
recovery_target=$(CDPATH=; cd "$recovery_target" 2>/dev/null && pwd -P) || return 1
recovery_top=$(git -C "$recovery_target" rev-parse --show-toplevel 2>/dev/null) || return 1
recovery_top=$(CDPATH=; cd "$recovery_top" 2>/dev/null && pwd -P) || return 1
recovery_origin=$(git -C "$recovery_target" remote get-url origin 2>/dev/null) || return 1
if [ "$recovery_top" != "$recovery_target" ] || \
[ "$recovery_origin" != "git@bitbucket.org:siakitem/my-pi.git" ]; then
warn "检测到 Pi Git 升级中断标记,但 checkout 身份校验失败,拒绝自动清理。"
return 1
fi
recovery_applicable=1
say "检测到默认组合包的 Pi Git 升级中断,正在有限重试依赖清理。"
recovery_attempt=1
while [ "$recovery_attempt" -le 4 ]; do
if git -C "$recovery_target" clean -fdx; then
say "✓ Pi Git 缓存清理在第 ${recovery_attempt} 次尝试后完成。"
return 0
fi
warn "Pi Git 缓存第 ${recovery_attempt} 次清理未完成。"
recovery_attempt=$((recovery_attempt + 1))
done
warn "Pi Git 缓存清理在 4 次尝试后仍未完成;将交由最后一次 pi update 恢复依赖并报告结果。"
return 1
}
migrate_tool_search_settings() {
settings_file=${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}/settings.json
if [ ! -e "$settings_file" ]; then
say "- Pi settings.json 尚不存在;Tool Search 将在下次加载时写入当前默认值。"
return 0
fi
if [ -z "${updated_bundle_dir:-}" ] || [ ! -f "$updated_bundle_dir/scripts/migrate-tool-search-settings.mjs" ]; then
warn "无法定位升级后组合包中的 Tool Search settings.json 迁移辅助脚本,已保留现有配置。"
return 1
fi
migration_helper=$updated_bundle_dir/scripts/migrate-tool-search-settings.mjs
if ! command -v node >/dev/null 2>&1; then
warn "迁移 Tool Search settings.json 需要 Node.js,已保留现有配置。"
return 1
fi
migration_result=$(node "$migration_helper" "$settings_file") || return 1
case $migration_result in
migrated:*)
say "✓ Tool Search 旧版默认配置已迁移到当前组合包默认值。"
say " 原配置备份:${migration_result#migrated:}"
;;
current)
say "✓ Tool Search 用户配置已是当前组合包默认版本。"
;;
custom)
say "- 检测到自定义 Tool Search 用户配置,已完整保留。"
;;
missing-section)
say "- settings.json 尚未包含 Tool Search 配置;下次加载时将补充当前默认值。"
;;
missing)
say "- settings.json 在迁移前已被移除;下次加载时将写入当前默认值。"
;;
*)
warn "Tool Search settings.json 迁移返回未知状态,已停止该项同步。"
return 1
;;
esac
}
resolve_agent_dir() {
if [ -n "${PI_CODING_AGENT_DIR:-}" ]; then
case $PI_CODING_AGENT_DIR in
~/*) printf '%s\n' "$HOME/${PI_CODING_AGENT_DIR#~/}" ;;
*) printf '%s\n' "$PI_CODING_AGENT_DIR" ;;
esac
return 0
fi
printf '%s\n' "$HOME/.pi/agent"
}
# 把组合包权威的系统提示词封面同步到 agent 目录;未受管的自定义封面会被覆盖。
deploy_system_prompt_cover() {
if [ -z "${updated_bundle_dir:-}" ] || [ ! -f "$updated_bundle_dir/scripts/deploy-system-prompt-cover.mjs" ]; then
warn "无法定位升级后组合包中的 SYSTEM_COVER.md 部署辅助脚本,已保留现有封面。"
return 1
fi
if ! command -v node >/dev/null 2>&1; then
warn "找不到 node,无法同步 SYSTEM_COVER.md;Pi 将使用默认系统提示词封面。"
return 1
fi
cover_helper=$updated_bundle_dir/scripts/deploy-system-prompt-cover.mjs
cover_agent_dir=$(resolve_agent_dir) || return 1
cover_status=$(node "$cover_helper" "$cover_agent_dir" 2>&1) || {
warn "SYSTEM_COVER.md 同步失败:$cover_status"
return 1
}
case $cover_status in
created) say "✓ 已部署系统提示词封面:$cover_agent_dir/SYSTEM_COVER.md" ;;
updated) say "✓ 已同步系统提示词封面到组合包当前版本。" ;;
current) say "✓ 系统提示词封面已是当前组合包版本。" ;;
*)
warn "SYSTEM_COVER.md 部署返回未知状态:$cover_status"
return 1
;;
esac
}
resolve_zsh_paths() {
oh_my_zsh_dir=${ZSH:-$HOME/.oh-my-zsh}
zsh_custom_dir=${ZSH_CUSTOM:-$oh_my_zsh_dir/custom}
zshrc_file=${ZDOTDIR:-$HOME}/.zshrc
my_pi_config_dir=${XDG_CONFIG_HOME:-$HOME/.config}/my-pi
p10k_managed_config_file=$my_pi_config_dir/p10k.zsh
}
replace_zsh_managed_block() {
managed_id=$1
managed_placement=$2
managed_anchor=$3
shift 3
managed_content=$(mktemp "${TMPDIR:-/tmp}/my-pi-zsh-block.XXXXXX") || return 1
printf '%s\n' "$@" > "$managed_content"
my_pi_replace_managed_block "$zshrc_file" "$managed_id" "$managed_content" \
"$managed_placement" "$managed_anchor"
managed_status=$?
rm -f "$managed_content"
return "$managed_status"
}
update_git_checkout() {
component=$1
checkout=$2
if [ ! -d "$checkout/.git" ]; then
warn "$component 已安装,但不是脚本可安全升级的 Git checkout,已跳过版本升级。"
return 0
fi
if ! command -v git >/dev/null 2>&1; then
warn "升级 $component 需要 git。"
return 1
fi
upstream=$(git -C "$checkout" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null) || {
warn "$component 的 Git checkout 没有可识别的上游分支,已跳过版本升级。"
return 1
}
git -C "$checkout" fetch --quiet || return 1
local_revision=$(git -C "$checkout" rev-parse HEAD) || return 1
remote_revision=$(git -C "$checkout" rev-parse "$upstream") || return 1
if [ "$local_revision" = "$remote_revision" ]; then
say "✓ $component 已是最新版本。"
return 0
fi
say "发现 $component 有新版本,正在快进升级。"
git -C "$checkout" merge --ff-only "$upstream"
}
refresh_brew_metadata() {
if [ "$brew_metadata_refreshed" -eq 1 ]; then
return 0
fi
say "正在刷新 Homebrew 版本信息。"
brew update || return 1
brew_metadata_refreshed=1
}
update_brew_formula_if_installed() {
formula=$1
description=$2
command_name=$3
if command -v brew >/dev/null 2>&1 && brew list --formula "$formula" >/dev/null 2>&1; then
refresh_brew_metadata || {
record_update_failure "$description"
return 0
}
outdated_formula=$(brew outdated --formula --quiet "$formula") || {
record_update_failure "$description"
return 0
}
if [ -z "$outdated_formula" ]; then
say "✓ $description 已是最新版本。"
else
say "发现 $description 有新版本,正在升级。"
brew upgrade "$formula" || record_update_failure "$description"
fi
elif command -v "$command_name" >/dev/null 2>&1; then
say "- $description 已安装,但不由 Homebrew 管理,已保留现有安装。"
else
say "- $description 未安装,升级流程跳过。"
fi
}
detect_kitty() {
kitty_bin=
if command -v kitty >/dev/null 2>&1; then
kitty_bin=$(command -v kitty)
return 0
fi
case $(uname -s) in
Darwin) kitty_candidates="/Applications/kitty.app/Contents/MacOS/kitty" ;;
*) kitty_candidates="$HOME/.local/kitty.app/bin/kitty" ;;
esac
for candidate in $kitty_candidates; do
if [ -x "$candidate" ]; then
kitty_bin=$candidate
return 0
fi
done
return 1
}
resolve_kitty_config_dir() {
kitty_config_dir=${KITTY_CONFIG_DIRECTORY:-${XDG_CONFIG_HOME:-$HOME/.config}/kitty}
}
kitty_uses_solarized_dark() {
resolve_kitty_config_dir
[ -f "$kitty_config_dir/current-theme.conf" ] &&
grep -F "Solarized Dark" "$kitty_config_dir/current-theme.conf" >/dev/null 2>&1
}
update_kitty() {
if ! detect_kitty; then
say "- Kitty 未安装,升级流程跳过。"
return 0
fi
if command -v brew >/dev/null 2>&1 && brew list --cask kitty >/dev/null 2>&1; then
if refresh_brew_metadata; then
if outdated_kitty=$(brew outdated --cask --quiet kitty); then
if [ -z "$outdated_kitty" ]; then
say "✓ Kitty 已是最新版本。"
else
say "发现 Kitty 有新版本,正在升级。"
brew upgrade --cask kitty || record_update_failure "Kitty"
fi
else
record_update_failure "Kitty"
fi
else
record_update_failure "Kitty"
fi
elif [ "$kitty_bin" = "$HOME/.local/kitty.app/bin/kitty" ] || \
[ "$kitty_bin" = "/Applications/kitty.app/Contents/MacOS/kitty" ]; then
if ! command -v curl >/dev/null 2>&1; then
record_update_failure "Kitty(检查版本需要 curl)"
else
kitty_version_output=$("$kitty_bin" --version 2>/dev/null) || kitty_version_output=
case $kitty_version_output in
kitty\ *)
installed_kitty_version=${kitty_version_output#kitty }
installed_kitty_version=${installed_kitty_version%% *}
;;
*) installed_kitty_version= ;;
esac
latest_kitty_version=$(curl -fsSL "$KITTY_VERSION_URL") || latest_kitty_version=
if [ -z "$installed_kitty_version" ] || [ -z "$latest_kitty_version" ]; then
record_update_failure "Kitty 版本检查"
elif [ "$installed_kitty_version" = "$latest_kitty_version" ]; then
say "✓ Kitty $installed_kitty_version 已是最新版本。"
else
say "发现 Kitty ${latest_kitty_version}(当前 ${installed_kitty_version}),正在升级。"
installer_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-kitty.XXXXXX") || return 1
if ! curl -fsSL "$KITTY_INSTALL_URL" -o "$installer_file" || \
! sh "$installer_file" launch=n "installer=version-$latest_kitty_version"; then
record_update_failure "Kitty"
fi
rm -f "$installer_file"
fi
fi
else
say "- Kitty 已安装,但不由 Homebrew 或官方 ~/.local 安装管理,已保留现有安装。"
fi
if kitty_uses_solarized_dark; then
"$kitty_bin" +kitten themes --reload-in=none "Solarized Dark" || \
record_update_failure "Kitty Solarized Dark 主题"
fi
}
update_codegraph() {
if ! command -v codegraph >/dev/null 2>&1; then
say "- CodeGraph 未安装,升级流程跳过。"
return 0
fi
if ! command -v curl >/dev/null 2>&1; then
warn "检查 CodeGraph 版本需要 curl。"
return 1
fi
installed_codegraph_version=$(codegraph --version 2>/dev/null) || installed_codegraph_version=
latest_release_url=$(curl -fsSLI -o /dev/null -w '%{url_effective}' \
"$CODEGRAPH_LATEST_RELEASE_URL") || latest_release_url=
case $latest_release_url in
*/releases/tag/*) latest_codegraph_tag=${latest_release_url##*/} ;;
*) latest_codegraph_tag= ;;
esac
latest_codegraph_version=${latest_codegraph_tag#v}
installed_codegraph_version=${installed_codegraph_version#v}
if [ -z "$installed_codegraph_version" ] || [ -z "$latest_codegraph_version" ]; then
warn "无法比较 CodeGraph 的本地版本与最新发布版本,已跳过替换。"
return 1
fi
if [ "$installed_codegraph_version" = "$latest_codegraph_version" ]; then
say "✓ CodeGraph $installed_codegraph_version 已是最新版本。"
return 0
fi
say "发现 CodeGraph ${latest_codegraph_version}(当前 ${installed_codegraph_version}),正在升级。"
installer_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-codegraph.XXXXXX") || return 1
if ! curl -fsSL "$CODEGRAPH_INSTALL_URL" -o "$installer_file" || \
! CODEGRAPH_VERSION="$latest_codegraph_tag" sh "$installer_file"; then
rm -f "$installer_file"
return 1
fi
rm -f "$installer_file"
}
update_zsh_environment() {
resolve_zsh_paths
if [ ! -f "$oh_my_zsh_dir/oh-my-zsh.sh" ]; then
say "- Oh My Zsh 未安装,Powerlevel10k 和 Zsh 插件升级均跳过。"
return 0
fi
if [ -f "$oh_my_zsh_dir/tools/upgrade.sh" ] && command -v zsh >/dev/null 2>&1; then
ZSH="$oh_my_zsh_dir" zsh "$oh_my_zsh_dir/tools/upgrade.sh" || record_update_failure "Oh My Zsh"
else
update_git_checkout "Oh My Zsh" "$oh_my_zsh_dir" || record_update_failure "Oh My Zsh"
fi
powerlevel10k_dir=$zsh_custom_dir/themes/powerlevel10k
if [ -f "$powerlevel10k_dir/powerlevel10k.zsh-theme" ]; then
update_git_checkout "Powerlevel10k" "$powerlevel10k_dir" || record_update_failure "Powerlevel10k"
else
say "- Powerlevel10k 未安装,升级流程跳过。"
fi
for plugin in zsh-autosuggestions zsh-syntax-highlighting; do
plugin_dir=$zsh_custom_dir/plugins/$plugin
if [ -d "$plugin_dir" ]; then
update_git_checkout "$plugin" "$plugin_dir" || record_update_failure "$plugin"
else
say "- $plugin 未安装,升级流程跳过。"
fi
done
[ -f "$zshrc_file" ] || return 0
if my_pi_managed_block_exists "$zshrc_file" "oh-my-zsh-loader"; then
replace_zsh_managed_block "oh-my-zsh-loader" "append" "" \
"export ZSH=\"$oh_my_zsh_dir\"" \
"source \"\$ZSH/oh-my-zsh.sh\"" || record_update_failure ".zshrc Oh My Zsh 受管块"
fi
if my_pi_managed_block_exists "$zshrc_file" "powerlevel10k-theme"; then
replace_zsh_managed_block "powerlevel10k-theme" "before" \
'^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \
'ZSH_THEME="powerlevel10k/powerlevel10k"' || record_update_failure ".zshrc Powerlevel10k 主题受管块"
fi
for plugin in git zsh-autosuggestions zsh-syntax-highlighting; do
if my_pi_managed_block_exists "$zshrc_file" "zsh-plugin-$plugin"; then
replace_zsh_managed_block "zsh-plugin-$plugin" "before" \
'^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \
"plugins+=($plugin)" || record_update_failure ".zshrc $plugin 受管块"
fi
done
if [ -f "$p10k_managed_config_file" ] || \
my_pi_managed_block_exists "$zshrc_file" "powerlevel10k-config"; then
if my_pi_sync_managed_file "$POWERLEVEL10K_PRESET_FILE" "$p10k_managed_config_file"; then
replace_zsh_managed_block "powerlevel10k-config" "append" "" \
"[[ ! -f \"$p10k_managed_config_file\" ]] || source \"$p10k_managed_config_file\"" || \
record_update_failure ".zshrc Powerlevel10k 配置受管块"
else
record_update_failure "Powerlevel10k 受管配置"
fi
fi
}
if ! command -v pi >/dev/null 2>&1; then
warn "找不到 pi,无法升级组合包。请确保 pi 位于 PATH。"
exit 127
fi
say "正在升级 Pi 组合包:$PACKAGE_SOURCE"
if pi update "$PACKAGE_SOURCE"; then
:
else
recovery_applicable=0
recovery_cleaned=0
if recover_incomplete_bundle_update; then
recovery_cleaned=1
fi
if [ "$recovery_applicable" -ne 1 ]; then
warn "pi update 执行失败,已停止后续升级。"
exit 1
fi
if [ "$recovery_cleaned" -eq 1 ]; then
say "正在重试 Pi 组合包升级,由 Pi 完成依赖安装并清除中断标记。"
else
warn "正在执行最后一次 pi update,以恢复可能被部分清理的依赖。"
fi
if ! pi update "$PACKAGE_SOURCE"; then
warn "pi update 自动恢复后仍然失败,已停止后续升级。"
exit 1
fi
fi
select_updated_bundle_config || exit 1
migrate_tool_search_settings || record_update_failure "Tool Search 用户配置"
deploy_system_prompt_cover || record_update_failure "SYSTEM_COVER.md"
say "组合包升级完成,开始升级已安装的终端环境和机器级 CLI;未安装项将直接跳过。"
update_kitty || record_update_failure "Kitty"
update_zsh_environment || record_update_failure "Zsh 环境"
update_codegraph || record_update_failure "CodeGraph"
update_brew_formula_if_installed "kotlin-lsp" "kotlin-lsp" "kotlin-lsp"
update_brew_formula_if_installed "openjdk@21" "Java 21" "java"
update_brew_formula_if_installed "jdtls" "jdtls" "jdtls"
say "已安装组件的升级与受管配置同步完成。"
exit "$update_failed"