Files
apes-sidebar/scripts/switch.sh
T
figmar 34e86141bd
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
feat(scripts): switch.sh — apes↔upstream 两线对称切换
- 同一 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 (发布时带上)
2026-09-07 02:25:15 +08:00

295 lines
12 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.
#!/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: <file: 本地路径>(本地开发)或
# 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 <aps|upstream> [版本]
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)使新副本生效。"