From 34e86141bd697c669ed3c3d8e09cba91daf39220 Mon Sep 17 00:00:00 2001 From: figmar Date: Mon, 7 Sep 2026 02:25:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20switch.sh=20=E2=80=94=20apes?= =?UTF-8?q?=E2=86=94upstream=20=E4=B8=A4=E7=BA=BF=E5=AF=B9=E7=A7=B0?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同一 profile 内二选一(双装不可行: 同 cordis entry id 冲突) - 探测当前线 → 移除对手线依赖+bundle → 写目标线 → pnpm install → 校验 bundle - 幂等可重复跑; --status 只读; --dry-run 不写文件; --profile 指定 profile - aps 线用 file: 本地源码, upstream 线用 registry latest/指定版本 - 修复 pnpm spec 双 @ bug (值不含包名, pnpm 从 key 取包名) - 双向真实切换验证通过 (aps→upstream→aps, 临时 profile 副本, EXIT=0) - package.json 加 switch/switch:status/switch:aps/switch:upstream 入口 - switch.sh 加入 files (发布时带上) --- package.json | 7 +- scripts/switch.sh | 294 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+), 1 deletion(-) create mode 100755 scripts/switch.sh diff --git a/package.json b/package.json index d0164b2..7be9fd4 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "src", "scripts/install.sh", "scripts/install.ps1", + "scripts/switch.sh", "cordis.patch.yml", "README.md", "README_EN.md", @@ -88,7 +89,11 @@ "test:mount": "bash scripts/e2e-mount.sh", "test:mount:aggregate": "bash scripts/e2e-aggregate-mount.sh", "clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"", - "check:consumer-types": "bash scripts/check-consumer-types.sh" + "check:consumer-types": "bash scripts/check-consumer-types.sh", + "switch": "bash scripts/switch.sh", + "switch:status": "bash scripts/switch.sh --status", + "switch:aps": "bash scripts/switch.sh aps", + "switch:upstream": "bash scripts/switch.sh upstream" }, "license": "MIT", "peerDependencies": { diff --git a/scripts/switch.sh b/scripts/switch.sh new file mode 100755 index 0000000..7648c7e --- /dev/null +++ b/scripts/switch.sh @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# ============================================================================= +# dsh-apes-sidebar 两线切换脚本(apes fork 线 ↔ upstream better-sidebar 线) +# +# 同一个 DSH profile 内**二选一**切换侧边栏插件——双装不可行(两条线都注册 +# 进 `ctx.betterSidebar`,cordis entry id 冲突,第二个被丢弃)。切换 = +# 换 profile 依赖 + 换 dsh.profile.bundles + pnpm install + 提示重启。 +# +# aps 本 fork 线(dsh-apes-sidebar,含 4 个 apes:* 信息层 tab) +# 依赖:dsh-apes-sidebar: (本地开发)或 +# dsh-apes-sidebar@<版本>(npm,发布后) +# upstream upstream 线(dsh-better-sidebar,官方 8 tab + 生态) +# 依赖:dsh-better-sidebar@<版本>(registry,默认 latest) +# +# 用法: +# bash scripts/switch.sh aps # 切到本 fork 线(本地 file: 安装) +# bash scripts/switch.sh upstream # 切到 upstream 线(registry 安装) +# bash scripts/switch.sh upstream 0.18.0 # 指定 upstream 版本 +# bash scripts/switch.sh --status # 只看当前线,不改任何东西 +# +# 通用 flag(两线均适用): +# --profile <名> 目标 profile(缺省 desktop) +# --dry-run 只打印将执行的操作,不写任何文件 +# -h/--help 打印本帮助 +# +# 环境(均可省略,脚本自动探测): +# DSH_HOME 默认 ~/.dsh +# REGISTRY 默认 https://registry.npmjs.org +# DSH_CMD 默认 PATH 上的 dsh,缺省 npx -y --package @deepseek-ai/dsh +# APS_SRC aps 线本地源码路径(缺省 = 本仓库根,即 file: 指向自己) +# +# 机制(幂等,可重复跑): +# 1. 探测当前线(依赖里哪个包在) +# 2. 与目标线相同 → 提示后直接退出(不重装) +# 3. 移除另一线的依赖 + 从 bundles 摘除另一线的 bundle +# 4. 写入目标线依赖 + 确保 workspace 放行 node-pty/protobufjs 构建 +# 5. cd profile && pnpm install(reconcileProfilePnpmWorkspace 会重建 +# node_modules——这是预期行为,两线不共存) +# 6. 确保 bundles 含目标线包名(bundle 通道挂载生效的判据) +# 7. 幂等移除 cordis.patch.yml 里对手线的手动挂载行(避免双挂载) +# 8. 提示重启 DSH +# +# 回滚:跑 `bash scripts/switch.sh <另一线>` 即可切回去。 +# ============================================================================= +set -euo pipefail + +for arg in "$@"; do + if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then + cat <<'EOF' +dsh-apes-sidebar 两线切换(同一 profile 内二选一) + +用法: + bash scripts/switch.sh [版本] + bash scripts/switch.sh --status + + aps 切到本 fork 线(dsh-apes-sidebar,4 个 apes:* tab) + upstream 切到 upstream 线(dsh-better-sidebar,官方) + <版本> 仅对 upstream 生效(缺省 latest) + --status 只看当前线,不改动 + --profile 目标 profile(缺省 desktop) + --dry-run 只打印操作,不写文件 + -h/--help 本帮助 + +环境变量:DSH_HOME、REGISTRY、DSH_CMD、APS_SRC(aps 本地源码路径) +EOF + exit 0 + fi +done + +DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}" +REGISTRY="${REGISTRY:-https://registry.npmjs.org}" +DSH_CMD="${DSH_CMD:-dsh}" +# aps 线本地源码:缺省 = 本仓库根(脚本在 scripts/ 下,根 = 上一级) +APS_SRC="${APS_SRC:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" + +APS_PKG="dsh-apes-sidebar" +UP_PKG="dsh-better-sidebar" + +DRY_RUN=false +STATUS_ONLY=false +TARGET="" +UP_VERSION="" +PROFILE_NAME="desktop" + +while [ $# -gt 0 ]; do + case "$1" in + --status) STATUS_ONLY=true ;; + --dry-run) DRY_RUN=true ;; + --profile) + if [ $# -lt 2 ]; then echo "--profile 需要一个 profile 名(如 desktop)" >&2; exit 2; fi + PROFILE_NAME="$2"; shift ;; + aps|upstream) TARGET="$1" ;; + -h|--help) : ;; + -*) echo "未知参数: ${1}(用 -h 查看用法)" >&2; exit 2 ;; + *) UP_VERSION="$1" ;; # 版本参数(仅 upstream 用) + esac + shift +done + +PROFILE_DIR="$DSH_HOME/profiles/$PROFILE_NAME" +PKG_JSON="$PROFILE_DIR/package.json" +WS_YML="$PROFILE_DIR/pnpm-workspace.yaml" +PATCH_YML="$PROFILE_DIR/cordis.patch.yml" + +say() { printf '\033[32m[switch]\033[0m %s\n' "$*"; } +warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; } + +command -v node >/dev/null 2>&1 || die "未找到 node(DSH 运行需要 Node.js ≥ 20)。" +[ -d "$PROFILE_DIR" ] || die "找不到 profile 目录:${PROFILE_DIR}(请先初始化 ${PROFILE_NAME} profile)" +[ -f "$PKG_JSON" ] || die "找不到 ${PKG_JSON}(profile 未初始化)" + +# ── 探测当前线(依赖里哪个包在)───────────────────────────────────────── +current_line() { + node -e ' + const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); + const deps = p.dependencies || {}; + if (deps["dsh-apes-sidebar"]) console.log("aps"); + else if (deps["dsh-better-sidebar"]) console.log("upstream"); + else console.log("none"); + ' "$PKG_JSON" +} + +if [ "$STATUS_ONLY" = true ]; then + CUR="$(current_line)" + say "profile: ${PROFILE_NAME} (${PROFILE_DIR})" + case "$CUR" in + aps) say "当前线:aps(${APS_PKG})" ;; + upstream) say "当前线:upstream(${UP_PKG})" ;; + none) say "当前线:无(两条线都未装)" ;; + esac + # 附带显示 bundles 里的 sidebar 条目 + node -e ' + const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); + const b = (p.dsh && p.dsh.profile && p.dsh.profile.bundles) || []; + const side = b.filter(x => /sidebar/.test(x)); + console.log("bundles 里的 sidebar 条目: " + (side.length ? side.join(", ") : "(无)")); + ' "$PKG_JSON" + exit 0 +fi + +[ -n "$TARGET" ] || die "缺少目标线(aps | upstream)。用 --status 查看当前线。" + +CUR="$(current_line)" +case "$TARGET" in + aps) T_PKG="$APS_PKG"; T_DEP="file:${APS_SRC}" ;; + upstream) + T_PKG="$UP_PKG" + # pnpm 从依赖 key 取包名,值只给版本 spec(不含包名,否则双 @) + case "${UP_VERSION:-latest}" in + latest) T_DEP="latest" ;; + *) T_DEP="$UP_VERSION" ;; + esac + ;; +esac + +[ "$CUR" = "$TARGET" ] && { + say "当前已是 ${TARGET} 线(${T_PKG}),无需切换。" + say "如需强制重装:先跑 --status 确认,再手动移除依赖后重跑。" + exit 0 +} + +OTHER_PKG="" +case "$TARGET" in + aps) OTHER_PKG="$UP_PKG" ;; + upstream) OTHER_PKG="$APS_PKG" ;; +esac + +say "切换:${CUR} → ${TARGET}(profile: ${PROFILE_NAME})" +say "目标依赖:${T_DEP}" + +if [ "$DRY_RUN" = true ]; then + say "[dry-run] 步骤 1:移除 ${OTHER_PKG:-(无)} 依赖 + 从 bundles 摘除 ${OTHER_PKG:-(无)}" + say "[dry-run] 步骤 2:写入 ${T_PKG} 依赖(${T_DEP})" + say "[dry-run] 步骤 3:确保 ${WS_YML} 放行 node-pty/protobufjs 构建" + say "[dry-run] 步骤 4:cd ${PROFILE_DIR} && pnpm install(重建 node_modules)" + say "[dry-run] 步骤 5:确保 bundles 含 ${T_PKG}" + say "[dry-run] 步骤 6:幂等移除 ${PATCH_YML} 里 ${OTHER_PKG:-对手线} 的手动挂载行" + say "[dry-run] 步骤 7:提示重启 DSH" + exit 0 +fi + +# ── 步骤 1+2+5 合并:一次 node 调用完成依赖 + bundles 的对称改写 ───────── +# 移除对手线依赖 + bundle,写入目标线依赖 + 确保目标 bundle 在列。幂等。 +node -e ' + const fs = require("fs"); + const p = process.argv[1]; + const keep = process.argv[2]; // 目标包名 + const depSpec = process.argv[3]; // 目标依赖 spec(file:... 或 pkg@ver) + const remove = process.argv[4]; // 对手包名(可能为空串) + const t = JSON.parse(fs.readFileSync(p, "utf8")); + t.dependencies = t.dependencies || {}; + // 移除对手线依赖 + if (remove) { const had = t.dependencies[remove]; delete t.dependencies[remove]; if (had) console.log("removed dep: " + remove + "=" + had); } + // 写入目标线依赖 + t.dependencies[keep] = depSpec; + // bundles:摘除对手线,确保目标线在列 + const prof = (t.dsh && t.dsh.profile) || {}; + const b = prof.bundles || []; + const nb = b.filter(x => x !== remove); + if (!nb.includes(keep)) nb.push(keep); + t.dsh = t.dsh || {}; t.dsh.profile = prof; prof.bundles = nb; + fs.writeFileSync(p, JSON.stringify(t, null, 2) + "\n"); + console.log("deps now: " + JSON.stringify(t.dependencies)); + console.log("bundles now: " + nb.join(", ")); +' "$PKG_JSON" "$T_PKG" "$T_DEP" "$OTHER_PKG" +say "依赖 + bundles 已改写" + +# ── 步骤 3:确保 workspace 放行 node-pty/protobufjs 构建(幂等)───────── +if [ -f "$WS_YML" ]; then + WS_RESULT="$(node -e ' + const fs = require("fs"); + const p = process.argv[1]; + let t = fs.readFileSync(p, "utf8"); + const before = t; + t = t.replace(/^(\s*)(node-pty|protobufjs):.*$/gm, "$1$2: true"); + if (!/^\s*allowBuilds:\s*$/m.test(t)) { + t += "\nallowBuilds:\n node-pty: true\n protobufjs: true\n"; + } else { + for (const k of ["node-pty", "protobufjs"]) { + if (!new RegExp("^\\s*" + k + ":\\s*true\\s*$", "m").test(t)) { + t = t.replace(/^(\s*allowBuilds:\s*)$/m, "$1\n " + k + ": true"); + } + } + } + if (t !== before) fs.writeFileSync(p, t); + console.log(t === before ? "unchanged" : "updated"); + ' "$WS_YML")" + [ "$WS_RESULT" = "updated" ] && say "已确保 ${WS_YML}:allowBuilds(node-pty/protobufjs: true)" \ + || say "workspace 设置已就绪,跳过" +else + warn "找不到 ${WS_YML},跳过 workspace 预写(若 pnpm install 报 ignored build scripts 需手动补)" +fi + +# ── 步骤 4:pnpm install(重建 node_modules——两线不共存,这是预期)────── +command -v pnpm >/dev/null 2>&1 || die "未找到 pnpm。请安装 pnpm 9+ 并加入 PATH。" +say "执行 cd ${PROFILE_DIR} && pnpm install(reconcileProfilePnpmWorkspace 会重建 node_modules)..." +if ! (cd "$PROFILE_DIR" && pnpm install); then + die "pnpm install 失败。请确认网络可用、依赖 spec 正确,然后重试。" +fi + +# ── 步骤 5 校验:bundles 含目标包(挂载生效判据)──────────────────────── +if ! node -e ' + const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")); + const b = (p.dsh && p.dsh.profile && p.dsh.profile.bundles) || []; + process.exit(b.includes(process.argv[2]) ? 0 : 1); +' "$PKG_JSON" "$T_PKG"; then + warn "${T_PKG} 未出现在 dsh.profile.bundles——挂载未注册,请检查 ${PKG_JSON}。" + exit 1 +fi +say "bundle 已注册:dsh.profile.bundles 含 ${T_PKG}(下次启动自动挂载)" + +# ── 步骤 6:幂等移除对手线在 cordis.patch.yml 里的手动挂载行 ───────────── +if [ -f "$PATCH_YML" ]; then + MOUNT_RESULT="$(node -e ' + const fs = require("fs"); + const p = process.argv[1]; + const removePkg = process.argv[2]; + if (!removePkg) { console.log("none"); process.exit(0); } + const lines = fs.readFileSync(p, "utf8").split("\n"); + const out = []; + let i = 0; + let removed = false; + while (i < lines.length) { + const line = lines[i]; + if (/^[ \t]*- insert:\s*$/.test(line)) { + const block = [line]; + let j = i + 1; + while (j < lines.length && lines[j].trim() !== "" && !/^- \s/.test(lines[j])) { + block.push(lines[j]); + j++; + } + if (block.some((l) => new RegExp("id:\\s*" + removePkg.replace(/[-/]/g, "\\$&") + "\\b").test(l))) { + while (out.length && /^[ \t]*#/.test(out[out.length - 1])) out.pop(); + i = j; + removed = true; + continue; + } + } + out.push(line); + i++; + } + if (!removed) console.log("none"); + else { const t = out.join("\n").replace(/\n{3,}/g, "\n\n"); fs.writeFileSync(p, t); console.log("removed"); } + ' "$PATCH_YML" "$OTHER_PKG")" + [ "$MOUNT_RESULT" = "removed" ] && say "已从 ${PATCH_YML} 移除 ${OTHER_PKG} 的手动挂载行" \ + || say "无 ${OTHER_PKG} 手动挂载行,跳过" +else + say "无 ${PATCH_YML},跳过手动挂载行清理" +fi + +# ── 步骤 7:提示重启 ──────────────────────────────────────────────────── +say "切换完成:${CUR} → ${TARGET}(${T_PKG})" +say "下一步:重启 DSH 并硬刷新(Cmd/Ctrl+Shift+R)使新副本生效。"