diff --git a/AGENTS.md b/AGENTS.md index 2939b8f..987fa8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # 仓库说明 -本仓库用于维护可通过一次 `pi install` 部署的个人 Pi 扩展组合包。根 `package.json` 是唯一的组合包安装入口;根 `install.sh` 和 `uninstall.sh` 只是对 `pi install` / `pi remove` 的便捷包装,不形成第二套部署机制。依赖、默认行为和必要配置必须由组合包自身声明或部署,不把安装后的手工配置作为默认流程。每个自维护扩展使用独立的顶层目录,扩展源码、测试和说明应保留在各自目录内。 +本仓库用于维护可通过一次 `pi install` 部署的个人 Pi 扩展组合包。根 `package.json` 是唯一的组合包安装入口;根 `install.sh`、`update.sh` 和 `uninstall.sh` 只是对 `pi install` / `pi update` / `pi remove` 的便捷包装,不形成第二套部署机制。依赖、默认行为和必要配置必须由组合包自身声明或部署,不把安装后的手工配置作为默认流程。每个自维护扩展使用独立的顶层目录,扩展源码、测试和说明应保留在各自目录内。 ## 当前扩展 @@ -21,6 +21,7 @@ - `@ogulcancelik/pi-codex-compaction@0.1.3`:为 `openai-codex` 提供原生远程 compaction,默认阈值为 90%。 - 根包还固定安装 `pi-context-view`、`@firstpick/pi-extension-codex-fast-mode` 和 `@gotgenes/pi-permission-system`。 - `install.sh`:先安装根组合包,再交互检查 Kitty/Solarized Dark、Oh My Zsh/Powerlevel10k/Zsh 插件、CodeGraph、Kotlin LSP、Java 21+ 和 JDT LS;缺失项目只在用户明确选择 `Y` 后安装或配置。Powerlevel10k 默认配置来自仓库内置的 `config/p10k.zsh`(当前 Rainbow/ASCII 单行紧凑主题)。 +- `update.sh`:先通过 `pi update` 升级根组合包,再只升级当前已安装的终端环境和机器级依赖;未安装项直接跳过。Powerlevel10k 配置优先从 `pi update` 后的已安装组合包读取,并与 `.zshrc` 受管块一起按内容比较后增量同步。 - `uninstall.sh`:只移除根组合包,不卸载或还原可能被其他项目共享的终端环境、CodeGraph、Kotlin LSP、JDT LS 或 Java。 ## 当前职责与默认行为 @@ -43,11 +44,11 @@ - 当前只把 Git 非只读操作和包管理类 Bash 命令交给 `pi-auto-permissions` 根据用户上下文复核。低风险操作可自动允许;中风险操作仅在用户明确授权目标与约束时自动允许;高风险操作即使已有概括授权也必须回到用户确认。文件/系统/网络高风险操作继续由现有解析能力更强的 `pi-permission-system` 处理。 - 将 Auto Permissions 规则配置为 `.*` 最多只能复核所有 Bash 命令,不能覆盖其他工具,并会为普通 Bash 调用增加额外模型请求、延迟和成本。若未来目标是让现有所有 `ask` 先经模型裁决,必须另行实现 `pi-permission-system` authorizer 集成,不能仅靠该 npm 包配置实现。 - `pi-lsp` 使用组合包部署的 `kotlin-lsp --stdio` 与 `jdtls` 配置,但根包安装本身不安装这两个系统可执行文件;便捷脚本可在用户逐项明确确认后通过 Homebrew 安装 Kotlin LSP、Java 21 和 JDT LS。 -- 终端环境配置只属于便捷脚本:Kitty 可选安装后可通过官方 kitten 启用 Solarized Dark;Oh My Zsh 使用不覆盖 `.zshrc`、不切换 shell 的 unattended 安装;Powerlevel10k 在用户没有现有 `~/.p10k.zsh` 时部署仓库内置的 `config/p10k.zsh`(Rainbow/ASCII 单行紧凑主题)以跳过首次向导,已有配置不得覆盖;`git`、`zsh-autosuggestions`、`zsh-syntax-highlighting` 逐项询问后才安装或启用。修改 `.zshrc` 前必须创建带时间戳的备份。 +- 终端环境配置只属于便捷脚本:Kitty 可选安装后可通过官方 kitten 启用 Solarized Dark;Oh My Zsh 使用不切换 shell 的 unattended 安装;Powerlevel10k 将仓库内置的 `config/p10k.zsh`(Rainbow/ASCII 单行紧凑主题)部署到 `~/.config/my-pi/p10k.zsh`(遵循 `XDG_CONFIG_HOME`),不得覆盖用户自己的 `~/.p10k.zsh`。`.zshrc` 中脚本拥有的内容必须使用 `# >>> my-pi: >>>` / `# <<< my-pi: <<<` 受管块,更新时只替换块内内容;标记不完整或重复时拒绝修改。实际修改前必须创建带时间戳的备份。`git`、`zsh-autosuggestions`、`zsh-syntax-highlighting` 在安装流程中逐项询问后才安装或启用。 - Codex 远程压缩只对 `openai-codex` 生效,默认在 turn boundary 达到 90% 时触发;Hermes Memory 默认使用 policy-only 模式。 - `install.sh` 默认从 `git:git@bitbucket.org:siakitem/my-pi.git` 安装,并允许用 `PI_PACKAGE_SOURCE` 覆盖来源;组合包安装失败时立即停止,机器级依赖安装失败时继续检查其余依赖并最终返回非零状态。 - CodeGraph 的可选安装使用官方远程安装脚本;macOS Kitty 使用 Homebrew cask,其他系统使用 Kitty 官方安装器;Kotlin LSP、Java 21 和 JDT LS 使用 Homebrew;Oh My Zsh 使用官方安装器,Powerlevel10k 与两个第三方 Zsh 插件从各自官方 Git 仓库克隆。缺少 Homebrew、curl 或 git 时保留已经完成的组合包安装并报告失败。 -- `install.sh` 不检查默认不需要的 `rtk` CLI;`uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则调用 `pi remove`,且不删除共享机器级工具。 +- `install.sh` 不检查默认不需要的 `rtk` CLI;`update.sh` 和 `uninstall.sh` 使用同一个 `PI_PACKAGE_SOURCE` 规则分别调用 `pi update` 和 `pi remove`。运行 `update.sh` 视为明确授权升级已安装组件及同步已有受管配置,但不得安装缺失组件或改写受管块之外的用户配置;卸载仍不改动共享机器级工具。 ## 修改边界 @@ -58,8 +59,8 @@ - 需要原生构建的依赖只按锁定版本加入根 `allowScripts`;当前仅允许 Hermes Memory 所需的 `better-sqlite3`,不得批量批准其他 install scripts。 - Pi 核心包只作为宿主 peer dependencies,不得在组合包内再安装或打包一套 Pi runtime;保留根 `.npmrc` 的 peer 安装策略。 - 上游仅作为参考来源。同步上游改动时先核对本仓库已有修改,再按明确范围移植;不要直接覆盖本地实现。 -- `install.sh` 和 `uninstall.sh` 必须保持 POSIX `sh` 兼容和可执行权限,并包含在根 `package.json` 的 `files` 中;修改脚本行为时同步更新 README 和本文件。 -- 机器级依赖和用户终端配置必须保持逐项询问且默认拒绝,不得在没有用户明确确认的情况下自动安装或改写;`.zshrc` 修改必须保留备份,卸载不得顺带删除或还原共享工具和用户终端配置。 +- `install.sh`、`update.sh` 和 `uninstall.sh` 必须保持 POSIX `sh` 兼容和可执行权限,并包含在根 `package.json` 的 `files` 中;修改脚本行为时同步更新 README 和本文件。 +- 安装流程中的机器级依赖和用户终端配置必须保持逐项询问且默认拒绝,不得在没有用户明确确认的情况下自动安装或改写。用户主动运行 `update.sh` 只授权升级已安装项和同步已有受管配置;缺失项仍必须跳过。`.zshrc` 修改必须局限于受管块并保留备份,卸载不得顺带删除或还原共享工具和用户终端配置。 - 未经明确要求,不执行发布、提交、推送、运行安装/卸载脚本或安装到用户 Pi 运行目录等外部写操作。 ## `pi-rtk-optimizer` 开发约定 @@ -76,7 +77,7 @@ 组合包依赖或加载入口变化时,至少验证根 `npm install` 幂等、锁文件有效,以及全部扩展可在隔离的临时 Pi agent 目录加载。权限配置变化时使用当前固定版本的 `pi-permission-system` schema 校验,并验证包装入口部署后的文件与仓库源配置一致。 -安装或卸载脚本变化时,至少运行 `sh -n install.sh`、`sh -n uninstall.sh` 和 ShellCheck,并核对脚本仍具有可执行权限、仍包含在根 `package.json` 的 `files` 中、README 描述与实际流程一致。涉及真实 `pi install`、`pi remove`、Homebrew、远程 Kitty/Oh My Zsh 安装器、Git 克隆或真实用户终端配置的端到端验证属于外部写操作,未经明确要求不得执行;可以使用隔离的临时 HOME 和 mock 命令验证分支行为。 +安装、升级或卸载脚本变化时,至少运行 `sh -n install.sh`、`sh -n update.sh`、`sh -n uninstall.sh` 和 ShellCheck,并核对脚本仍具有可执行权限、仍包含在根 `package.json` 的 `files` 中、README 描述与实际流程一致。涉及真实 `pi install`、`pi update`、`pi remove`、Homebrew、远程 Kitty/Oh My Zsh 安装器、Git 克隆或真实用户终端配置的端到端验证属于外部写操作,未经明确要求不得执行;可以使用隔离的临时 HOME 和 mock 命令验证分支行为。 在 `pi-rtk-optimizer/` 内按改动范围选择最小充分验证: diff --git a/README.md b/README.md index e3a643d..f265790 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - `@gotgenes/pi-permission-system@26.2.1`:负责工具、路径、MCP、硬拒绝和兜底权限基线。 - `extensions/tool-routing.ts`:保留 Pi 默认系统提示词,并按当前激活工具追加简短的工具与搜索路由规则;提供 `/dump-system-prompt` 导出当前有效提示词。 -## 安装与卸载 +## 安装、升级与卸载 推荐从仓库检出目录运行安装脚本: @@ -36,22 +36,46 @@ pi install git:git@bitbucket.org:siakitem/my-pi.git 可用后继续检查并可通过官方 `themes` kitten 配置 `Solarized Dark`。 - 检查 Oh My Zsh。安装采用官方 unattended 模式,不切换默认 shell、不立即启动 Zsh, 也不覆盖现有 `.zshrc`。 -- 检查 Powerlevel10k;可克隆到 Oh My Zsh 的 custom themes 目录,并在没有现有 - `~/.p10k.zsh` 时复制仓库内置的 `config/p10k.zsh`。该配置来自当前使用的 Powerlevel10k - Rainbow/ASCII 单行紧凑主题;脚本会把 `ZSH_THEME` 和配置加载语句写入 `.zshrc`,因此 - 首次启动不会进入交互配置向导。已有 `~/.p10k.zsh` 始终保留,不会被覆盖。 +- 检查 Powerlevel10k;可克隆到 Oh My Zsh 的 custom themes 目录,并把仓库内置的 + `config/p10k.zsh` 原子同步到 `~/.config/my-pi/p10k.zsh`(遵循 `XDG_CONFIG_HOME`)。 + 该配置来自当前使用的 Powerlevel10k Rainbow/ASCII 单行紧凑主题;用户自己的 + `~/.p10k.zsh` 不会被覆盖。脚本只在 `.zshrc` 中写入带首尾标记的主题和加载受管块。 - 逐项检查 `git`、`zsh-autosuggestions` 和 `zsh-syntax-highlighting` 是否已经在 `.zshrc` 的 `plugins` 数组中启用。后两个插件缺少源码时会先克隆到 Oh My Zsh 的 custom plugins 目录;语法高亮按最后一个插件处理。 - 最后继续检查 `codegraph`、`kotlin-lsp`、Java 21+ 和 `jdtls`。Kotlin、Java 和 JDT LS 的自动安装使用 Homebrew。 -脚本首次改动 `.zshrc` 前会在同目录创建带时间戳的备份。若机器没有所需的 Homebrew、 -`curl` 或 `git`,脚本会保留已完成的安装结果、继续检查其他项目,并最终返回非零状态。 +脚本首次实际改动 `.zshrc` 前会在同目录创建带时间戳的备份。Oh My Zsh loader、 +Powerlevel10k 主题/配置和各 Zsh 插件使用 `# >>> my-pi: >>>` 到 +`# <<< my-pi: <<<` 的受管块协议;以后只替换块内内容,块外用户配置保持不变。 +标记缺失、重复或不完整时脚本会拒绝修改。若机器没有所需的 Homebrew、`curl` 或 `git`, +脚本会保留已完成的安装结果、继续检查其他项目,并最终返回非零状态。 `rtk` 只供默认关闭的命令改写使用,因此不会作为必装依赖检查。 也可以绕过脚本,直接运行上面的 `pi install` 命令,此时机器级 CLI 不会被检查。 +升级已安装的组合包请使用独立脚本: + +```bash +./update.sh +``` + +升级脚本先执行: + +```bash +pi update git:git@bitbucket.org:siakitem/my-pi.git +``` + +随后只处理已经安装的项目,未安装项直接跳过且不会询问安装:Kitty 及已启用的 +Solarized Dark、Oh My Zsh、Powerlevel10k、两个第三方 Zsh 插件、CodeGraph,以及由 +Homebrew 管理的 Kotlin LSP、Java 21 和 JDT LS。Git checkout 使用 `pull --ff-only`, +Homebrew 包使用 `brew upgrade`;无法确认安装来源时保留现状并报告。 + +升级时会优先从 `pi update` 后的已安装组合包目录读取最新 `config/p10k.zsh`,再与受管副本比较, +只在内容不同时备份并原子同步;`.zshrc` 也只更新已有的 my-pi 受管块,不触碰块外配置。因此安装流程负责补齐缺失项,升级流程只升级 +已安装项并同步已受管配置。 + 卸载组合包: ```bash @@ -60,11 +84,12 @@ pi install git:git@bitbucket.org:siakitem/my-pi.git 卸载脚本只调用 `pi remove`,不会删除 Kitty、终端主题、Oh My Zsh、Powerlevel10k、Zsh 插件、CodeGraph、Kotlin LSP、JDT LS 或 Java,因为这些用户环境或机器级工具可能仍被其他 -项目使用。两个脚本都可通过 `PI_PACKAGE_SOURCE` 覆盖默认包源, +项目使用。三个脚本都可通过 `PI_PACKAGE_SOURCE` 覆盖默认包源, 例如在本地检出中测试: ```bash PI_PACKAGE_SOURCE="$PWD" ./install.sh +PI_PACKAGE_SOURCE="git:git@bitbucket.org:siakitem/my-pi.git" ./update.sh ``` 根 `package.json` 使用精确版本,`package-lock.json` 固定完整依赖树。不要再用项目级 diff --git a/install.sh b/install.sh index 111b26c..b94891a 100755 --- a/install.sh +++ b/install.sh @@ -3,6 +3,14 @@ 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 @@ -12,7 +20,6 @@ POWERLEVEL10K_REPOSITORY=https://github.com/romkatv/powerlevel10k.git ZSH_AUTOSUGGESTIONS_REPOSITORY=https://github.com/zsh-users/zsh-autosuggestions.git ZSH_SYNTAX_HIGHLIGHTING_REPOSITORY=https://github.com/zsh-users/zsh-syntax-highlighting.git install_failed=0 -zshrc_backup_done=0 say() { printf '%s\n' "$*" @@ -162,6 +169,8 @@ 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 } oh_my_zsh_installed() { @@ -190,15 +199,21 @@ prepare_zshrc() { zshrc_dir=${zshrc_file%/*} [ "$zshrc_dir" = "$zshrc_file" ] && zshrc_dir=. mkdir -p "$zshrc_dir" || return 1 - if [ ! -f "$zshrc_file" ]; then - : > "$zshrc_file" || return 1 - fi - if [ "$zshrc_backup_done" -eq 0 ]; then - zshrc_backup="$zshrc_file.my-pi-backup-$(date +%Y%m%d%H%M%S)" - cp "$zshrc_file" "$zshrc_backup" || return 1 - zshrc_backup_done=1 - say "已备份现有 Zsh 配置到 $zshrc_backup" - fi + [ -f "$zshrc_file" ] || : > "$zshrc_file" || return 1 +} + +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" } zshrc_has_oh_my_zsh_loader() { @@ -211,50 +226,26 @@ ensure_oh_my_zsh_loader() { if zshrc_has_oh_my_zsh_loader; then return 0 fi - { - printf '\n# Oh My Zsh (managed by my-pi install.sh)\n' - printf 'export ZSH="%s"\n' "$oh_my_zsh_dir" - printf '%s\n' "source \"\$ZSH/oh-my-zsh.sh\"" - } >> "$zshrc_file" + replace_zsh_managed_block "oh-my-zsh-loader" "append" "" \ + "export ZSH=\"$oh_my_zsh_dir\"" \ + "source \"\$ZSH/oh-my-zsh.sh\"" } set_powerlevel10k_theme() { prepare_zshrc || return 1 - tmp_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-zshrc.XXXXXX") || return 1 - if ! awk ' - BEGIN { inserted = 0 } - /^[[:space:]]*ZSH_THEME[[:space:]]*=/ { next } - !inserted && /^[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh/ { - print "ZSH_THEME=\"powerlevel10k/powerlevel10k\"" - inserted = 1 - } - { print } - END { - if (!inserted) print "ZSH_THEME=\"powerlevel10k/powerlevel10k\"" - } - ' "$zshrc_file" > "$tmp_file"; then - rm -f "$tmp_file" - return 1 - fi - cp "$tmp_file" "$zshrc_file" || { - rm -f "$tmp_file" - return 1 - } - rm -f "$tmp_file" + replace_zsh_managed_block "powerlevel10k-theme" "before" \ + '^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \ + 'ZSH_THEME="powerlevel10k/powerlevel10k"' } p10k_config_sourced() { - [ -f "$zshrc_file" ] || return 1 - grep -E '^[[:space:]]*\[\[[[:space:]].*\.p10k\.zsh.*source[[:space:]].*\.p10k\.zsh' \ - "$zshrc_file" >/dev/null 2>&1 + my_pi_managed_block_exists "$zshrc_file" "powerlevel10k-config" } ensure_p10k_config_source() { - if p10k_config_sourced; then - return 0 - fi - printf '\n# Powerlevel10k preset (managed by my-pi install.sh)\n[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh\n' \ - >> "$zshrc_file" + prepare_zshrc || return 1 + replace_zsh_managed_block "powerlevel10k-config" "append" "" \ + "[[ ! -f \"$p10k_managed_config_file\" ]] || source \"$p10k_managed_config_file\"" } configure_powerlevel10k() { @@ -269,17 +260,11 @@ configure_powerlevel10k() { git clone --depth=1 "$POWERLEVEL10K_REPOSITORY" "$powerlevel10k_dir" || return 1 fi - if [ ! -f "$HOME/.p10k.zsh" ]; then - if [ ! -f "$POWERLEVEL10K_PRESET_FILE" ]; then - warn "找不到仓库内置的 Powerlevel10k 配置:$POWERLEVEL10K_PRESET_FILE" - return 1 - fi - cp "$POWERLEVEL10K_PRESET_FILE" "$HOME/.p10k.zsh" || return 1 - say "已写入仓库内置的 Powerlevel10k Rainbow/ASCII 配置,首次启动不会进入配置向导。" - fi + my_pi_sync_managed_file "$POWERLEVEL10K_PRESET_FILE" "$p10k_managed_config_file" || return 1 set_powerlevel10k_theme || return 1 ensure_oh_my_zsh_loader || return 1 - ensure_p10k_config_source + ensure_p10k_config_source || return 1 + say "已应用仓库内置的 Powerlevel10k Rainbow/ASCII 受管配置。" } zsh_plugin_enabled() { @@ -305,24 +290,9 @@ add_zsh_plugin() { plugin=$1 prepare_zshrc || return 1 ensure_oh_my_zsh_loader || return 1 - tmp_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-zshrc.XXXXXX") || return 1 - if ! awk -v plugin="$plugin" ' - BEGIN { added = 0 } - !added && /^[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh/ { - print "plugins+=(" plugin ")" - added = 1 - } - { print } - END { if (!added) print "plugins+=(" plugin ")" } - ' "$zshrc_file" > "$tmp_file"; then - rm -f "$tmp_file" - return 1 - fi - cp "$tmp_file" "$zshrc_file" || { - rm -f "$tmp_file" - return 1 - } - rm -f "$tmp_file" + replace_zsh_managed_block "zsh-plugin-$plugin" "before" \ + '^(# >>> my-pi:oh-my-zsh-loader >>>|[[:space:]]*(source|\.)[[:space:]].*oh-my-zsh\.sh)' \ + "plugins+=($plugin)" } install_and_enable_zsh_plugin() { @@ -409,8 +379,9 @@ if [ "$oh_my_zsh_ok" -eq 1 ]; then resolve_zsh_paths powerlevel10k_dir=$zsh_custom_dir/themes/powerlevel10k if [ -f "$powerlevel10k_dir/powerlevel10k.zsh-theme" ] && \ - grep -E "^[[:space:]]*ZSH_THEME[[:space:]]*=[[:space:]]*[\"']?powerlevel10k/powerlevel10k[\"']?" \ - "$zshrc_file" >/dev/null 2>&1 && [ -f "$HOME/.p10k.zsh" ] && p10k_config_sourced; then + my_pi_managed_block_exists "$zshrc_file" "powerlevel10k-theme" && \ + [ -f "$p10k_managed_config_file" ] && \ + cmp -s "$POWERLEVEL10K_PRESET_FILE" "$p10k_managed_config_file" && p10k_config_sourced; then say "✓ Powerlevel10k 已安装并配置" elif ask_yes_no "Powerlevel10k 尚未完整配置。是否安装并应用仓库内置的 Rainbow/ASCII 配置?"; then configure_powerlevel10k || record_install_failure "Powerlevel10k" diff --git a/package.json b/package.json index 1d1b50c..e591ff3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "extensions", "pi-rtk-optimizer", "config", + "scripts", "install.sh", + "update.sh", "uninstall.sh", "README.md", "AGENTS.md" diff --git a/scripts/managed-config.sh b/scripts/managed-config.sh new file mode 100644 index 0000000..6f2546b --- /dev/null +++ b/scripts/managed-config.sh @@ -0,0 +1,147 @@ +#!/bin/sh + +# Shared helpers for files that contain my-pi-owned blocks. +# Callers must provide say() and warn(). + +MY_PI_MANAGED_BACKUPS=${MY_PI_MANAGED_BACKUPS:-} + +my_pi_backup_once() { + managed_file=$1 + case "|$MY_PI_MANAGED_BACKUPS|" in + *"|$managed_file|"*) return 0 ;; + esac + + if [ -f "$managed_file" ]; then + managed_backup=$(mktemp "$managed_file.my-pi-backup-$(date +%Y%m%d%H%M%S).XXXXXX") || return 1 + cp "$managed_file" "$managed_backup" || { + rm -f "$managed_backup" + return 1 + } + say "已备份配置到 $managed_backup" + fi + MY_PI_MANAGED_BACKUPS=${MY_PI_MANAGED_BACKUPS:+$MY_PI_MANAGED_BACKUPS|}$managed_file +} + +my_pi_managed_block_exists() { + managed_file=$1 + managed_id=$2 + managed_begin="# >>> my-pi:$managed_id >>>" + managed_end="# <<< my-pi:$managed_id <<<" + [ -f "$managed_file" ] || return 1 + grep -F -x "$managed_begin" "$managed_file" >/dev/null 2>&1 && + grep -F -x "$managed_end" "$managed_file" >/dev/null 2>&1 +} + +my_pi_replace_managed_block() { + managed_file=$1 + managed_id=$2 + managed_content_file=$3 + managed_placement=${4:-append} + managed_anchor=${5:-} + managed_begin="# >>> my-pi:$managed_id >>>" + managed_end="# <<< my-pi:$managed_id <<<" + managed_dir=${managed_file%/*} + [ "$managed_dir" = "$managed_file" ] && managed_dir=. + mkdir -p "$managed_dir" || return 1 + [ -f "$managed_file" ] || : > "$managed_file" || return 1 + + managed_counts=$(awk -v begin="$managed_begin" -v end="$managed_end" ' + $0 == begin { + begins++ + if (active) invalid = 1 + active = 1 + } + $0 == end { + ends++ + if (!active) invalid = 1 + active = 0 + } + END { + if (active) invalid = 1 + printf "%d %d %d\n", begins, ends, invalid + } + ' "$managed_file") || return 1 + managed_begin_count=${managed_counts%% *} + managed_remainder=${managed_counts#* } + managed_end_count=${managed_remainder%% *} + managed_invalid=${managed_remainder#* } + if [ "$managed_begin_count" -ne "$managed_end_count" ] || \ + [ "$managed_begin_count" -gt 1 ] || [ "$managed_invalid" -ne 0 ]; then + warn "$managed_file 中的 my-pi:$managed_id 受管标记不完整或重复,已拒绝修改。" + return 1 + fi + + managed_tmp=$(mktemp "$managed_dir/.my-pi-managed.XXXXXX") || return 1 + if ! awk \ + -v begin="$managed_begin" \ + -v end="$managed_end" \ + -v content="$managed_content_file" \ + -v placement="$managed_placement" \ + -v anchor="$managed_anchor" ' + function emit_block( line) { + print begin + while ((getline line < content) > 0) print line + close(content) + print end + } + BEGIN { replacing = 0; emitted = 0 } + $0 == begin { + emit_block() + replacing = 1 + emitted = 1 + next + } + replacing && $0 == end { replacing = 0; next } + replacing { next } + !emitted && placement == "before" && anchor != "" && $0 ~ anchor { + emit_block() + emitted = 1 + } + { print } + END { + if (!emitted) { + if (NR > 0) print "" + emit_block() + } + } + ' "$managed_file" > "$managed_tmp"; then + rm -f "$managed_tmp" + return 1 + fi + + if cmp -s "$managed_tmp" "$managed_file"; then + rm -f "$managed_tmp" + return 0 + fi + my_pi_backup_once "$managed_file" || { + rm -f "$managed_tmp" + return 1 + } + if ! mv "$managed_tmp" "$managed_file"; then + rm -f "$managed_tmp" + return 1 + fi +} + +my_pi_sync_managed_file() { + managed_source=$1 + managed_target=$2 + [ -f "$managed_source" ] || { + warn "找不到仓库配置:$managed_source" + return 1 + } + if [ -f "$managed_target" ] && cmp -s "$managed_source" "$managed_target"; then + return 0 + fi + + managed_target_dir=${managed_target%/*} + [ "$managed_target_dir" = "$managed_target" ] && managed_target_dir=. + mkdir -p "$managed_target_dir" || return 1 + my_pi_backup_once "$managed_target" || return 1 + managed_tmp=$(mktemp "$managed_target_dir/.my-pi-sync.XXXXXX") || return 1 + if ! cp "$managed_source" "$managed_tmp" || ! mv "$managed_tmp" "$managed_target"; then + rm -f "$managed_tmp" + return 1 + fi + say "已同步受管配置:$managed_target" +} diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..05fd2cb --- /dev/null +++ b/update.sh @@ -0,0 +1,264 @@ +#!/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 +KITTY_INSTALL_URL=https://sw.kovidgoyal.net/kitty/installer.sh +update_failed=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 +} + +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 + git -C "$checkout" pull --ff-only +} + +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 + say "正在升级 $description。" + brew upgrade "$formula" || record_update_failure "$description" + 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 + brew upgrade --cask kitty || record_update_failure "Kitty" + 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 + 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; then + record_update_failure "Kitty" + fi + rm -f "$installer_file" + 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 + installer_file=$(mktemp "${TMPDIR:-/tmp}/my-pi-codegraph.XXXXXX") || return 1 + if ! curl -fsSL "$CODEGRAPH_INSTALL_URL" -o "$installer_file" || ! 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 + warn "pi update 执行失败,已停止后续升级。" + exit 1 +fi +select_updated_bundle_config || exit 1 + +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"