Merge pull request #17 from omdsh-dev/release/v0.10.3
release: v0.10.3 — turnTail 拦截修复 + 安装体验升级(新增 Windows 一键脚本)
This commit is contained in:
@@ -35,43 +35,115 @@ https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86
|
||||
|
||||
## 🚀 安装
|
||||
|
||||
前置:已安装 DSH(`dsh web` 可运行),Node.js ≥ 20、pnpm ≥ 10。插件已发布到 npm:**`dsh-better-sidebar@0.10.2`**(`@deepseek-ai/*` peer 依赖对齐宿主实际版本 `^0.1.0-rc.6` / `@deepseek-ai/cordis@^4.0.1`,同版本单实例)。包内声明了 `dsh.bundle.patch`(随包发布的 `cordis.patch.yml`),安装后由官方 CLI 自动挂载,不修改 DSH 源码。
|
||||
**前置**:已装好 DSH(`dsh web` 能正常运行),Node.js ≥ 20、pnpm ≥ 10。
|
||||
|
||||
### 一键安装(推荐)
|
||||
**macOS / Linux**(Windows 装了 Git Bash 或 WSL 也可):
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash
|
||||
# 指定版本: curl -fsSL <同上> | bash -s 0.10.2
|
||||
# 装完自动重启 DSH: curl -fsSL <同上> | bash -s -- --restart
|
||||
```
|
||||
|
||||
脚本自动完成:`pnpm add` 登记依赖到 `~/.dsh/profiles/web/package.json` → 预写 `allowBuilds`(node-pty/protobufjs,规避 pnpm 11 的构建脚本拦截)→ 识别包内 `dsh.bundle.patch`,把插件自动加进 `dsh.profile.bundles` 挂载 → 幂等清理旧的手动挂载行(防双挂载)。版本默认最新(`latest`),可传参;`--dry-run` 只预览不落盘。
|
||||
**Windows(PowerShell 5.1+ / pwsh)**:
|
||||
|
||||
### 手动安装(npx dsh + 命令链)
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
等价于上面脚本的一连串 bash 命令(全新 profile 首次装可能因构建脚本被拦,链里 `||` 分支会自动批准并重试一次):
|
||||
装完**重启 DSH 并硬刷新**(Cmd/Ctrl+Shift+R)即可看到侧边栏。
|
||||
|
||||
<details>
|
||||
<summary><b>指定版本 / 装完自动重启(可选)</b></summary>
|
||||
|
||||
```sh
|
||||
cd ~/.dsh/profiles/web \
|
||||
&& npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar \
|
||||
|| (pnpm approve-builds --all \
|
||||
&& npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar)
|
||||
# 固定版本:把上面的 dsh-better-sidebar 换成 dsh-better-sidebar@0.10.2
|
||||
# macOS / Linux
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.10.3 --restart
|
||||
|
||||
# Windows PowerShell
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.10.3 -Restart
|
||||
```
|
||||
|
||||
不带 `@<版本>` 时,pnpm 解析 npm 的 `latest` 标签(最新发布版)。`dsh plugin add` 完成:`pnpm add` 登记依赖 → 识别 `dsh.bundle.patch` 自动注册到 `dsh.profile.bundles` → 下次启动自动挂载(无需手写 `cordis.patch.yml`)。
|
||||
不确定的话,可先加 `--dry-run`(PowerShell 用 `-DryRun`)预览步骤再执行。
|
||||
|
||||
> ⚠️ pnpm 11 的两道供应链策略,命令链无法完全规避:
|
||||
> - `strict-dep-builds` 拦截 node-pty/protobufjs 的构建脚本:首次装报 `Ignored build scripts`(包其实已装上、node-pty 预编译产物可直接用),`pnpm approve-builds --all` 一次即可。
|
||||
> - `minimumReleaseAge` 拒绝发布 <24h 的新版本:这是 pnpm 的策略、命令链无法完全规避,仅因插件刚发布不久才会触发,**24 小时后自动消失**(触发时重跑一次,pnpm 会自动补 `minimumReleaseAgeExclude`)。
|
||||
</details>
|
||||
|
||||
### 更新
|
||||
<details>
|
||||
<summary><b>手动安装(逐步命令,想看清每一步)</b></summary>
|
||||
|
||||
与一键脚本等价。**第 ③ 步可重复执行;①② 只需做一次。**
|
||||
|
||||
**macOS / Linux(bash)**:
|
||||
|
||||
```sh
|
||||
cd ~/.dsh/profiles/web
|
||||
|
||||
# ① 放行 node-pty / protobufjs 的构建脚本(pnpm 11 默认拦截;pnpm 10 可跳过)
|
||||
pnpm approve-builds --all
|
||||
|
||||
# ② 放行「发布不足 24h」的新版本(装老版本可跳过;若已有该键,把下面那行并入其下即可)
|
||||
cat >> pnpm-workspace.yaml <<'EOF'
|
||||
minimumReleaseAgeExclude:
|
||||
- dsh-better-sidebar
|
||||
EOF
|
||||
|
||||
# ③ 安装并自动挂载(不带 @版本 = npm 的 latest;固定版本写 dsh-better-sidebar@0.10.3)
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
**Windows(PowerShell)**:
|
||||
|
||||
```powershell
|
||||
cd ~\.dsh\profiles\web
|
||||
|
||||
# ① 放行构建脚本
|
||||
pnpm approve-builds --all
|
||||
|
||||
# ② 放行新版本(一次性;若已有该键,把 - dsh-better-sidebar 并入其下即可)
|
||||
Add-Content -Path pnpm-workspace.yaml -Value "`nminimumReleaseAgeExclude:`n - dsh-better-sidebar"
|
||||
|
||||
# ③ 安装并自动挂载
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>脚本内部做了什么(技术细节)</b></summary>
|
||||
|
||||
一键脚本自动完成 4 件事(全部幂等,可安全重复执行):
|
||||
|
||||
1. 预写 `allowBuilds`(node-pty / protobufjs),规避 pnpm 11 的构建脚本拦截;
|
||||
2. 预写 `minimumReleaseAgeExclude`,放行「发布不足 24 小时」的新版本;
|
||||
3. 执行 `dsh plugin --profile web add dsh-better-sidebar`:登记依赖 → 识别包内 `dsh.bundle.patch` → 自动注册进 `dsh.profile.bundles` 挂载;
|
||||
4. 清理旧版残留的手动挂载行,避免「双挂载」(页面出现两个侧边栏)。
|
||||
|
||||
`curl | bash` / `irm | iex` 会执行远程代码——脚本已随仓库开源(`scripts/install.sh` / `scripts/install.ps1`),可先下载审阅。插件以 npm 包 `dsh-better-sidebar@0.10.3` 发布,通过 `dsh.bundle.patch`(随包的 `cordis.patch.yml`)由官方 CLI 自动挂载,**不修改 DSH 源码**。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>更新</b></summary>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
或重跑一次一键脚本;也可手动把 `~/.dsh/profiles/web/package.json` 版本号改新版后 `pnpm install`。随后重启 DSH 并硬刷新(Cmd/Ctrl+Shift+R)。
|
||||
或重跑一次一键脚本;也可把 `~/.dsh/profiles/web/package.json` 里的版本号改高后 `pnpm install`。改完**重启 DSH 并硬刷新**(Cmd/Ctrl+Shift+R)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>常见问题</b></summary>
|
||||
|
||||
| 现象 | 原因与解决 |
|
||||
|---|---|
|
||||
| 报 `Ignored build scripts` | pnpm 11 拦截构建脚本。跑 `pnpm approve-builds --all`(一键脚本已自动处理)。 |
|
||||
| 报 `minimum release age` / 版本不足 24h | 装的版本发布不足 24 小时。等 24h 或重跑一次(pnpm 会自动补 `minimumReleaseAgeExclude`);一键脚本已自动处理。 |
|
||||
| 报「找不到 profile 目录」 | 先跑一次 `dsh web`,让它初始化 `~/.dsh/profiles/web`。 |
|
||||
| 页面出现**两个侧边栏** | 双挂载:`~/.dsh/profiles/web/cordis.patch.yml` 还留着旧的手动挂载行,删掉那段 `- insert: ... better-sidebar ...`(一键脚本会自动清)。 |
|
||||
| Windows 下终端无法使用 | `node-pty` 依赖预编译二进制;若当前 Node 版本没有对应产物,需装编译工具链(VS Build Tools)。主流 Node 版本一般已有预编译。 |
|
||||
| Windows 没有 bash / curl | 直接用 PowerShell 一键命令;或安装 Git Bash / WSL 再跑 bash 命令。 |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>从源码安装 / 开发(可选,替代 npm 方式)</b></summary>
|
||||
@@ -82,12 +154,15 @@ dsh plugin --profile web add dsh-better-sidebar
|
||||
1. git clone https://github.com/omdsh-dev/DSH-better-sidebar.git ~/Code/DSH-better-sidebar
|
||||
cd ~/Code/DSH-better-sidebar && pnpm install && pnpm build
|
||||
2. ~/.dsh/profiles/web/package.json 的 dependencies 写 "dsh-better-sidebar": "link:<克隆目录绝对路径>"
|
||||
3. ~/.dsh/profiles/web/cordis.patch.yml 追加挂载行(同上)
|
||||
3. ~/.dsh/profiles/web/cordis.patch.yml 追加挂载行:
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-better-sidebar'
|
||||
4. 在 ~/.dsh/profiles/web 执行 pnpm install
|
||||
5. 重启 DSH 并硬刷新
|
||||
```
|
||||
|
||||
更新:`git pull && pnpm install && pnpm build` → 重启 DSH(仅 client 改动可硬刷新)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.10.2"` 再 `pnpm install`。
|
||||
更新:`git pull && pnpm install && pnpm build` → 重启 DSH(仅 client 改动可硬刷新)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.10.3"` 再 `pnpm install`。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+95
-20
@@ -35,43 +35,115 @@ https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
Prerequisites: DSH installed (`dsh web` works), Node.js ≥ 20, pnpm ≥ 10. The plugin is published to npm as **`dsh-better-sidebar@0.10.2`** (`@deepseek-ai/*` peers aligned with the host's actual versions `^0.1.0-rc.6` / `@deepseek-ai/cordis@^4.0.1`, single instance). The package declares `dsh.bundle.patch` (the shipped `cordis.patch.yml`), so the official CLI mounts it after install — the DSH source is never modified.
|
||||
**Prerequisites**: DSH installed (`dsh web` boots), Node.js ≥ 20, pnpm ≥ 10.
|
||||
|
||||
### One-click install (recommended)
|
||||
**macOS / Linux** (also works in Git Bash / WSL on Windows):
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash
|
||||
# Pick a version: curl -fsSL <same URL> | bash -s 0.10.2
|
||||
# Auto-restart DSH: curl -fsSL <same URL> | bash -s -- --restart
|
||||
```
|
||||
|
||||
The script: runs `pnpm add` to register the dependency in `~/.dsh/profiles/web/package.json`, pre-writes `allowBuilds` (node-pty/protobufjs, to dodge pnpm 11's build-script block), detects the package's `dsh.bundle.patch`, auto-appends the plugin to `dsh.profile.bundles`, and idempotently cleans up the old hand-written mount line (to avoid double-mounting). Version defaults to latest (`latest`); `--dry-run` previews without writing.
|
||||
**Windows (PowerShell 5.1+ / pwsh)**:
|
||||
|
||||
### Manual install (npx dsh + command chain)
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
The same steps as a chain of bash commands (a fresh profile may hit the build-script block once; the `||` branch approves and retries):
|
||||
Then **restart DSH and hard-refresh** (Cmd/Ctrl+Shift+R) to see the sidebar.
|
||||
|
||||
<details>
|
||||
<summary><b>Pin a version / auto-restart (optional)</b></summary>
|
||||
|
||||
```sh
|
||||
cd ~/.dsh/profiles/web \
|
||||
&& npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar \
|
||||
|| (pnpm approve-builds --all \
|
||||
&& npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar)
|
||||
# Pin a version: replace dsh-better-sidebar with dsh-better-sidebar@0.10.2
|
||||
# macOS / Linux
|
||||
curl -fsSL https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.sh | bash -s 0.10.3 --restart
|
||||
|
||||
# Windows PowerShell
|
||||
& ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.10.3 -Restart
|
||||
```
|
||||
|
||||
Without `@<version>`, pnpm resolves npm's `latest` tag (the newest release). `dsh plugin add` runs `pnpm add` to register the dependency, detects `dsh.bundle.patch`, auto-appends the plugin to `dsh.profile.bundles`, and the next boot mounts it — no hand-written `cordis.patch.yml` mount line.
|
||||
Not sure? Add `--dry-run` (`-DryRun` in PowerShell) to preview before running.
|
||||
|
||||
> ⚠️ pnpm 11's two supply-chain policies, which no command chain can fully dodge:
|
||||
> - `strict-dep-builds` blocks node-pty/protobufjs build scripts: the first install reports `Ignored build scripts` (the package is already installed and node-pty's prebuilds work as-is); run `pnpm approve-builds --all` once.
|
||||
> - `minimumReleaseAge` rejects releases younger than 24h: this is a pnpm policy that no command chain can fully avoid — it only triggers because the plugin was just published, and **it disappears on its own after 24 hours** (if it fires, just re-run once; pnpm auto-appends a `minimumReleaseAgeExclude` entry).
|
||||
</details>
|
||||
|
||||
### Updating
|
||||
<details>
|
||||
<summary><b>Manual install (step by step)</b></summary>
|
||||
|
||||
Equivalent to the one-click script. **Step ③ is repeatable; ①② only need to run once.**
|
||||
|
||||
**macOS / Linux (bash)**:
|
||||
|
||||
```sh
|
||||
cd ~/.dsh/profiles/web
|
||||
|
||||
# ① Allow node-pty / protobufjs build scripts (pnpm 11 blocks them by default; skip on pnpm 10)
|
||||
pnpm approve-builds --all
|
||||
|
||||
# ② Allow versions published less than 24h ago (skip for older releases; if the key already exists, merge the line under it instead)
|
||||
cat >> pnpm-workspace.yaml <<'EOF'
|
||||
minimumReleaseAgeExclude:
|
||||
- dsh-better-sidebar
|
||||
EOF
|
||||
|
||||
# ③ Install and auto-mount (no @version = npm's latest; pin with dsh-better-sidebar@0.10.3)
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**:
|
||||
|
||||
```powershell
|
||||
cd ~\.dsh\profiles\web
|
||||
|
||||
# ① Allow build scripts
|
||||
pnpm approve-builds --all
|
||||
|
||||
# ② Allow fresh releases (once; if the key already exists, merge - dsh-better-sidebar under it instead)
|
||||
Add-Content -Path pnpm-workspace.yaml -Value "`nminimumReleaseAgeExclude:`n - dsh-better-sidebar"
|
||||
|
||||
# ③ Install and auto-mount
|
||||
npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>What the script does (technical details)</b></summary>
|
||||
|
||||
The one-click script does four things, all idempotent (safe to re-run):
|
||||
|
||||
1. Pre-writes `allowBuilds` (node-pty / protobufjs) to dodge pnpm 11's build-script block;
|
||||
2. Pre-writes `minimumReleaseAgeExclude` to allow versions younger than 24 hours;
|
||||
3. Runs `dsh plugin --profile web add dsh-better-sidebar`: registers the dependency → detects `dsh.bundle.patch` → auto-appends the plugin to `dsh.profile.bundles`;
|
||||
4. Removes any leftover hand-written mount line to avoid double-mounting (two sidebars on the page).
|
||||
|
||||
`curl | bash` / `irm | iex` executes remote code — the scripts are open source in the repo (`scripts/install.sh` / `scripts/install.ps1`); download and review them first if you prefer. The plugin ships as npm package `dsh-better-sidebar@0.10.3` and mounts via `dsh.bundle.patch` (the shipped `cordis.patch.yml`), so the DSH source is never modified.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Updating</b></summary>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-better-sidebar
|
||||
```
|
||||
|
||||
or re-run the one-click script; or bump the version range in `~/.dsh/profiles/web/package.json` (e.g. `"^0.10.2"`) and run `pnpm install`. Then restart DSH and hard-refresh (Cmd/Ctrl+Shift+R).
|
||||
or re-run the one-click script; or bump the version in `~/.dsh/profiles/web/package.json` (e.g. `"^0.10.3"`) and run `pnpm install`. Then restart DSH and hard-refresh (Cmd/Ctrl+Shift+R).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Troubleshooting</b></summary>
|
||||
|
||||
| Symptom | Cause & fix |
|
||||
|---|---|
|
||||
| `Ignored build scripts` | pnpm 11 blocked build scripts. Run `pnpm approve-builds --all` (the one-click script handles it). |
|
||||
| `minimum release age` / version `< 24h` | The release is younger than 24 hours. Wait, or re-run once (pnpm auto-adds `minimumReleaseAgeExclude`); the one-click script handles it. |
|
||||
| "profile directory not found" | Run `dsh web` once so it initializes `~/.dsh/profiles/web`. |
|
||||
| Two sidebars on the page | Double-mount: `~/.dsh/profiles/web/cordis.patch.yml` still has the old hand-written `- insert: ... better-sidebar ...` line — delete it (the one-click script cleans it). |
|
||||
| Terminal fails on Windows | `node-pty` relies on prebuilt binaries; if none match your Node version, install a build toolchain (VS Build Tools). Mainstream Node versions are usually covered. |
|
||||
| No bash / curl on Windows | Use the PowerShell one-click command, or install Git Bash / WSL and run the bash commands. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Install from source / develop (optional — alternative to the npm flow)</b></summary>
|
||||
@@ -82,12 +154,15 @@ To debug local changes or track the dev branch, point the dependency at a local
|
||||
1. git clone https://github.com/omdsh-dev/DSH-better-sidebar.git ~/Code/DSH-better-sidebar
|
||||
cd ~/Code/DSH-better-sidebar && pnpm install && pnpm build
|
||||
2. In ~/.dsh/profiles/web/package.json dependencies write "dsh-better-sidebar": "link:<absolute path of the clone>"
|
||||
3. Append the mount line to ~/.dsh/profiles/web/cordis.patch.yml (same as above)
|
||||
3. Append this mount line to ~/.dsh/profiles/web/cordis.patch.yml:
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-better-sidebar'
|
||||
4. Run pnpm install in ~/.dsh/profiles/web
|
||||
5. Restart DSH and hard-refresh
|
||||
```
|
||||
|
||||
Update: `git pull && pnpm install && pnpm build` → restart DSH (client-only changes can just hard-refresh). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.10.2"` and re-run `pnpm install`.
|
||||
Update: `git pull && pnpm install && pnpm build` → restart DSH (client-only changes can just hard-refresh). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.10.3"` and re-run `pnpm install`.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "dsh-external/dsh-better-sidebar",
|
||||
"version": "0.10.2",
|
||||
"version": "0.10.3",
|
||||
"main": "./lib/index.js",
|
||||
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
|
||||
"engines": {
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dsh-better-sidebar",
|
||||
"version": "0.10.2",
|
||||
"version": "0.10.3",
|
||||
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
@@ -66,6 +66,7 @@
|
||||
"lib/types/**/*.d.ts",
|
||||
"src",
|
||||
"scripts/install.sh",
|
||||
"scripts/install.ps1",
|
||||
"cordis.patch.yml",
|
||||
"README.md",
|
||||
"README_EN.md",
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
# =============================================================================
|
||||
# dsh-better-sidebar 一键安装脚本(官方 CLI 方式,Windows PowerShell 5.1+ / pwsh)
|
||||
#
|
||||
# 通过 DSH 官方插件命令安装 npm 包并自动挂载:
|
||||
# dsh plugin --profile web add dsh-better-sidebar@<version>
|
||||
#
|
||||
# 包内声明了 dsh.bundle.patch(cordis.patch.yml):CLI 的 bundle 协调会把它
|
||||
# 自动加进 profile 的 dsh.profile.bundles,下次启动即挂载——无需手动写
|
||||
# cordis.patch.yml 挂载行。符合仓库硬约束:不修改 DSH 源码,插件永远作为
|
||||
# 独立包被 profile 引用。
|
||||
#
|
||||
# 用法(任选其一):
|
||||
# # 默认最新版
|
||||
# irm https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1 | iex
|
||||
# # 指定版本 / 装完重启
|
||||
# & ([scriptblock]::Create((irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1'))) -Version 0.10.2 -Restart
|
||||
# # 本地保存后运行
|
||||
# powershell -ExecutionPolicy Bypass -File install.ps1 -Version 0.10.2 -DryRun
|
||||
#
|
||||
# 参数:
|
||||
# -Version npm 版本号/范围,缺省 latest(自动解析为最新)。
|
||||
# -Restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅提示)。会断开当前页面会话。
|
||||
# -DryRun 只打印将要执行的操作,不写任何文件。
|
||||
#
|
||||
# 环境变量(均可省略):
|
||||
# DSH_HOME 默认 %USERPROFILE%\.dsh
|
||||
# REGISTRY 默认 https://registry.npmjs.org
|
||||
# DSH_CMD 默认优先 PATH 上的 dsh,缺省回退 npx -y --package @deepseek-ai/dsh
|
||||
#
|
||||
# 说明:
|
||||
# - pnpm 11 的 strict-dep-builds 会拦截 node-pty/protobufjs 的构建脚本并使
|
||||
# `dsh plugin add` 非零退出。脚本会先把这两个构建许可写进 profile 的
|
||||
# pnpm-workspace.yaml(幂等),保证 CLI 一步成功。
|
||||
# - pnpm 11 的 minimumReleaseAge 会拒绝发布 <24h 的新版本。脚本会预写
|
||||
# minimumReleaseAgeExclude(幂等),放行本插件,避免"重跑一次才成功"。
|
||||
# - 老版本(<0.10.2)用手动挂载行,bundle 通道激活后需移除,否则双挂载。
|
||||
# 脚本会幂等移除 better-sidebar 挂载行。
|
||||
# =============================================================================
|
||||
param(
|
||||
[string]$Version = '',
|
||||
[switch]$Restart,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$PKG = 'dsh-better-sidebar'
|
||||
$REGISTRY = if ($env:REGISTRY) { $env:REGISTRY } else { 'https://registry.npmjs.org' }
|
||||
|
||||
# DSH_HOME:DSH_HOME 环境变量 > %USERPROFILE% > $HOME
|
||||
if ($env:DSH_HOME) {
|
||||
$DSH_HOME = $env:DSH_HOME
|
||||
} elseif ($env:USERPROFILE) {
|
||||
$DSH_HOME = Join-Path $env:USERPROFILE '.dsh'
|
||||
} else {
|
||||
$DSH_HOME = Join-Path $HOME '.dsh'
|
||||
}
|
||||
$PROFILE_DIR = Join-Path $DSH_HOME 'profiles\web'
|
||||
$WS_YML = Join-Path $PROFILE_DIR 'pnpm-workspace.yaml'
|
||||
$PATCH_YML = Join-Path $PROFILE_DIR 'cordis.patch.yml'
|
||||
|
||||
function Say([string]$m) { Write-Host "[install] $m" -ForegroundColor Green }
|
||||
function Warn([string]$m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
function Die([string]$m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# 解析版本 -> npm spec("x.y.z" / "^x.y.z" / latest)
|
||||
function Resolve-Spec {
|
||||
param([string]$Given)
|
||||
if ([string]::IsNullOrWhiteSpace($Given) -or $Given -eq 'latest') {
|
||||
$v = $null
|
||||
foreach ($tool in @('npm', 'pnpm')) {
|
||||
if (Get-Command $tool -ErrorAction SilentlyContinue) {
|
||||
$v = (& $tool view $PKG version "--registry=$REGISTRY" 2>$null | Select-Object -Last 1)
|
||||
if ($v) { break }
|
||||
}
|
||||
}
|
||||
if ($v) { return ([string]$v).Trim() }
|
||||
Warn '无法联网解析最新版本(npm/pnpm 查询失败),回退为 latest,由 pnpm 直接解析。'
|
||||
Warn '若已知版本号,可显式传入:-Version 0.10.2'
|
||||
return 'latest'
|
||||
}
|
||||
return $Given
|
||||
}
|
||||
|
||||
# 组装 dsh CLI:优先 PATH 上的 dsh,缺省 npx 拉官方包
|
||||
function Get-DshCli {
|
||||
if ($env:DSH_CMD) { return $env:DSH_CMD }
|
||||
if (Get-Command dsh -ErrorAction SilentlyContinue) { return 'dsh' }
|
||||
if (Get-Command npx -ErrorAction SilentlyContinue) { return 'npx' }
|
||||
return $null
|
||||
}
|
||||
|
||||
# 前置校验
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Die '未找到 node(DSH 运行需要 Node.js >= 20),请先安装 Node.js 并加入 PATH。'
|
||||
}
|
||||
if (-not (Test-Path $PROFILE_DIR)) {
|
||||
Die "找不到 profile 目录:$PROFILE_DIR(请先安装并运行过一次 dsh web)"
|
||||
}
|
||||
if (-not (Test-Path $WS_YML)) {
|
||||
Die "找不到 $WS_YML(请先初始化 web profile)"
|
||||
}
|
||||
|
||||
$SPEC = Resolve-Spec $Version
|
||||
$CLI = Get-DshCli
|
||||
if (-not $CLI) {
|
||||
Die '未找到 dsh 或 npx。请先安装 DSH(并确保 Node/npm 可用),或用 DSH_CMD 指定。'
|
||||
}
|
||||
Say "目标:$CLI plugin --profile web add $PKG@$SPEC(profile: $PROFILE_DIR)"
|
||||
|
||||
if ($DryRun) {
|
||||
Say "[dry-run] 步骤 1:确保 $WS_YML 含 allowBuilds(node-pty/protobufjs: true)与 minimumReleaseAgeExclude($PKG)"
|
||||
Say "[dry-run] 步骤 2:执行 $CLI plugin --profile web add $PKG@$SPEC(安装 + bundle 自动注册)"
|
||||
Say "[dry-run] 步骤 3:校验 dsh.profile.bundles 含 $PKG"
|
||||
Say "[dry-run] 步骤 4:幂等移除 $PATCH_YML 里旧的 better-sidebar 手动挂载行(避免双挂载)"
|
||||
if ($Restart) { Say '[dry-run] 步骤 5:pm2 restart dsh-web' } else { Say '[dry-run] 步骤 5:提示用户手动重启 DSH' }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 步骤 1:预写 workspace 设置(幂等),保证 pnpm 不拦截构建、放行本插件新版本
|
||||
$wsScript = @'
|
||||
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 (!/^\s*-\s+dsh-better-sidebar\s*$/m.test(t)) {
|
||||
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
||||
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-better-sidebar");
|
||||
} else {
|
||||
t += "\nminimumReleaseAgeExclude:\n - dsh-better-sidebar\n";
|
||||
}
|
||||
}
|
||||
if (t !== before) fs.writeFileSync(p, t);
|
||||
console.log(t === before ? "unchanged" : "updated");
|
||||
'@
|
||||
$wsOut = node -e $wsScript "$WS_YML" 2>&1
|
||||
$wsCode = $LASTEXITCODE
|
||||
$wsResult = (($wsOut | Out-String)).Trim()
|
||||
if ($wsCode -ne 0) { Die "处理 $WS_YML 失败(node 退出码 $wsCode):$wsResult" }
|
||||
if ($wsResult -eq 'updated') {
|
||||
Say "已确保 $WS_YML:allowBuilds(node-pty/protobufjs: true)+ minimumReleaseAgeExclude($PKG)"
|
||||
} else {
|
||||
Say 'workspace 设置已就绪,跳过'
|
||||
}
|
||||
|
||||
# 步骤 2:官方 CLI 安装 + bundle 自动注册(含挂载)
|
||||
if ($CLI -eq 'dsh') {
|
||||
$cliArgs = @('plugin', '--profile', 'web', 'add', "$PKG@$SPEC")
|
||||
} else {
|
||||
$cliArgs = @('-y', '--package', '@deepseek-ai/dsh', 'dsh', 'plugin', '--profile', 'web', 'add', "$PKG@$SPEC")
|
||||
}
|
||||
Say "执行 $CLI plugin --profile web add $PKG@$SPEC ..."
|
||||
$addOut = & $CLI @cliArgs 2>&1
|
||||
$addCode = $LASTEXITCODE
|
||||
$addOut | ForEach-Object { $_ }
|
||||
if ($addCode -ne 0) {
|
||||
Warn 'dsh plugin add 失败。已预写 allowBuilds 与 minimumReleaseAgeExclude,仍失败的可能原因:'
|
||||
Warn ' - 网络/登录问题:npm registry 不可达或需要登录。'
|
||||
Warn " - 依赖安装冲突:可手动重试 cd $PROFILE_DIR; pnpm install。"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 步骤 3:校验 bundle 已注册(挂载生效的判据)
|
||||
$pkgJson = Get-Content -Raw (Join-Path $PROFILE_DIR 'package.json') | ConvertFrom-Json
|
||||
$bundles = $pkgJson.dsh.profile.bundles
|
||||
if ($bundles -notcontains $PKG) {
|
||||
Warn 'dsh-better-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册。'
|
||||
Warn "若上面的 pnpm 输出提示 ignored build scripts,请确认 $WS_YML 的 allowBuilds 后重跑本脚本。"
|
||||
exit 1
|
||||
}
|
||||
Say "bundle 已注册:dsh.profile.bundles 包含 $PKG(下次启动自动挂载)"
|
||||
|
||||
# 步骤 4:幂等移除旧的 manual 挂载行(避免与 bundle 双挂载)
|
||||
$mountScript = @'
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
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) => /id:\s*better-sidebar\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");
|
||||
}
|
||||
'@
|
||||
$mountOut = node -e $mountScript "$PATCH_YML" 2>&1
|
||||
$mountCode = $LASTEXITCODE
|
||||
$mountResult = (($mountOut | Out-String)).Trim()
|
||||
if ($mountCode -ne 0) { Die "处理 $PATCH_YML 失败(node 退出码 $mountCode):$mountResult" }
|
||||
if ($mountResult -eq 'removed') {
|
||||
Say "已从 $PATCH_YML 移除旧的 better-sidebar 手动挂载行(bundle 通道接管挂载)"
|
||||
} else {
|
||||
Say '无旧手动挂载行,跳过'
|
||||
}
|
||||
|
||||
Say "安装完成:$PKG@$SPEC"
|
||||
|
||||
# 步骤 5:重启提示
|
||||
if ($Restart) {
|
||||
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
||||
Say '重启 dsh-web(pm2)...'
|
||||
pm2 restart dsh-web
|
||||
if ($LASTEXITCODE -ne 0) { Warn 'pm2 restart 失败,请手动重启 DSH' }
|
||||
} else {
|
||||
Warn '未找到 pm2,请手动重启 DSH(如:pm2 restart dsh-web 或 dsh web)'
|
||||
}
|
||||
} else {
|
||||
Say '下一步:重启 DSH 并硬刷新(Ctrl+Shift+R / Cmd+Shift+R)使新副本生效。'
|
||||
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
||||
Say '本机可用:pm2 restart dsh-web(会短暂断开当前页面会话)'
|
||||
}
|
||||
}
|
||||
+126
-50
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dsh-better-sidebar 一键安装脚本(官方 CLI 方式)
|
||||
# dsh-better-sidebar 一键安装脚本(官方 CLI 方式,macOS / Linux / Windows Git Bash)
|
||||
#
|
||||
# 通过 DSH 官方插件命令安装 npm 包并自动挂载:
|
||||
# dsh plugin --profile web add dsh-better-sidebar@<version>
|
||||
@@ -13,14 +13,15 @@
|
||||
# 用法:
|
||||
# bash scripts/install.sh [版本] [--restart] [--dry-run]
|
||||
#
|
||||
# 版本 npm 版本号/范围,缺省为 latest(自动解析为 ^<最新>)。
|
||||
# 示例:0.10.2、^0.10.2、~0.10.2、latest
|
||||
# 版本 npm 版本号/范围,缺省为 latest(自动解析为 ^<最新>)。
|
||||
# 示例:0.10.2、^0.10.2、~0.10.2、latest
|
||||
# --restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅打印提示)。
|
||||
# 注意:重启会断开当前 DSH 页面会话,默认不自动重启。
|
||||
# --dry-run 只打印将要执行的操作,不写任何文件。
|
||||
# -h/--help 打印本帮助。
|
||||
#
|
||||
# 环境:
|
||||
# DSH_HOME 默认 ~/.dsh
|
||||
# 环境(均可省略,脚本会自动探测):
|
||||
# DSH_HOME 默认 ~/.dsh(Windows Git Bash 下回退 $USERPROFILE/.dsh)
|
||||
# REGISTRY 默认 https://registry.npmjs.org(发布源;装依赖仍走 pnpm 配置)
|
||||
# DSH_CMD 默认优先用 PATH 上的 `dsh`,缺省回退 npx -y --package @deepseek-ai/dsh
|
||||
#
|
||||
@@ -28,6 +29,8 @@
|
||||
# - pnpm 11 的 strict-dep-builds 会拦截 node-pty/protobufjs 的构建脚本并使
|
||||
# `dsh plugin add` 非零退出(bundle 协调因此被跳过)。脚本会先把这两个
|
||||
# 构建许可写进 profile 的 pnpm-workspace.yaml(幂等),保证 CLI 一步成功。
|
||||
# - pnpm 11 的 minimumReleaseAge 会拒绝发布 <24h 的新版本。脚本会预写
|
||||
# minimumReleaseAgeExclude(幂等),放行本插件,避免"重跑一次才成功"。
|
||||
# - 老版本(<0.10.2)用手动挂载行,bundle 通道激活后需移除,否则双挂载
|
||||
# (Node 半挂两次、页面两个侧边栏)。脚本会幂等移除 better-sidebar 挂载行。
|
||||
# - 回滚:dsh plugin --profile web remove dsh-better-sidebar,或把 profile 依赖
|
||||
@@ -35,7 +38,25 @@
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
|
||||
# 帮助请求优先处理
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
|
||||
cat <<'EOF'
|
||||
dsh-better-sidebar 一键安装脚本
|
||||
|
||||
用法:bash scripts/install.sh [版本] [--restart] [--dry-run]
|
||||
|
||||
版本 npm 版本号/范围,缺省 latest(自动解析为最新)。示例:0.10.2、^0.10.2、latest
|
||||
--restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅提示)
|
||||
--dry-run 只打印将要执行的操作,不写任何文件
|
||||
|
||||
环境变量(可省略):DSH_HOME(默认 ~/.dsh)、REGISTRY(npm 源)、DSH_CMD(dsh 命令)
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
|
||||
PROFILE_DIR="$DSH_HOME/profiles/web"
|
||||
WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
|
||||
PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
|
||||
@@ -50,7 +71,8 @@ for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--restart) RESTART=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
-*) echo "未知参数: $arg" >&2; exit 2 ;;
|
||||
-h|--help) : ;; # 已在上面处理
|
||||
-*) echo "未知参数: ${arg}(用 -h 查看用法)" >&2; exit 2 ;;
|
||||
*) VERSION_SPEC="$arg" ;;
|
||||
esac
|
||||
done
|
||||
@@ -59,15 +81,25 @@ say() { printf '\033[32m[install]\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; }
|
||||
|
||||
# 解析用户给的版本 -> CLI 要用的 npm spec("x.y.z" / "^x.y.z")
|
||||
# 解析用户给的版本 -> CLI 要用的 npm spec("x.y.z" / "^x.y.z" / latest)
|
||||
resolve_spec() {
|
||||
local given="${1:-latest}"
|
||||
case "$given" in
|
||||
latest)
|
||||
local v
|
||||
v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" \
|
||||
|| die "无法从 $REGISTRY 解析 $PKG 的最新版本(请检查网络/登录)。可显式传版本号:bash scripts/install.sh 0.10.2"
|
||||
printf '%s' "$v"
|
||||
local v=""
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
||||
fi
|
||||
if [ -z "$v" ] && command -v pnpm >/dev/null 2>&1; then
|
||||
v="$(pnpm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
||||
fi
|
||||
if [ -n "$v" ]; then
|
||||
printf '%s' "$v"
|
||||
else
|
||||
warn "无法联网解析最新版本(npm/pnpm 查询失败),回退为 latest,由 pnpm 直接解析。"
|
||||
warn "若已知版本号,可显式传入:bash scripts/install.sh 0.10.2"
|
||||
printf 'latest'
|
||||
fi
|
||||
;;
|
||||
*) printf '%s' "$given" ;;
|
||||
esac
|
||||
@@ -77,11 +109,16 @@ resolve_spec() {
|
||||
dsh_cli() {
|
||||
if command -v "$DSH_CMD" >/dev/null 2>&1; then
|
||||
printf '%s' "$DSH_CMD"
|
||||
else
|
||||
elif command -v npx >/dev/null 2>&1; then
|
||||
printf 'npx -y --package @deepseek-ai/dsh dsh'
|
||||
else
|
||||
die "未找到 dsh 或 npx。请先安装 DSH(并确保 Node/npm 可用),或用 DSH_CMD 指定 dsh 路径。"
|
||||
fi
|
||||
}
|
||||
|
||||
# 前置校验
|
||||
command -v node >/dev/null 2>&1 || die "未找到 node(DSH 运行需要 Node.js ≥ 20),请先安装 Node.js 并加入 PATH。"
|
||||
|
||||
[ -d "$PROFILE_DIR" ] || die "找不到 profile 目录:${PROFILE_DIR}(请先安装并运行过一次 dsh web)"
|
||||
[ -f "$WS_YML" ] || die "找不到 ${WS_YML}(请先初始化 web profile)"
|
||||
|
||||
@@ -90,35 +127,52 @@ CLI="$(dsh_cli)"
|
||||
say "目标:$CLI plugin --profile web add $PKG@${SPEC}(profile: ${PROFILE_DIR})"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
say "[dry-run] 步骤 1:确保 $WS_YML 含 allowBuilds(node-pty/protobufjs: true)"
|
||||
say "[dry-run] 步骤 1:确保 $WS_YML 含 allowBuilds(node-pty/protobufjs: true)与 minimumReleaseAgeExclude(${PKG})"
|
||||
say "[dry-run] 步骤 2:执行 $CLI plugin --profile web add $PKG@${SPEC}(安装 + bundle 自动注册)"
|
||||
say "[dry-run] 步骤 3:幂等移除 $PATCH_YML 里旧的 better-sidebar 手动挂载行(避免双挂载)"
|
||||
if [ "$RESTART" = true ]; then say "[dry-run] 步骤 4:pm2 restart dsh-web"; else say "[dry-run] 步骤 4:提示用户手动重启 DSH"; fi
|
||||
say "[dry-run] 步骤 3:校验 dsh.profile.bundles 含 $PKG"
|
||||
say "[dry-run] 步骤 4:幂等移除 $PATCH_YML 里旧的 better-sidebar 手动挂载行(避免双挂载)"
|
||||
if [ "$RESTART" = true ]; then say "[dry-run] 步骤 5:pm2 restart dsh-web"; else say "[dry-run] 步骤 5:提示用户手动重启 DSH"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 步骤 1:预写 allowBuilds(幂等),保证 pnpm 不为 node-pty/protobufjs 拦截构建
|
||||
if ! grep -qE '^\s*node-pty: true\s*$' "$WS_YML"; then
|
||||
# 清除 pnpm 自动写入的占位值("set this to true or false")后写入 true
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
t = t.replace(/node-pty:.*/g, "node-pty: true");
|
||||
t = t.replace(/protobufjs:.*/g, "protobufjs: true");
|
||||
if (!t.includes("allowBuilds:")) t += "\nallowBuilds:\n node-pty: true\n protobufjs: true\n";
|
||||
fs.writeFileSync(p, t);
|
||||
' "$WS_YML"
|
||||
say "已确保 $WS_YML 的 allowBuilds(node-pty/protobufjs: true)"
|
||||
else
|
||||
say "allowBuilds 已就绪,跳过"
|
||||
fi
|
||||
# 步骤 1:预写 workspace 设置(幂等),保证 pnpm 不拦截构建、不放行旧版本策略
|
||||
WS_RESULT="$(node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
const before = t;
|
||||
// allowBuilds:把 node-pty/protobufjs 归位为 true
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
// minimumReleaseAgeExclude:放行本插件(版本无关),避免 <24h 新版本被拒
|
||||
if (!/^\s*-\s+dsh-better-sidebar\s*$/m.test(t)) {
|
||||
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
||||
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-better-sidebar");
|
||||
} else {
|
||||
t += "\nminimumReleaseAgeExclude:\n - dsh-better-sidebar\n";
|
||||
}
|
||||
}
|
||||
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)+ minimumReleaseAgeExclude(${PKG})" \
|
||||
|| say "workspace 设置已就绪,跳过"
|
||||
|
||||
# 步骤 2:官方 CLI 安装 + bundle 自动注册(含挂载)
|
||||
say "执行 $CLI plugin --profile web add $PKG@$SPEC ..."
|
||||
if ! $CLI plugin --profile web add "$PKG@$SPEC" 2>&1 | tail -n +1; then
|
||||
warn "dsh plugin add 失败。可能原因:minimumReleaseAge(发布 <24h,pnpm 通常自动写入排除并重试)"
|
||||
warn "或 allowBuilds 未生效。可手动重试:cd $PROFILE_DIR && pnpm install"
|
||||
warn "dsh plugin add 失败。已预写 allowBuilds 与 minimumReleaseAgeExclude,仍失败的可能原因:"
|
||||
warn " - 网络/登录问题:npm registry 不可达或需要登录。"
|
||||
warn " - 依赖安装冲突:可手动重试 cd $PROFILE_DIR && pnpm install。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -133,24 +187,46 @@ if ! node -e '
|
||||
warn "若上面的 pnpm 输出提示 ignored build scripts,请确认 $WS_YML 的 allowBuilds 后重跑本脚本。"
|
||||
exit 1
|
||||
fi
|
||||
say "bundle 已注册:dsh.profile.bundles 包含 $PKG(下次启动自动挂载)"
|
||||
say "bundle 已注册:dsh.profile.bundles 包含 ${PKG}(下次启动自动挂载)"
|
||||
|
||||
# 步骤 3:幂等移除旧的 manual 挂载行(避免与 bundle 双挂载)
|
||||
if grep -qE '^\s*- id: better-sidebar\b' "$PATCH_YML"; then
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
// 移除 better-sidebar 的 insert 块(含其前的注释行)
|
||||
t = t.replace(/(?:^[ \t]*#[^\n]*\n)*[ \t]*- insert:\n[ \t]+- id: better-sidebar\n[ \t]+name: '"'"'dsh-better-sidebar'"'"'\n?/g, "");
|
||||
// 清理可能留下的空行堆积
|
||||
t = t.replace(/\n{3,}/g, "\n\n");
|
||||
fs.writeFileSync(p, t);
|
||||
' "$PATCH_YML"
|
||||
say "已从 $PATCH_YML 移除旧的 better-sidebar 手动挂载行(bundle 通道接管挂载)"
|
||||
else
|
||||
say "无旧手动挂载行,跳过"
|
||||
fi
|
||||
MOUNT_RESULT="$(node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
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) => /id:\s*better-sidebar\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")"
|
||||
[ "$MOUNT_RESULT" = "removed" ] \
|
||||
&& say "已从 $PATCH_YML 移除旧的 better-sidebar 手动挂载行(bundle 通道接管挂载)" \
|
||||
|| say "无旧手动挂载行,跳过"
|
||||
|
||||
say "安装完成:$PKG@$SPEC"
|
||||
|
||||
|
||||
@@ -57,9 +57,22 @@ export function SidebarProducedFiles(props: {
|
||||
)
|
||||
}
|
||||
|
||||
/** Register the turn-tail interception (returns the disposer). */
|
||||
/**
|
||||
* Register the turn-tail interception (returns the disposer).
|
||||
*
|
||||
* The slot is a CHILD slot the host's ui-conversation declares in its
|
||||
* `conversation.chat.node` children table (kind: chain, scope: session).
|
||||
* Registering it directly races the declaration — the ui-slots core's
|
||||
* load-time validation throws "not declared (a parent entry's children
|
||||
* table must declare it)" when the parent entry is not on the ledger yet.
|
||||
* slots.inject waits for the declaration: the callback runs synchronously
|
||||
* when the slot is already declared, otherwise it runs inside the declaring
|
||||
* register() call once the declaration commits; declaration collapse
|
||||
* disposes the entry and a later declaration re-registers it. This mirrors
|
||||
* @deepseek-ai/dsh-client-ui-deliverables' registration of the same slot.
|
||||
*/
|
||||
export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void {
|
||||
return ctx.slots.register({
|
||||
return ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
|
||||
name: 'conversation.chat.turnTail',
|
||||
// Decline the takeover while the editor tab type is disabled in the side
|
||||
// card settings: the produced-files row falls back to the default
|
||||
@@ -73,7 +86,7 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
|
||||
inject: (sessionId: string) => ({
|
||||
openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
|
||||
}),
|
||||
}, SidebarProducedFiles)
|
||||
}, SidebarProducedFiles))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Turn-tail interception registration spec (issue #15): `registerTurnTailInterception`
|
||||
* must go through `ctx.slots.inject` — the slot is a CHILD slot the host's
|
||||
* ui-conversation declares in its `conversation.chat.node` children table, so a
|
||||
* direct `slots.register` races the declaration and the ui-slots core throws
|
||||
* "not declared (a parent entry's children table must declare it)".
|
||||
*
|
||||
* The fake `slots` mirrors SlotRegistry.inject's semantics: run the callback
|
||||
* synchronously when the slot is already declared; otherwise wait and run it
|
||||
* when the declaration commits; the returned disposer cancels a pending wait
|
||||
* and disposes any active registration; the register disposer is idempotent.
|
||||
*/
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createSidebarStore } from '../src/client/state.ts'
|
||||
import { registerTurnTailInterception } from '../src/client/intercept.tsx'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
|
||||
interface RegisteredSlot {
|
||||
options: Record<string, unknown>
|
||||
component: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A structural fake of the client slots service. `declared` selects the
|
||||
* timing: already-on-ledger (callback runs synchronously) vs. pending
|
||||
* (callback runs on `declare()`, unless the controller was disposed first).
|
||||
*/
|
||||
const fakeSlots = (declared: boolean) => {
|
||||
const registered: RegisteredSlot[] = []
|
||||
const disposals: number[] = []
|
||||
const pendings: Array<() => void> = []
|
||||
return {
|
||||
registered,
|
||||
disposals,
|
||||
pendings,
|
||||
slots: {
|
||||
register: (options: Record<string, unknown>, component: unknown) => {
|
||||
registered.push({ options, component })
|
||||
return () => { disposals.push(1) }
|
||||
},
|
||||
inject: (key: string, callback: () => () => void) => {
|
||||
if (key !== 'conversation.chat.turnTail') {
|
||||
throw new Error(`unexpected injected key "${key}"`)
|
||||
}
|
||||
let active: (() => void) | undefined
|
||||
let stopped = false
|
||||
const run = (): void => {
|
||||
if (stopped || active !== undefined) return
|
||||
active = callback()
|
||||
}
|
||||
if (declared) run()
|
||||
else pendings.push(run)
|
||||
return () => {
|
||||
stopped = true
|
||||
active?.()
|
||||
active = undefined
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** A produced-files owner currency: one closing assistant seq + its nodes. */
|
||||
const producedOwner = (paths: string[]): unknown => ({
|
||||
nodes: [
|
||||
{ kind: 'assistant', seq: 1, turn: 1 },
|
||||
...paths.map(path => ({
|
||||
kind: 'tool-result', isError: false, callView: { card: 'diff', locations: [{ path }] },
|
||||
})),
|
||||
{ kind: 'assistant', seq: 2, turn: 1 },
|
||||
],
|
||||
seq: 2,
|
||||
})
|
||||
|
||||
const emptyOwner = (): unknown => ({ nodes: [{ kind: 'assistant', seq: 1, turn: 1 }], seq: 1 })
|
||||
|
||||
/** The minimal client-context fake the registration (and its seats) touch. */
|
||||
const clientCtx = (slots: unknown): Context => ({
|
||||
slots,
|
||||
sessions: {
|
||||
list: { getSnapshot: () => ({ current: 's1', byId: { s1: { id: 's1', cwd: '/w', displayTitle: 's1' } } }) },
|
||||
},
|
||||
betterSidebar: { openTab: vi.fn() },
|
||||
} as unknown as Context)
|
||||
|
||||
describe('turn-tail interception registration (issue #15)', () => {
|
||||
it('registers through slots.inject and lands once the slot is already declared', () => {
|
||||
const fake = fakeSlots(true)
|
||||
const store = createSidebarStore()
|
||||
const restore = registerTurnTailInterception(clientCtx(fake.slots), store)
|
||||
|
||||
// Exactly one registration, with the takeover descriptor.
|
||||
expect(fake.registered).toHaveLength(1)
|
||||
const { options, component } = fake.registered[0]!
|
||||
expect(options.name).toBe('conversation.chat.turnTail')
|
||||
expect(options.priority).toBe(-1)
|
||||
expect(options.registrant).toBe('dsh-better-sidebar')
|
||||
expect(options.select).toBeTypeOf('function')
|
||||
expect(options.inject).toBeTypeOf('function')
|
||||
expect(component).toBeTypeOf('function')
|
||||
|
||||
// Disposal removes the registration; the disposer is idempotent.
|
||||
restore()
|
||||
expect(fake.disposals).toHaveLength(1)
|
||||
restore()
|
||||
expect(fake.disposals).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('waits for the host declaration instead of throwing (the pre-fix race)', () => {
|
||||
const fake = fakeSlots(false)
|
||||
const store = createSidebarStore()
|
||||
const restore = registerTurnTailInterception(clientCtx(fake.slots), store)
|
||||
|
||||
// The slot is undeclared: nothing registered yet, no error.
|
||||
expect(fake.registered).toHaveLength(0)
|
||||
|
||||
// The host's ui-conversation commits the declaration → the entry lands.
|
||||
expect(fake.pendings).toHaveLength(1)
|
||||
fake.pendings[0]!()
|
||||
expect(fake.registered).toHaveLength(1)
|
||||
|
||||
// A later re-declaration does not double-register while the entry is live.
|
||||
fake.pendings[0]!()
|
||||
expect(fake.registered).toHaveLength(1)
|
||||
|
||||
restore()
|
||||
expect(fake.disposals).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('a disposal before the declaration cancels the wait permanently', () => {
|
||||
const fake = fakeSlots(false)
|
||||
const store = createSidebarStore()
|
||||
const restore = registerTurnTailInterception(clientCtx(fake.slots), store)
|
||||
restore()
|
||||
|
||||
// The declaration arrives after the controller was disposed: ignored.
|
||||
fake.pendings[0]!()
|
||||
expect(fake.registered).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('declines the takeover while the editor tab is disabled in the settings', () => {
|
||||
const fake = fakeSlots(true)
|
||||
const store = createSidebarStore()
|
||||
const restore = registerTurnTailInterception(clientCtx(fake.slots), store)
|
||||
const select = fake.registered[0]!.options.select as (owner: unknown) => unknown
|
||||
|
||||
// Enabled (default): a produced turn claims the chain; an empty one declines.
|
||||
expect(select(producedOwner(['a.ts', 'b.ts']))).toEqual(['a.ts', 'b.ts'])
|
||||
expect(select(emptyOwner())).toBeNull()
|
||||
|
||||
// Editor tab disabled: even a produced turn falls back to the default
|
||||
// deliverables row (chips that cannot open must not be offered).
|
||||
store.setPrefs({ ...store.getPrefs(), tabsEnabled: { editor: false } })
|
||||
expect(select(producedOwner(['a.ts']))).toBeNull()
|
||||
|
||||
restore()
|
||||
})
|
||||
|
||||
it('wires the openInSidebar seat to the sidebar editor tab', () => {
|
||||
const fake = fakeSlots(true)
|
||||
const ctx = clientCtx(fake.slots)
|
||||
const store = createSidebarStore()
|
||||
const restore = registerTurnTailInterception(ctx, store)
|
||||
const inject = fake.registered[0]!.options.inject as (sessionId: string) => {
|
||||
openInSidebar: (path: string) => void
|
||||
}
|
||||
|
||||
// The seat hands the session-scoped opener to the chips row.
|
||||
const seat = inject('s1')
|
||||
expect(seat.openInSidebar).toBeTypeOf('function')
|
||||
seat.openInSidebar('/w/src/a.ts')
|
||||
expect(ctx.betterSidebar.openTab).toHaveBeenCalledWith({
|
||||
type: 'editor',
|
||||
title: 'a.ts',
|
||||
path: '/w/src/a.ts',
|
||||
id: 'editor:/w/src/a.ts',
|
||||
})
|
||||
|
||||
restore()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user