bookmark-research 0.5.0-beta.5 → 0.5.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/bundle.patch.yml +18 -0
- package/docs/distribution.md +1 -1
- package/docs/harness-compatibility.md +7 -5
- package/docs/host-validation.json +31 -0
- package/docs/installation.en.md +10 -1
- package/docs/installation.md +11 -2
- package/docs/release-notes-0.5.0-beta.6.md +30 -0
- package/hosts/dsh/package.json +3 -0
- package/hosts/dsh/plugin.js +49 -1
- package/package.json +5 -3
- package/scripts/build_zip.py +1 -0
- package/scripts/export_bundle.py +43 -20
- package/scripts/host_assets.py +4 -0
- package/scripts/host_clients.py +69 -7
- package/scripts/host_install.py +1 -1
- package/scripts/npm_source.py +33 -4
package/README.md
CHANGED
|
@@ -22,13 +22,13 @@ Works in **Codex, Claude Code, Pi and DSH (DeepSeek Harness)**.
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
-
**Current beta (`0.5.0-beta.
|
|
25
|
+
**Current beta (`0.5.0-beta.6`)**, interactive on macOS, Linux and Windows:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
npx bookmark-research@0.5.0-beta.
|
|
28
|
+
npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
This pins both the installer and the content. The `@latest` examples below follow the default channel, which remains `0.5.0-beta.3`; `@beta` currently points to `0.5.0-beta.
|
|
31
|
+
This pins both the installer and the content. The `@latest` examples below follow the default channel, which remains `0.5.0-beta.3`; `@beta` currently points to `0.5.0-beta.6`. Changing only the npx selector does not change the installer's default content source.
|
|
32
32
|
|
|
33
33
|
**Default-channel interactive installation:**
|
|
34
34
|
|
|
@@ -51,6 +51,8 @@ For a permanent `bookmark-research` command: `npm install -g bookmark-research`.
|
|
|
51
51
|
|
|
52
52
|
**One release source for all four hosts:** npm contains the shared runtime, Skill and host adapters. The installer downloads the release, prepares the selected host format, then invokes the host’s native commands. Updates retain an npm selector rather than an npx cache path. GitHub hosts source and installation catalogs; unpublished source uses the Python installer. See [distribution policy and official references](docs/distribution.md).
|
|
53
53
|
|
|
54
|
+
**DSH installs the release directly:** the published package declares `dsh.bundle.patch`, so `dsh plugin --profile <name> add bookmark-research@<version>` — or the DSH plugin manager — installs it with no export first, resolving Python and MCP paths from the installed package. DeepSeek Harness Desktop reserves the `desktop` profile for its own carrier, so install that one with the app's CLI. See the [installation guide](docs/installation.en.md).
|
|
55
|
+
|
|
54
56
|
**Windows:** the installer writes the detected Python path into each client's MCP config, because Windows often has no working `python3`. Install Codex with `bookmark-research install codex` rather than adding the repository to Codex directly. `bookmark-research status` shows the interpreter in use.
|
|
55
57
|
|
|
56
58
|
Several at once: `npx bookmark-research@latest install claude dsh --profile web --non-interactive`. Project scope: `--scope project --project /path/to/project` (Claude Code, Pi). All options: [installation guide](docs/installation.en.md).
|
package/README.zh.md
CHANGED
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
|
|
23
23
|
## 安装
|
|
24
24
|
|
|
25
|
-
**当前测试版(`0.5.0-beta.
|
|
25
|
+
**当前测试版(`0.5.0-beta.6`)**,支持 macOS、Linux、Windows 交互安装:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
npx bookmark-research@0.5.0-beta.
|
|
28
|
+
npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
这条命令同时固定安装器和插件内容。下面的 `@latest` 示例使用默认渠道,目前仍为 `0.5.0-beta.3`;`@beta` 当前指向 `0.5.0-beta.
|
|
31
|
+
这条命令同时固定安装器和插件内容。下面的 `@latest` 示例使用默认渠道,目前仍为 `0.5.0-beta.3`;`@beta` 当前指向 `0.5.0-beta.6`。只改 npx 的版本选择,不会改变安装器默认下载的内容来源。
|
|
32
32
|
|
|
33
33
|
**默认渠道的交互安装:**
|
|
34
34
|
|
|
@@ -51,6 +51,8 @@ npx bookmark-research@latest install
|
|
|
51
51
|
|
|
52
52
|
**四个宿主统一发布来源:** npm 包包含共享运行时、Skill 和宿主适配。安装器下载发布包,生成所选宿主格式,再调用宿主原生命令;更新记录 npm 来源,不依赖 npx 临时缓存。GitHub 提供源码和安装清单,未发布源码用 Python 安装器。见[发布约定与官方依据](docs/distribution.md)。
|
|
53
53
|
|
|
54
|
+
**DSH 可直接安装发布包:** 发布包声明 `dsh.bundle.patch`,`dsh plugin --profile <名称> add bookmark-research@<版本>`(或 DSH 插件管理界面)无需先导出即可安装,Python 与 MCP 路径按安装位置解析。DeepSeek Harness 桌面版把 `desktop` profile 保留给自身载体,该 profile 请用桌面版自带 CLI 安装。详见[安装说明](docs/installation.md)。
|
|
55
|
+
|
|
54
56
|
**Windows:** Windows 上常常没有能用的 `python3`,所以安装器会把检测到的 Python 路径写进各宿主的 MCP 配置。Codex 请用 `bookmark-research install codex` 安装,不要直接把仓库加进 Codex。`bookmark-research status` 可以查看实际使用的 Python。
|
|
55
57
|
|
|
56
58
|
一次装多个:`npx bookmark-research@latest install claude dsh --profile web --non-interactive`。装到指定项目:`--scope project --project /path/to/project`(Claude Code、Pi)。全部参数见[安装说明](docs/installation.md)。
|
package/bundle.patch.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# DeepSeek Harness bundle layer. Paths resolve from the installed package through
|
|
2
|
+
# the module's context, so this file is identical in the npm package and in an
|
|
3
|
+
# exported copy; `python` and the forwarded environment come from the module
|
|
4
|
+
# (see hosts/dsh/plugin.js), not from this machine.
|
|
5
|
+
- insert:
|
|
6
|
+
- id: bookmark-research-skill
|
|
7
|
+
name: bookmark-research
|
|
8
|
+
- id: bookmark-research-mcp
|
|
9
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
10
|
+
inject: [bookmarkResearchPaths]
|
|
11
|
+
config:
|
|
12
|
+
serverName: bookmark-research
|
|
13
|
+
transport: stdio
|
|
14
|
+
command: !!js ctx.bookmarkResearchPaths.python.command
|
|
15
|
+
args: !!js ctx.bookmarkResearchPaths.mcpArgs
|
|
16
|
+
cwd: !!js ctx.bookmarkResearchPaths.root
|
|
17
|
+
env: !!js ctx.bookmarkResearchPaths.env
|
|
18
|
+
failOnStartupError: true
|
package/docs/distribution.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
发布顺序:修改源码版本并完成 CI,发布已经验证的 npm 包,确认 registry 可取得该版本,再更新 GitHub marketplace 的固定版本。未发布源码不能通过 npm 入口安装;需要先用源码入口测试。
|
|
21
21
|
|
|
22
|
-
测试版使用 npm 的 `beta` 标签;若创建 GitHub 版本说明,则标记为预发布。不会自动移动 npm 的 `latest` 标签。安装测试版时同时固定安装器与内容来源,例如 `npx bookmark-research@0.5.0-beta.
|
|
22
|
+
测试版使用 npm 的 `beta` 标签;若创建 GitHub 版本说明,则标记为预发布。不会自动移动 npm 的 `latest` 标签。安装测试版时同时固定安装器与内容来源,例如 `npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6`。仅将 npx 改为 `@beta` 不会改变默认内容来源 `latest`;已固定的旧版安装也不会自动换成测试版。
|
|
23
23
|
|
|
24
24
|
## 官方资料与项目实例
|
|
25
25
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 宿主兼容、工作流与打包
|
|
2
2
|
|
|
3
|
-
更新日期:2026-
|
|
3
|
+
更新日期:2026-10-08。四宿主共用同一 Skill、Python CLI、stdio MCP 和研究档案。当前宿主模型负责规划、搜索/阅读、判断证据、按缺口补查及综合;原生协作支持独立分工。Deep Research 在这条流程上增加持续调查、独立核验、覆盖检查和报告。外部研究 API 可承担具体子问题,缺少它们仍能研究。
|
|
4
4
|
|
|
5
|
-
**正文依据、文件生成、隔离脚本检查、真实 MCP 调用和宿主端到端研究是不同验证层级。**
|
|
5
|
+
**正文依据、文件生成、隔离脚本检查、真实 MCP 调用和宿主端到端研究是不同验证层级。** 本轮在 DSH 桌面版 profile `desktop` 上完成了一次真实安装,并观察到 Skill 注册与 MCP 工具实际应答;Pi 仍未安装,未改全局配置,未启动付费研究模型。具体脚本调用和研究规则见 [宿主工作流参考](../skills/bookmark-research/references/host-workflows.md)。
|
|
6
6
|
|
|
7
7
|
| 宿主 | 协作基础与本插件入口 | 生命周期与限制 |
|
|
8
8
|
| --- | --- | --- |
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
| Claude Code 插件 | `.claude-plugin/plugin.json`;当前文档允许省略 manifest 后按约定发现组件 | 根 `skills/`、`.mcp.json` 或 manifest 配置 | 单独保留 Claude manifest/MCP 配置 |
|
|
59
59
|
| Agent Plugins 1.0.0 标准包 | **根 `plugin.json`**,必填 `$schema` 与 `name` | 固定发现根 `skills/`、`mcp.json` | 从同一源码独立导出标准包 |
|
|
60
60
|
| Pi | `package.json` 的 `pi` 字段,或资源目录约定 | 原生 skills/extensions;MCP 要靠扩展 | `pi.skills`+Skill 调用 CLI;多组任务另登记项目工作流 |
|
|
61
|
-
| DeepSeek Harness(DSH) |
|
|
61
|
+
| DeepSeek Harness(DSH) | 包在 `package.json` 声明 `dsh.bundle.patch`,由 `dsh plugin` 或插件管理器安装;`--patch` 仍可手工加载 Cordis 配置 | 原生 Skill provider;官方 `dsh-mcp-client` 桥接 MCP tools | 发布包自带按安装位置解析的 `bundle.patch.yml`,registry 可直接安装;绝对路径 `cordis.patch.yml` 仅作手工适配 |
|
|
62
62
|
|
|
63
63
|
这些入口不是同一文件的不同别名。符合 Agent Skills 的 `SKILL.md` 和 MCP 协议是主要复用边界;某个客户端能加载 Skill,并不表示它已经支持 Agent Plugins 的根 manifest。[H01][H04][H05][P02][D02]
|
|
64
64
|
|
|
@@ -157,7 +157,9 @@ DSH 的原生运行插件是 Cordis 模块,可导出 `name`、`inject` 和 `ap
|
|
|
157
157
|
|
|
158
158
|
`command` 与 `args` 分开传递。不要在这个 DSH 片段里假设 `${PLUGIN_ROOT}` 会像 Codex/Agent Plugins 一样展开;本轮核实的 MCP 客户端并未定义这种插件根占位符约定。绝对路径片段适合本机 checkout;搬迁或重新安装到其他位置后需要重新生成。DSH 的 patch 文件位置也不会改变模块路径解析所依据的 profile 目录。[D03][D05]
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
发布包现在**自带** `bundle.patch.yml` 并在 `package.json` 声明 `dsh.bundle.patch`,因此 `dsh plugin --profile <name> add bookmark-research@<version>`(以及 DSH 插件管理界面)可以直接从 registry 安装;`dsh` 的 bundle 对账逻辑会把声明了 `dsh.bundle` 且 patch 存在的包自动加入 `dsh.profile.bundles`。层内不写死本机路径:`command`、`args`、`cwd` 与转发的环境变量都由入口模块 `hosts/dsh/plugin.js` 在加载时提供(`bookmarkResearchPaths.python/mcpArgs/env`),模块用与 npm 启动器相同的顺序发现 Python 3.9+/SQLite,所以导出副本、registry 安装和 Windows 都不需要重生成 patch。统一安装器仍导出带校验和的固定副本,用于可验证的托管安装;对不含该文件的旧发布包或旧 Git ref,导出器按模块的旧契约生成等价层,安装不回归。[D02][D03]
|
|
161
|
+
|
|
162
|
+
DSH 桌面版把 profile 名 `desktop` 保留给 Electron 载体:启动器对该名字拒绝一切 boot/dump(`--dump-config` 亦然),只有载体自身可以管理其插件。因此对 `desktop` 的安装须用桌面版自带 CLI(`--dsh "<App>/Contents/Resources/runtime/cli/bin/dsh"`,它自带 pnpm,无需 PATH 中的 pnpm),安装器的验证改为读取持久层:profile 依赖指向该包、`dsh.bundle.patch` 与入口模块按包目录相对解析且文件存在、层内包含 Skill 与 MCP 行。2026-10-08 在本机(macOS、桌面运行时 0.2.0-rc.2、发布包 0.5.0-beta.5)实测:注册后 Skill 出现在宿主目录,`mcp__bookmark-research__*` 工具正常应答,运行中的 MCP 子进程解析到已安装包目录。[D02][D08]
|
|
161
163
|
|
|
162
164
|
导出及查看合并配置的形式是:
|
|
163
165
|
|
|
@@ -166,7 +168,7 @@ python3 scripts/export_bundle.py --format dsh --output /private/tmp/bookmark-res
|
|
|
166
168
|
dsh --profile web --patch /private/tmp/bookmark-research-dsh/cordis.patch.yml --dump-config
|
|
167
169
|
```
|
|
168
170
|
|
|
169
|
-
实际启动可使用 `dsh web --patch
|
|
171
|
+
实际启动可使用 `dsh web --patch <绝对配置路径>`。该手工路线仍属待客户端验证的接入说明,且不覆盖保留的 `desktop` profile。新建任意名称的 profile 默认只有 base bundle;`web` profile 才会按官方模板包含 Web 表面,不应把“创建一个任意 profile”描述成自动创建了完整 Web 应用。[D02][D08]
|
|
170
172
|
|
|
171
173
|
`dsh plugin` 在 profile 目录中通过 pnpm 管理依赖,并把声明了 `dsh.bundle` 的包加入 `dsh.profile.bundles`。没有该声明的 npm 包仍能作为普通依赖安装,但不会自动增加插件配置层。后层 patch 会替换命中行的整个 `config`,不做深度合并。[D02][D08]
|
|
172
174
|
|
|
@@ -294,5 +294,36 @@
|
|
|
294
294
|
"Codex feature state and Claude manifest validation do not prove native tools or account access in an arbitrary session.",
|
|
295
295
|
"Local correlation IDs beginning local: are not native host run IDs."
|
|
296
296
|
]
|
|
297
|
+
},
|
|
298
|
+
"host_install_review": {
|
|
299
|
+
"checked_on": "2026-10-08",
|
|
300
|
+
"host": "dsh",
|
|
301
|
+
"profile": "desktop",
|
|
302
|
+
"runtime_version": "0.2.0-rc.2",
|
|
303
|
+
"package_version": "0.5.0-beta.5",
|
|
304
|
+
"methods": [
|
|
305
|
+
"scripts/install.py install --host dsh --profile desktop with the managed export from this source tree",
|
|
306
|
+
"dsh plugin --profile <throwaway> add <packed release tarball> (registry-equivalent spec)"
|
|
307
|
+
],
|
|
308
|
+
"observed": {
|
|
309
|
+
"bundle_selected_into_profile": true,
|
|
310
|
+
"skill_registered_in_host": true,
|
|
311
|
+
"mcp_tools_listed": 37,
|
|
312
|
+
"mcp_child_resolved_to_installed_package": true,
|
|
313
|
+
"composed_rows": [
|
|
314
|
+
"bookmark-research-skill",
|
|
315
|
+
"bookmark-research-mcp"
|
|
316
|
+
],
|
|
317
|
+
"desktop_profile_dump_refused": true,
|
|
318
|
+
"managed_receipt_state": "verified",
|
|
319
|
+
"managed_export_files": 71,
|
|
320
|
+
"unit_tests_passed": 506,
|
|
321
|
+
"temporary_test_profile_removed": true
|
|
322
|
+
},
|
|
323
|
+
"not_verified": [
|
|
324
|
+
"a published npm release that carries bundle.patch.yml; the released 0.5.0-beta.5 predates it",
|
|
325
|
+
"Windows or Pi installation",
|
|
326
|
+
"the npm cache permission retry on a failing machine (unit-tested only; this machine used npm_config_cache instead)"
|
|
327
|
+
]
|
|
297
328
|
}
|
|
298
329
|
}
|
package/docs/installation.en.md
CHANGED
|
@@ -181,7 +181,16 @@ python3 scripts/install.py install --host dsh --profile web
|
|
|
181
181
|
python3 scripts/install.py verify --host dsh --profile web
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
The installer exports a relocatable `dsh.bundle` and calls `dsh plugin --profile web add PATH`.
|
|
184
|
+
The installer exports a relocatable `dsh.bundle` and calls `dsh plugin --profile web add PATH`. A released package also installs straight from the registry: it declares `dsh.bundle.patch` and ships that layer, so `dsh plugin --profile web add bookmark-research@VERSION` — or the DSH plugin manager — needs no export first. The layer resolves the interpreter, the working directory and the MCP argument list from the installed module, so nothing is tied to the machine that built it; the selected profile must still provide the Skill registry and the official `@deepseek-ai/dsh-mcp-client`.
|
|
185
|
+
|
|
186
|
+
`dsh plugin` delegates to pnpm. A generic CLI therefore needs pnpm on PATH (`npm install -g pnpm` or `corepack enable pnpm`) and the installer stops before creating a profile without it; the DeepSeek Harness Desktop CLI carries its own pnpm and needs none. DeepSeek Harness Desktop reserves the `desktop` profile — its launcher refuses every boot and dump for that name — so install it with the app's own CLI, and verification reads the persisted layer instead of `--dump-config`:
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
python3 scripts/install.py install --host dsh --profile desktop \
|
|
190
|
+
--dsh "<DSH app>/Contents/Resources/runtime/cli/bin/dsh"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The legacy `cordis.patch.yml` still contains absolute paths and requires separate Skill discovery. Workflow execution separately requires the host service/engine; `hosts/dsh/workflow-call.py --research-id RID --run-key KEY` prepares a call without launching it.
|
|
185
194
|
|
|
186
195
|
### Agent Plugins 1.0.0
|
|
187
196
|
|
package/docs/installation.md
CHANGED
|
@@ -237,14 +237,23 @@ pi_subagent_workflow({action:"run",name:"bookmark-research",args:{research_id:"R
|
|
|
237
237
|
|
|
238
238
|
### DSH / DeepSeek Harness
|
|
239
239
|
|
|
240
|
-
所选 DSH profile 需提供 Skill registry 和官方 `@deepseek-ai/dsh-mcp-client
|
|
240
|
+
所选 DSH profile 需提供 Skill registry 和官方 `@deepseek-ai/dsh-mcp-client`。发布包自带 `bundle.patch.yml` 并声明 `dsh.bundle.patch`,因此除统一安装器之外,也可以直接从 registry 安装:`dsh plugin --profile <name> add bookmark-research@VERSION`,或在 DSH 插件管理界面按同一个 spec 安装;DSH 会把声明了 `dsh.bundle` 且 patch 存在的包自动加入 `dsh.profile.bundles`。层内不写死本机路径,Python 解释器、工作目录、MCP 参数与环境转发都由入口模块按安装位置解析。
|
|
241
|
+
|
|
242
|
+
统一安装器生成可搬移的 `dsh.bundle`,调用原生 `dsh plugin --profile web add PATH`,注册 Skill 并从安装后位置解析 Python/MCP 路径:
|
|
241
243
|
|
|
242
244
|
```sh
|
|
243
245
|
python3 scripts/install.py install --host dsh --profile web
|
|
244
246
|
python3 scripts/install.py verify --host dsh --profile web
|
|
245
247
|
```
|
|
246
248
|
|
|
247
|
-
|
|
249
|
+
`dsh plugin` 由 pnpm 执行:通用 CLI 需要 PATH 中的 pnpm(`npm install -g pnpm` 或 `corepack enable pnpm`),缺少时安装器在创建 profile 前停止;DSH 桌面版 CLI 自带 pnpm,无需另装。桌面版把 profile 名 `desktop` 保留给 Electron 载体,其启动器对该名字拒绝 boot/dump,因此用桌面版自带 CLI 安装,验证改为读取持久层而非 `--dump-config`:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
python3 scripts/install.py install --host dsh --profile desktop \
|
|
253
|
+
--dsh "<DSH 应用>/Contents/Resources/runtime/cli/bin/dsh"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
手动分发时使用 `export_bundle.py --format dsh`,再用上述原生命令安装稳定导出目录;不含该层的旧发布包或旧 Git ref 仍会按模块的旧契约生成等价层。保留的旧 `cordis.patch.yml` 仍含绝对路径,需单独配置 Skill 发现;移动后应重新生成。多组研究另需 workflow service、worker-thread engine 和 `workflow` tool。参见 [官方 MCP client 文档](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/mcp/mcp-client/README.md)。
|
|
248
257
|
|
|
249
258
|
生成可直接传给宿主 `workflow` 工具的完整 JSON:
|
|
250
259
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Bookmark Research 0.5.0 beta 6
|
|
2
|
+
|
|
3
|
+
**Beta / 测试版.** Uses the npm `beta` channel. The npm `latest` tag remains unchanged.
|
|
4
|
+
|
|
5
|
+
## Install this beta
|
|
6
|
+
|
|
7
|
+
Pin both the installer and the plugin content:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx bookmark-research@0.5.0-beta.6 install --source npm:bookmark-research@0.5.0-beta.6
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Choose Codex, Claude Code, Pi or DSH in the installer. Existing installations retain their recorded source and version selection. The GitHub Codex marketplace advances to this npm version after the registry confirms publication.
|
|
14
|
+
|
|
15
|
+
## Changes
|
|
16
|
+
|
|
17
|
+
- **DSH installs the release directly.** The package now declares `dsh.bundle.patch` and ships `bundle.patch.yml`, so `dsh plugin --profile <name> add bookmark-research@0.5.0-beta.6` — or the DSH plugin manager — installs it with no export step first, and DSH selects the bundle into `dsh.profile.bundles` on its own. Nothing in the layer is tied to the building machine: the entry module supplies the interpreter, working directory, MCP arguments and forwarded environment variables at load time.
|
|
18
|
+
- **No `python3` assumption in the DSH layer.** The module discovers Python 3.9+ with SQLite in the same order as the npm launcher (`BOOKMARK_RESEARCH_PYTHON`, `python3`, `python`, `py -3`) and reports a warning instead of failing silently when none is found, so Windows no longer depends on a `python3` alias.
|
|
19
|
+
- **DSH Desktop's reserved profile is installable.** DeepSeek Harness Desktop refuses to boot or dump the profile name `desktop`, so the installer verifies that profile from the persisted layer (dependency target, package-relative `dsh.bundle.patch`, entry module, Skill and MCP rows) instead of `--dump-config`.
|
|
20
|
+
- **A bundled package manager satisfies the DSH prerequisite.** A CLI that carries its own pnpm — the DeepSeek Harness Desktop CLI does — no longer requires `pnpm` on `PATH`; a generic CLI still fails before creating a profile, with instructions.
|
|
21
|
+
- **Unwritable npm caches are diagnosed and retried.** When npm reports a cache permission failure (for example root-owned files from a privileged install), the download retries once in a scratch cache and otherwise reports the `chown` repair command.
|
|
22
|
+
- **The exported DSH bundle is the released layer.** `export_bundle.py --format dsh` copies the committed `bundle.patch.yml` byte for byte; a source tree that predates the layer (an older published package or Git ref) still exports a working equivalent, so pinned installations do not regress.
|
|
23
|
+
|
|
24
|
+
## Validation and limits
|
|
25
|
+
|
|
26
|
+
The functional diff passed 506 discovered local tests, including new coverage for the released DSH layer, the offline verification of the reserved `desktop` profile, the bundled package manager, and the npm cache retry. A real installation into the DeepSeek Harness Desktop profile (`desktop`, runtime `0.2.0-rc.2`) registered the Skill, listed 37 MCP tools, and resolved the running MCP child to the installed package; a packed release installed in a throwaway profile was selected into `dsh.profile.bundles` automatically.
|
|
27
|
+
|
|
28
|
+
Not verified in this release: Windows and Pi installation, and the cache-retry path on a machine whose npm cache is genuinely unwritable. These are covered by unit tests only. No paid research model was started.
|
|
29
|
+
|
|
30
|
+
Install from [npm](https://www.npmjs.com/package/bookmark-research/v/0.5.0-beta.6). GitHub supplies source, version notes and installation catalogs; npm remains the shared release package for all four hosts.
|
package/hosts/dsh/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
|
|
@@ -6,6 +7,7 @@ export const inject = ["skills"];
|
|
|
6
7
|
|
|
7
8
|
// The module travels with the Python runtime; no exporting machine's path is embedded.
|
|
8
9
|
const root = fileURLToPath(new URL("../../", import.meta.url));
|
|
10
|
+
const cli = fileURLToPath(new URL("../../src/cli.py", import.meta.url));
|
|
9
11
|
const skillUrl = new URL("../../skills/bookmark-research/SKILL.md", import.meta.url);
|
|
10
12
|
const resourceBase = { kind: "directory", path: fileURLToPath(new URL("./", skillUrl)) };
|
|
11
13
|
const candidate = {
|
|
@@ -20,10 +22,56 @@ const candidate = {
|
|
|
20
22
|
locator: skillUrl,
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
// The MCP child inherits only these variables, so saved data/config paths and
|
|
26
|
+
// provider keys survive without baking values into a published patch. This list
|
|
27
|
+
// must equal the native manifest's mcpServers env_vars; tests assert that parity.
|
|
28
|
+
const FORWARDED_ENV = [
|
|
29
|
+
"BOOKMARK_RESEARCH_DATA_DIR",
|
|
30
|
+
"BOOKMARK_RESEARCH_CONFIG",
|
|
31
|
+
"BOOKMARK_RESEARCH_CREDENTIALS",
|
|
32
|
+
"XDG_DATA_HOME",
|
|
33
|
+
"XDG_CONFIG_HOME",
|
|
34
|
+
"EXA_API_KEY",
|
|
35
|
+
"PARALLEL_API_KEY",
|
|
36
|
+
"TAVILY_API_KEY",
|
|
37
|
+
"JINA_API_KEY",
|
|
38
|
+
"OPENAI_API_KEY",
|
|
39
|
+
"SystemRoot",
|
|
40
|
+
"windir",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// The same discovery order as the npm launcher: an explicit interpreter, then
|
|
44
|
+
// the platform aliases, each accepted only with Python 3.9+ and SQLite present.
|
|
45
|
+
const PYTHON_PROBE = "import sys, sqlite3; sys.exit(0 if sys.version_info >= (3, 9) else 3)";
|
|
46
|
+
|
|
47
|
+
function findPython() {
|
|
48
|
+
const candidates = [];
|
|
49
|
+
if (process.env.BOOKMARK_RESEARCH_PYTHON) candidates.push([process.env.BOOKMARK_RESEARCH_PYTHON]);
|
|
50
|
+
candidates.push(["python3"], ["python"]);
|
|
51
|
+
if (process.platform === "win32") candidates.push(["py", "-3"]);
|
|
52
|
+
for (const [command, ...args] of candidates) {
|
|
53
|
+
const probe = spawnSync(command, [...args, "-c", PYTHON_PROBE], { stdio: "ignore", windowsHide: true });
|
|
54
|
+
if (probe.status === 0) return { command, args };
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
export function apply(ctx) {
|
|
60
|
+
let python = findPython();
|
|
61
|
+
if (python === undefined) {
|
|
62
|
+
process.stderr.write("bookmark-research: no Python 3.9+ with SQLite was found; the MCP client will try python3. "
|
|
63
|
+
+ "Set BOOKMARK_RESEARCH_PYTHON to select an interpreter.\n");
|
|
64
|
+
python = { command: "python3", args: [] };
|
|
65
|
+
}
|
|
66
|
+
const env = Object.fromEntries(FORWARDED_ENV
|
|
67
|
+
.filter((key) => process.env[key] !== undefined)
|
|
68
|
+
.map((key) => [key, process.env[key]]));
|
|
24
69
|
ctx.provide("bookmarkResearchPaths", {
|
|
25
70
|
root,
|
|
26
|
-
cli
|
|
71
|
+
cli,
|
|
72
|
+
python,
|
|
73
|
+
mcpArgs: [...python.args, cli, "serve"],
|
|
74
|
+
env,
|
|
27
75
|
});
|
|
28
76
|
ctx.skills.registerProvider(() => ({
|
|
29
77
|
name,
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bookmark-research",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.6",
|
|
4
4
|
"description": "Check bookmark URLs or Canvas packages, compare sources with host-led research, and retain verifiable reports.",
|
|
5
5
|
"license": "GPL-3.0-only",
|
|
6
6
|
"repository": {"type": "git", "url": "git+https://github.com/Browser-bookmark-hub/Bookmark-Research.git"},
|
|
7
7
|
"keywords": ["bookmark-research", "mcp", "claude-code", "codex", "pi-package", "deepseek-harness"],
|
|
8
8
|
"bin": {"bookmark-research": "bin/bookmark-research.js"},
|
|
9
|
+
"main": "./hosts/dsh/plugin.js",
|
|
9
10
|
"engines": {"node": ">=18"},
|
|
10
|
-
"files": ["bin", "src", "scripts", "config", "skills", "hosts", "docs", ".codex-plugin/plugin.json", ".agents/plugins/marketplace.json", "install.sh", "LICENSE", "README.md", "README.zh.md", "!**/__pycache__", "!**/*.pyc"],
|
|
11
|
-
"pi": {"skills": ["./skills"]}
|
|
11
|
+
"files": ["bin", "src", "scripts", "config", "skills", "hosts", "docs", "bundle.patch.yml", ".codex-plugin/plugin.json", ".agents/plugins/marketplace.json", "install.sh", "LICENSE", "README.md", "README.zh.md", "!**/__pycache__", "!**/*.pyc"],
|
|
12
|
+
"pi": {"skills": ["./skills"]},
|
|
13
|
+
"dsh": {"bundle": {"patch": "./bundle.patch.yml"}}
|
|
12
14
|
}
|
package/scripts/build_zip.py
CHANGED
package/scripts/export_bundle.py
CHANGED
|
@@ -16,6 +16,8 @@ from host_assets import export_host_assets
|
|
|
16
16
|
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
|
17
17
|
FORMATS = ("codex", "agent-plugin", "claude", "pi", "dsh")
|
|
18
18
|
NAME = "bookmark-research"
|
|
19
|
+
# The DSH bundle layer a released tree ships and declares in `dsh.bundle.patch`.
|
|
20
|
+
BUNDLE_PATCH = "bundle.patch.yml"
|
|
19
21
|
SCHEMA_ROOT = "https://agent-plugins.org/schemas/1.0.0/"
|
|
20
22
|
SHARED_ROOTS = ("src", "config", "skills")
|
|
21
23
|
SHARED_DOCUMENTS = (
|
|
@@ -366,8 +368,34 @@ Reference: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/
|
|
|
366
368
|
return introduction + instructions
|
|
367
369
|
|
|
368
370
|
|
|
369
|
-
def
|
|
371
|
+
def _generated_bundle_patch(python, environment):
|
|
372
|
+
"""The bundle layer for a source tree that does not ship `bundle.patch.yml`.
|
|
373
|
+
|
|
374
|
+
Released trees carry the committed patch, which resolves the interpreter and
|
|
375
|
+
the forwarded environment from the module. An older published package or Git
|
|
376
|
+
ref only provides `root` and `cli`, so its generated layer must stay on that
|
|
377
|
+
narrower contract.
|
|
378
|
+
"""
|
|
379
|
+
return """- insert:
|
|
380
|
+
- id: bookmark-research-skill
|
|
381
|
+
name: bookmark-research
|
|
382
|
+
- id: bookmark-research-mcp
|
|
383
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
384
|
+
inject: [bookmarkResearchPaths]
|
|
385
|
+
config:
|
|
386
|
+
serverName: bookmark-research
|
|
387
|
+
transport: stdio
|
|
388
|
+
command: %s
|
|
389
|
+
args: !!js "[ctx.bookmarkResearchPaths.cli, 'serve']"
|
|
390
|
+
cwd: !!js ctx.bookmarkResearchPaths.root
|
|
391
|
+
env: !!js %s
|
|
392
|
+
failOnStartupError: true
|
|
393
|
+
""" % (json.dumps(python), environment)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _write_adapter(stage, format_name, output, manifest, python="python3", source_root=SOURCE_ROOT):
|
|
370
397
|
metadata = {"name": NAME, "version": manifest["version"], "description": manifest["description"]}
|
|
398
|
+
adapter = {}
|
|
371
399
|
if format_name == "codex":
|
|
372
400
|
if python != "python3":
|
|
373
401
|
# Codex registers this manifest verbatim; pin the detected interpreter.
|
|
@@ -408,27 +436,21 @@ def _write_adapter(stage, format_name, output, manifest, python="python3"):
|
|
|
408
436
|
_write_json(stage, "package.json", {
|
|
409
437
|
**metadata, "type": "module", "main": "./hosts/dsh/plugin.js",
|
|
410
438
|
"exports": "./hosts/dsh/plugin.js",
|
|
411
|
-
"files": ["src", "config", "skills", "hosts", "workflows", "docs",
|
|
412
|
-
"dsh": {"bundle": {"patch": "./
|
|
439
|
+
"files": ["src", "config", "skills", "hosts", "workflows", "docs", BUNDLE_PATCH, "LICENSE"],
|
|
440
|
+
"dsh": {"bundle": {"patch": "./" + BUNDLE_PATCH}},
|
|
413
441
|
})
|
|
414
442
|
forwarded = manifest["mcpServers"][NAME]["env_vars"]
|
|
415
443
|
environment = json.dumps("Object.fromEntries(" + json.dumps(forwarded) +
|
|
416
444
|
".filter(key => process.env[key] !== undefined).map(key => [key, process.env[key]]))")
|
|
417
|
-
(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
command: %s
|
|
427
|
-
args: !!js "[ctx.bookmarkResearchPaths.cli, 'serve']"
|
|
428
|
-
cwd: !!js ctx.bookmarkResearchPaths.root
|
|
429
|
-
env: !!js %s
|
|
430
|
-
failOnStartupError: true
|
|
431
|
-
""" % (json.dumps(python), environment), encoding="utf-8")
|
|
445
|
+
committed = Path(source_root) / BUNDLE_PATCH
|
|
446
|
+
if not committed.is_symlink() and committed.is_file():
|
|
447
|
+
# The released tree ships the layer; copying it keeps the npm package
|
|
448
|
+
# and the exported bundle on byte-identical patch content.
|
|
449
|
+
(stage / BUNDLE_PATCH).write_bytes(committed.read_bytes())
|
|
450
|
+
adapter["bundle_patch"] = "committed"
|
|
451
|
+
else:
|
|
452
|
+
(stage / BUNDLE_PATCH).write_text(_generated_bundle_patch(python, environment), encoding="utf-8")
|
|
453
|
+
adapter["bundle_patch"] = "generated"
|
|
432
454
|
# JSON strings are valid YAML scalars, including paths with spaces/quotes.
|
|
433
455
|
cli_path = json.dumps(str(output / "src/cli.py"), ensure_ascii=False)
|
|
434
456
|
patch = """- insert:
|
|
@@ -446,6 +468,7 @@ def _write_adapter(stage, format_name, output, manifest, python="python3"):
|
|
|
446
468
|
""" % (json.dumps(python), cli_path, environment)
|
|
447
469
|
(stage / "cordis.patch.yml").write_text(patch, encoding="utf-8")
|
|
448
470
|
(stage / "README.md").write_text(_readme(format_name, output), encoding="utf-8")
|
|
471
|
+
return adapter
|
|
449
472
|
|
|
450
473
|
|
|
451
474
|
def export_bundle(format_name, output, source_root=SOURCE_ROOT, python="python3"):
|
|
@@ -473,13 +496,13 @@ def export_bundle(format_name, output, source_root=SOURCE_ROOT, python="python3"
|
|
|
473
496
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
474
497
|
shutil.copy2(source_root / relative, destination)
|
|
475
498
|
host_files = export_host_assets(source_root, stage, format_name)
|
|
476
|
-
_write_adapter(stage, format_name, output, manifest, python)
|
|
499
|
+
adapter = _write_adapter(stage, format_name, output, manifest, python, source_root=source_root)
|
|
477
500
|
_check_destination(output)
|
|
478
501
|
if output.exists():
|
|
479
502
|
output.rmdir() # Only an empty directory can be removed here.
|
|
480
503
|
stage.rename(output) # Renaming a directory cannot replace nonempty data.
|
|
481
504
|
return {"format": format_name, "version": manifest["version"], "output": str(output), "shared_files": len(files),
|
|
482
|
-
"host_files": host_files, "installed": False}
|
|
505
|
+
"host_files": host_files, "adapter": adapter, "installed": False}
|
|
483
506
|
|
|
484
507
|
|
|
485
508
|
def main(argv=None):
|
package/scripts/host_assets.py
CHANGED
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
HOST_SOURCE_FILES = (
|
|
8
8
|
"hosts/shared/workflow.json", "hosts/shared/research-flow.js", "hosts/shared/research-call.py",
|
|
9
9
|
"hosts/claude/runtime.js", "hosts/dsh/runtime.js", "hosts/dsh/workflow-call.py", "hosts/dsh/plugin.js",
|
|
10
|
+
"hosts/dsh/package.json",
|
|
10
11
|
"hosts/pi/runtime.js", "hosts/pi/register-workflow.py",
|
|
11
12
|
"hosts/codex/delegate.md", "hosts/codex/prepare.py",
|
|
12
13
|
)
|
|
@@ -59,6 +60,9 @@ def export_host_assets(source, stage, host):
|
|
|
59
60
|
relative = "hosts/pi/register-workflow.py"
|
|
60
61
|
else:
|
|
61
62
|
files["hosts/dsh/plugin.js"] = read_asset(source, "hosts/dsh/plugin.js")
|
|
63
|
+
# Scope ESM to the entry module so a package root may stay CommonJS
|
|
64
|
+
# (the npm package keeps a CommonJS bin and the pi field).
|
|
65
|
+
files["hosts/dsh/package.json"] = read_asset(source, "hosts/dsh/package.json")
|
|
62
66
|
files["workflows/bookmark-research/meta.json"] = (
|
|
63
67
|
json.dumps(definition["meta"], ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
64
68
|
relative = "hosts/dsh/workflow-call.py"
|
package/scripts/host_clients.py
CHANGED
|
@@ -23,6 +23,28 @@ def executable(binary):
|
|
|
23
23
|
return found
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def bundled_pnpm(binary):
|
|
27
|
+
"""The package manager a DSH CLI carries itself, if it ships one.
|
|
28
|
+
|
|
29
|
+
DeepSeek Harness Desktop runs `dsh plugin` with its own bundled pnpm, so that
|
|
30
|
+
carrier needs no `pnpm` on PATH. A generic CLI installs profile plugins by
|
|
31
|
+
forwarding to the pnpm it finds instead.
|
|
32
|
+
"""
|
|
33
|
+
found = executable(binary)
|
|
34
|
+
if not found:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
path = Path(found).resolve()
|
|
38
|
+
except OSError:
|
|
39
|
+
return None
|
|
40
|
+
# Desktop layout: <resources>/runtime/cli/bin/dsh beside <resources>/runtime/pnpm.
|
|
41
|
+
for parent in list(path.parents)[:5]:
|
|
42
|
+
candidate = parent / "pnpm" / "bin" / "pnpm.mjs"
|
|
43
|
+
if candidate.is_file():
|
|
44
|
+
return candidate
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
26
48
|
def read_json(path, default=None):
|
|
27
49
|
if not path.exists():
|
|
28
50
|
return default
|
|
@@ -81,10 +103,11 @@ class HostClient:
|
|
|
81
103
|
def require(self):
|
|
82
104
|
if not executable(self.binary):
|
|
83
105
|
raise ValueError(self.host + " CLI was not found: " + self.binary)
|
|
84
|
-
if self.host == "dsh" and not shutil.which("pnpm"):
|
|
85
|
-
# dsh plugin
|
|
86
|
-
raise ValueError("DSH installs profile plugins with pnpm, which was not found on PATH
|
|
87
|
-
"install it (npm install -g pnpm, or corepack enable pnpm) and retry"
|
|
106
|
+
if self.host == "dsh" and not shutil.which("pnpm") and bundled_pnpm(self.binary) is None:
|
|
107
|
+
# A generic dsh forwards `plugin` to pnpm; fail before creating a profile.
|
|
108
|
+
raise ValueError("DSH installs profile plugins with pnpm, which was not found on PATH and is not bundled with "
|
|
109
|
+
+ self.binary + "; install it (npm install -g pnpm, or corepack enable pnpm) and retry, "
|
|
110
|
+
"or pass the DeepSeek Harness Desktop CLI with --dsh (it carries its own pnpm)")
|
|
88
111
|
|
|
89
112
|
def run(self, arguments, structured=False, quiet_error=False):
|
|
90
113
|
result = subprocess.run([executable(self.binary) or self.binary, *arguments], cwd=self.cwd, text=True, encoding="utf-8", errors="replace",
|
|
@@ -186,11 +209,50 @@ class HostClient:
|
|
|
186
209
|
if self.host == "dsh":
|
|
187
210
|
if not registration["enabled"]:
|
|
188
211
|
raise RuntimeError("DSH installed the dependency without enabling its bundle")
|
|
189
|
-
composition = self.run(["--profile", self.profile, "--dump-config"], quiet_error=True)
|
|
190
|
-
if not all(name in composition for name in ("bookmark-research-skill", "bookmark-research-mcp", "bookmarkResearchPaths")):
|
|
191
|
-
raise RuntimeError("DSH composition is missing the Bookmark Research Skill or MCP configuration")
|
|
192
212
|
package = self.settings.parent / "node_modules" / NAME
|
|
193
213
|
if not package.is_dir():
|
|
194
214
|
raise RuntimeError("DSH did not retain the installed bundle in its profile")
|
|
215
|
+
if self.profile.casefold() == "desktop":
|
|
216
|
+
# DeepSeek Harness Desktop reserves this profile name: its launcher
|
|
217
|
+
# refuses every boot and dump for "desktop" (only the Electron carrier
|
|
218
|
+
# may manage it), so read the persisted layer the way the launcher does.
|
|
219
|
+
composition = self.desktop_composition(package)
|
|
220
|
+
else:
|
|
221
|
+
composition = self.run(["--profile", self.profile, "--dump-config"], quiet_error=True)
|
|
222
|
+
if not all(name in composition for name in ("bookmark-research-skill", "bookmark-research-mcp", "bookmarkResearchPaths")):
|
|
223
|
+
raise RuntimeError("DSH composition is missing the Bookmark Research Skill or MCP configuration")
|
|
195
224
|
return package.resolve()
|
|
196
225
|
return bundle
|
|
226
|
+
|
|
227
|
+
def desktop_composition(self, package):
|
|
228
|
+
"""Offline evidence that the reserved desktop profile composes this bundle.
|
|
229
|
+
|
|
230
|
+
The installed layer must be exactly what the launcher loads: the profile
|
|
231
|
+
depends on the package, and the package declares a package-relative patch,
|
|
232
|
+
an entry module, and the Skill and MCP rows that patch inserts.
|
|
233
|
+
"""
|
|
234
|
+
settings = read_json(self.settings, {})
|
|
235
|
+
if not isinstance(settings, dict):
|
|
236
|
+
raise ValueError("Unexpected DSH profile manifest: " + str(self.settings))
|
|
237
|
+
dependency = settings.get("dependencies", {}).get(NAME)
|
|
238
|
+
if not isinstance(dependency, str) or not dependency.strip():
|
|
239
|
+
raise RuntimeError("The desktop profile does not depend on " + NAME)
|
|
240
|
+
linked = local_path(dependency, self.settings.parent)
|
|
241
|
+
if linked is not None and linked != package.resolve():
|
|
242
|
+
raise RuntimeError("The desktop profile depends on another " + NAME + " source")
|
|
243
|
+
manifest = read_json(package / "package.json", {})
|
|
244
|
+
if not isinstance(manifest, dict) or manifest.get("name") != NAME:
|
|
245
|
+
raise RuntimeError("The installed bundle is not " + NAME)
|
|
246
|
+
declared = manifest.get("dsh", {})
|
|
247
|
+
patch = declared.get("bundle", {}).get("patch") if isinstance(declared, dict) else None
|
|
248
|
+
if not isinstance(patch, str) or not patch or Path(patch).is_absolute():
|
|
249
|
+
raise RuntimeError("The installed bundle declares no package-relative dsh.bundle.patch")
|
|
250
|
+
entry = manifest.get("main")
|
|
251
|
+
if not isinstance(entry, str) or not entry or Path(entry).is_absolute():
|
|
252
|
+
raise RuntimeError("The installed bundle declares no package-relative entry module")
|
|
253
|
+
for relative in (patch, entry):
|
|
254
|
+
resolved = (package / relative).resolve()
|
|
255
|
+
if package.resolve() not in resolved.parents or not resolved.is_file():
|
|
256
|
+
raise RuntimeError("The installed bundle is missing " + relative)
|
|
257
|
+
# The markers below are the same names the composed dump would contain.
|
|
258
|
+
return entry + "\n" + (package / patch).read_text(encoding="utf-8")
|
package/scripts/host_install.py
CHANGED
|
@@ -201,7 +201,7 @@ def _manage(action, client, base, source, ref, dry_run):
|
|
|
201
201
|
export_bundle(client.host, stage, root, python)
|
|
202
202
|
manifest = read_plugin_manifest(root)
|
|
203
203
|
# Render path-dependent manual instructions for the persistent location.
|
|
204
|
-
_write_adapter(stage, client.host, bundle, manifest, python)
|
|
204
|
+
_write_adapter(stage, client.host, bundle, manifest, python, source_root=root)
|
|
205
205
|
content = _files(stage)
|
|
206
206
|
digest = hashlib.sha256(json.dumps(content, sort_keys=True).encode()).hexdigest()
|
|
207
207
|
version = manifest["version"]
|
package/scripts/npm_source.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path, PurePosixPath
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
7
8
|
import subprocess
|
|
8
9
|
import tarfile
|
|
@@ -13,6 +14,16 @@ from export_bundle import NAME, VERSION_PATTERN, read_plugin_manifest
|
|
|
13
14
|
|
|
14
15
|
REGISTRY = "https://registry.npmjs.org"
|
|
15
16
|
DEFAULT_SOURCE = "npm:" + NAME
|
|
17
|
+
CACHE_PERMISSION = re.compile(r"EACCES|EPERM")
|
|
18
|
+
CACHE_HINT = ("Hint: npm's cache may contain files owned by another user (a known npm state after a "
|
|
19
|
+
"privileged install); repair it with `sudo chown -R $(id -u):$(id -g) \"$(npm config get cache)\"` "
|
|
20
|
+
"or point npm_config_cache at a writable directory.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _cache_permission_failure(detail):
|
|
24
|
+
"""Whether npm failed because its cache directory is not writable."""
|
|
25
|
+
text = detail or ""
|
|
26
|
+
return "cache" in text.casefold() and CACHE_PERMISSION.search(text) is not None
|
|
16
27
|
|
|
17
28
|
|
|
18
29
|
def source(value, ref=None):
|
|
@@ -35,10 +46,28 @@ def checkout(requested, timeout):
|
|
|
35
46
|
raise ValueError("npm is required to download Bookmark Research releases")
|
|
36
47
|
with tempfile.TemporaryDirectory(prefix="bookmark-npm-source-") as directory:
|
|
37
48
|
base = Path(directory)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
|
|
50
|
+
def pack(cache=None):
|
|
51
|
+
command = [
|
|
52
|
+
npm, "pack", NAME + "@" + selected["ref"], "--ignore-scripts", "--json",
|
|
53
|
+
"--registry", REGISTRY, "--pack-destination", str(base),
|
|
54
|
+
]
|
|
55
|
+
if cache is not None:
|
|
56
|
+
command += ["--cache", str(cache)]
|
|
57
|
+
return subprocess.run(command, cwd=base, capture_output=True, text=True,
|
|
58
|
+
encoding="utf-8", errors="replace", timeout=timeout)
|
|
59
|
+
|
|
60
|
+
result = pack()
|
|
61
|
+
if result.returncode and _cache_permission_failure(result.stderr):
|
|
62
|
+
# One retry in a scratch cache: a machine whose npm cache is unwritable
|
|
63
|
+
# otherwise cannot install any release, and the failure is not the release's.
|
|
64
|
+
with tempfile.TemporaryDirectory(prefix="bookmark-npm-cache-") as cache:
|
|
65
|
+
retry = pack(cache)
|
|
66
|
+
if retry.returncode == 0:
|
|
67
|
+
result = retry
|
|
68
|
+
else:
|
|
69
|
+
raise RuntimeError("Could not download the npm release: "
|
|
70
|
+
+ result.stderr.strip()[-2000:] + "\n" + CACHE_HINT)
|
|
42
71
|
if result.returncode:
|
|
43
72
|
raise RuntimeError("Could not download the npm release: " + result.stderr.strip()[-2000:])
|
|
44
73
|
rows = json.loads(result.stdout)
|