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 (发布时带上)
This commit is contained in:
+6
-1
@@ -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": {
|
||||
|
||||
Executable
+294
@@ -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: <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)使新副本生效。"
|
||||
Reference in New Issue
Block a user