#!/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)使新副本生效。"