@xiaoso/dsh-tool-plus 0.1.14 → 0.1.16-beta.0
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/README.en.md +164 -130
- package/README.md +36 -3
- package/lib/client.js +1 -1
- package/lib/index.mjs +5 -5
- package/lib/pi-natives-bootstrap.mjs +34 -0
- package/lib/{scrapers-DSZkQist.mjs → scrapers-C6oBIusN.mjs} +1 -1
- package/lib/{utils-CHnQw1zZ.mjs → utils-eJcxzVy7.mjs} +1 -1
- package/package.json +3 -3
- package/scripts/patch-pi-natives.mjs +241 -108
package/README.en.md
CHANGED
|
@@ -1,130 +1,164 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img width="160" src="assets/icon.png" alt="dsh tool plus icon"><br>
|
|
3
|
-
<img width="240" src="assets/wordmark.png" alt="dsh tool plus">
|
|
4
|
-
</div>
|
|
5
|
-
|
|
6
|
-
<div align="center">
|
|
7
|
-
|
|
8
|
-
[简体中文](./README.md) | English
|
|
9
|
-
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
<div align="center">
|
|
13
|
-
|
|
14
|
-
[](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
|
|
15
|
-
[](LICENSE)
|
|
16
|
-
[](https://nodejs.org)
|
|
17
|
-
[]()
|
|
18
|
-
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
Essential tools enhancement for DeepSeek Harness: persistent bash, structured read, multi-mode edit, atomic write, full-text search, and direct image reading — one plugin covers it all. Ported from the [Oh My Pi](https://github.com/can1357/oh-my-pi) core; once installed it automatically takes over the official bash / pwsh / file / search tools, with optional `ast_grep` / `ast_edit` structural search and rewrite.
|
|
22
|
-
|
|
23
|
-
## Features
|
|
24
|
-
|
|
25
|
-
- **bash**: persistent shell; `cd` and `export` keep state across calls; verbose logs (git/npm/cargo…) are condensed automatically; overlong output keeps only head and tail, with the full content written to disk for later retrieval; long-running commands are moved to the background automatically; optionally intercepts `cat`, `grep`, `find`, and `sed -i` to steer you toward the dedicated tools
|
|
26
|
-
- **Safe delete**: `rm` moves files to the system trash by default instead of deleting permanently, so accidental deletes are recoverable; use `command rm` for a real delete
|
|
27
|
-
- **read**: precise line-range reading (supports `:N-M`, `:raw`, and multiple ranges); large code files return a structural summary by default, with details expanded on demand; zip/tar archives, SQLite, notebooks, and PDFs read directly; PNG / JPEG / WebP / GIF images read directly, oversized ones scaled down automatically; can fetch web content (including images embedded in the page); SPA sites (content rendered dynamically by JS, e.g. excalidraw.com) are automatically fetched via local browser rendering
|
|
28
|
-
- **write**: atomic writes that return a diff of the changes; supports patch-style writing, and can write directly into zip/tar archive members and SQLite data
|
|
29
|
-
- **edit**: `replace` by default, with patch / hashline / apply-patch formats also supported; multi-hunk edits, uniqueness validation, fuzzy matching tolerant of whitespace differences
|
|
30
|
-
- **grep / glob**: full-text search and filename matching; mtime sorting, context lines, and configurable ignore rules
|
|
31
|
-
- **ast_grep / ast_edit** (optional): syntax-tree based structural code search and rewrite, enabled in settings
|
|
32
|
-
- **agent presets**: two companion templates — Standard enhanced and PTC (Code Mode)
|
|
33
|
-
|
|
34
|
-
Settings panel (Bash tab):
|
|
35
|
-
|
|
36
|
-

|
|
37
|
-
|
|
38
|
-
## Recommended Environment & Configuration
|
|
39
|
-
|
|
40
|
-
- **Full-access mode**: the plugin is best used under `danger-full-access`, which avoids file writes being blocked by mistake in sandboxed modes.
|
|
41
|
-
|
|
42
|
-
## Installation
|
|
43
|
-
|
|
44
|
-
### Install from npm (recommended)
|
|
45
|
-
|
|
46
|
-
#### Web
|
|
47
|
-
|
|
48
|
-
```sh
|
|
49
|
-
dsh plugin --profile web add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`--allow-build` lets install scripts run (once for this package, once for `koffi`); it requires pnpm ≥ 10.4, and on pnpm 11+ omitting it fails the install with `ERR_PNPM_IGNORED_BUILDS`. If your pnpm is too old for the flag, allow the scripts first and re-run:
|
|
53
|
-
|
|
54
|
-
- Append to `~/.dsh/profiles/web/pnpm-workspace.yaml`: `allowBuilds:`, ` '@xiaoso/dsh-tool-plus': true` and ` koffi: true`
|
|
55
|
-
- Or run `cd ~/.dsh/profiles/web && pnpm approve-builds --all` (pnpm ≥ 10.32)
|
|
56
|
-
|
|
57
|
-
#### Desktop
|
|
58
|
-
|
|
59
|
-
The profile is owned by the desktop app, so `dsh plugin --profile desktop` is refused; install inside the profile directory instead:
|
|
60
|
-
|
|
61
|
-
```sh
|
|
62
|
-
cd ~/.dsh/profiles/desktop
|
|
63
|
-
pnpm add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Local development
|
|
67
|
-
|
|
68
|
-
```sh
|
|
69
|
-
dsh plugin --profile web add link:<path to this repo>
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### Presets
|
|
73
|
-
|
|
74
|
-
| Preset | Base | What it is |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| **Tool Plus 标准增强版** | official standard | everything in standard mode, with the file/shell toolset swapped for this plugin |
|
|
77
|
-
| **Tool Plus PTC 增强版** | official PTC (Code Mode) | same, but composing multi-step operations through Code Mode's `run_code` |
|
|
78
|
-
|
|
79
|
-
Both presets are **declared by the plugin**: installing it (as a profile bundle) brings two `@deepseek-ai/dsh-agent-preset` declaration rows along in its patch layer, so they appear in the preset list immediately. There is no install step, and the plugin writes nothing at startup.
|
|
80
|
-
|
|
81
|
-
**Tool Plus → Presets** in the settings page is where you change them. Since dsh 0.1.7 the official preset page is read-only and the `agent_preset` tool is gone, so this panel is the only graphical entry point:
|
|
82
|
-
|
|
83
|
-
- **Minimal update** — disables just the official tool rows still mounted, touching nothing else
|
|
84
|
-
- **Align to template** — replaces this row's whole plugin list with the selected template
|
|
85
|
-
- **Revert to bundled** — removes the override you wrote in the profile configuration (`<profile>/cordis.patch.yml`), falling back to the bundled declaration
|
|
86
|
-
- **View differences** — shows which entries differ before you touch anything
|
|
87
|
-
|
|
88
|
-
Writes go through the harness's own `ctx.configEditor`, so they inherit the profile lock, concurrency protection, atomic write and rollback.
|
|
89
|
-
|
|
90
|
-
> Upgrading from 0.1.9 or earlier: `~/.dsh/.agent-presets/` is **read by nothing** any more (0.1.7 moved presets into the patch layer), so it is safe to delete; the panel tells you when it finds one.
|
|
91
|
-
|
|
92
|
-
## Configuration
|
|
93
|
-
|
|
94
|
-
Works out of the box, no configuration needed. Common tweaks: background threshold (`autoBackgroundMs`), timeouts (`defaultTimeoutMs` / `maxTimeoutMs`), the output truncation window, the default edit mode (`editMode`, default `replace`), and the structured summary toggle (`readSummarizeEnabled`).
|
|
95
|
-
|
|
96
|
-
##
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
| |
|
|
110
|
-
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img width="160" src="assets/icon.png" alt="dsh tool plus icon"><br>
|
|
3
|
+
<img width="240" src="assets/wordmark.png" alt="dsh tool plus">
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div align="center">
|
|
7
|
+
|
|
8
|
+
[简体中文](./README.md) | English
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
[](https://www.npmjs.com/package/@xiaoso/dsh-tool-plus)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
[](https://nodejs.org)
|
|
17
|
+
[]()
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
Essential tools enhancement for DeepSeek Harness: persistent bash, structured read, multi-mode edit, atomic write, full-text search, and direct image reading — one plugin covers it all. Ported from the [Oh My Pi](https://github.com/can1357/oh-my-pi) core; once installed it automatically takes over the official bash / pwsh / file / search tools, with optional `ast_grep` / `ast_edit` structural search and rewrite.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **bash**: persistent shell; `cd` and `export` keep state across calls; verbose logs (git/npm/cargo…) are condensed automatically; overlong output keeps only head and tail, with the full content written to disk for later retrieval; long-running commands are moved to the background automatically; optionally intercepts `cat`, `grep`, `find`, and `sed -i` to steer you toward the dedicated tools (off by default)
|
|
26
|
+
- **Safe delete**: `rm` moves files to the system trash by default instead of deleting permanently, so accidental deletes are recoverable; use `command rm` for a real delete
|
|
27
|
+
- **read**: precise line-range reading (supports `:N-M`, `:raw`, and multiple ranges); large code files return a structural summary by default, with details expanded on demand; zip/tar archives, SQLite, notebooks, and PDFs read directly; PNG / JPEG / WebP / GIF images read directly, oversized ones scaled down automatically; can fetch web content (including images embedded in the page); SPA sites (content rendered dynamically by JS, e.g. excalidraw.com) are automatically fetched via local browser rendering
|
|
28
|
+
- **write**: atomic writes that return a diff of the changes; supports patch-style writing, and can write directly into zip/tar archive members and SQLite data
|
|
29
|
+
- **edit**: `replace` by default, with patch / hashline / apply-patch formats also supported; multi-hunk edits, uniqueness validation, fuzzy matching tolerant of whitespace differences
|
|
30
|
+
- **grep / glob**: full-text search and filename matching; mtime sorting, context lines, and configurable ignore rules
|
|
31
|
+
- **ast_grep / ast_edit** (optional): syntax-tree based structural code search and rewrite, enabled in settings
|
|
32
|
+
- **agent presets**: two companion templates — Standard enhanced and PTC (Code Mode)
|
|
33
|
+
|
|
34
|
+
Settings panel (Bash tab):
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
## Recommended Environment & Configuration
|
|
39
|
+
|
|
40
|
+
- **Full-access mode**: the plugin is best used under `danger-full-access`, which avoids file writes being blocked by mistake in sandboxed modes.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Install from npm (recommended)
|
|
45
|
+
|
|
46
|
+
#### Web
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh plugin --profile web add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`--allow-build` lets install scripts run (once for this package, once for `koffi`); it requires pnpm ≥ 10.4, and on pnpm 11+ omitting it fails the install with `ERR_PNPM_IGNORED_BUILDS`. If your pnpm is too old for the flag, allow the scripts first and re-run:
|
|
53
|
+
|
|
54
|
+
- Append to `~/.dsh/profiles/web/pnpm-workspace.yaml`: `allowBuilds:`, ` '@xiaoso/dsh-tool-plus': true` and ` koffi: true`
|
|
55
|
+
- Or run `cd ~/.dsh/profiles/web && pnpm approve-builds --all` (pnpm ≥ 10.32)
|
|
56
|
+
|
|
57
|
+
#### Desktop
|
|
58
|
+
|
|
59
|
+
The profile is owned by the desktop app, so `dsh plugin --profile desktop` is refused; install inside the profile directory instead:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
cd ~/.dsh/profiles/desktop
|
|
63
|
+
pnpm add --allow-build=@xiaoso/dsh-tool-plus --allow-build=koffi @xiaoso/dsh-tool-plus
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Local development
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
dsh plugin --profile web add link:<path to this repo>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Presets
|
|
73
|
+
|
|
74
|
+
| Preset | Base | What it is |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| **Tool Plus 标准增强版** | official standard | everything in standard mode, with the file/shell toolset swapped for this plugin |
|
|
77
|
+
| **Tool Plus PTC 增强版** | official PTC (Code Mode) | same, but composing multi-step operations through Code Mode's `run_code` |
|
|
78
|
+
|
|
79
|
+
Both presets are **declared by the plugin**: installing it (as a profile bundle) brings two `@deepseek-ai/dsh-agent-preset` declaration rows along in its patch layer, so they appear in the preset list immediately. There is no install step, and the plugin writes nothing at startup.
|
|
80
|
+
|
|
81
|
+
**Tool Plus → Presets** in the settings page is where you change them. Since dsh 0.1.7 the official preset page is read-only and the `agent_preset` tool is gone, so this panel is the only graphical entry point:
|
|
82
|
+
|
|
83
|
+
- **Minimal update** — disables just the official tool rows still mounted, touching nothing else
|
|
84
|
+
- **Align to template** — replaces this row's whole plugin list with the selected template
|
|
85
|
+
- **Revert to bundled** — removes the override you wrote in the profile configuration (`<profile>/cordis.patch.yml`), falling back to the bundled declaration
|
|
86
|
+
- **View differences** — shows which entries differ before you touch anything
|
|
87
|
+
|
|
88
|
+
Writes go through the harness's own `ctx.configEditor`, so they inherit the profile lock, concurrency protection, atomic write and rollback.
|
|
89
|
+
|
|
90
|
+
> Upgrading from 0.1.9 or earlier: `~/.dsh/.agent-presets/` is **read by nothing** any more (0.1.7 moved presets into the patch layer), so it is safe to delete; the panel tells you when it finds one.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
Works out of the box, no configuration needed. Common tweaks: background threshold (`autoBackgroundMs`), timeouts (`defaultTimeoutMs` / `maxTimeoutMs`), the output truncation window, the default edit mode (`editMode`, default `replace`), and the structured summary toggle (`readSummarizeEnabled`).
|
|
95
|
+
|
|
96
|
+
## Performance
|
|
97
|
+
|
|
98
|
+
On Linux, comparing the official standard mode with this plugin's Standard enhanced preset (SWE-bench Multilingual, 20 tasks × 3 rounds):
|
|
99
|
+
|
|
100
|
+
| Metric | Official tool surface | This plugin (command interception on) | This plugin (command interception off, default) |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| Resolve rate | 83.3% | **83.3%** | 81.7% (1 task lower, within normal variance) |
|
|
103
|
+
| Steps / task | 24.4 | 29.7 (+21.7%) | 25.3 (+3.8%) |
|
|
104
|
+
| Total tokens / task | **729,434** | 954,100 (+30.8%) | 779,799 (+6.9%) |
|
|
105
|
+
| System prompt / step (incl. tool definitions) | **4,642** | 6,305 (+35.8%) | 6,305 (+35.8%) |
|
|
106
|
+
| System prompt total / task (per step × steps) | **113,265** | 187,259 (+65.3%) | 159,622 (+40.9%) |
|
|
107
|
+
| Net tokens / task (excl. system prompt) | **616,169** | 766,841 (+24.5%) | 620,177 (+0.7%) |
|
|
108
|
+
| Average context length at task end (tokens) | **42,817** | 48,645 (+13.6%) | 44,781 (+4.6%) |
|
|
109
|
+
| `read` calls | 225 | 459 | **198** |
|
|
110
|
+
| `bash` time per call | 3,140 ms | **2,391 ms** | 2,045 ms |
|
|
111
|
+
| `bash` tokens returned per call | 790 tok | **343 tok** | 638 tok |
|
|
112
|
+
| Average time / task (excludes token-rate and network-speed fluctuation) | **123 s** | 135 s | 151 s |
|
|
113
|
+
|
|
114
|
+
Notes:
|
|
115
|
+
|
|
116
|
+
1. The average time is an end-to-end measurement that does not account for token-rate or network-speed fluctuation; use the absolute values for horizontal comparison only.
|
|
117
|
+
2. The figures above describe this benchmark only (its tasks use little context, so they do not represent large tasks).
|
|
118
|
+
|
|
119
|
+
Conclusions:
|
|
120
|
+
|
|
121
|
+
1. The resolve rate matches the official tools (the interception-off group is 1 task lower, within normal variance).
|
|
122
|
+
2. `bash` itself is leaner, and the tokens it saves roughly offset the extra system prompt from thicker tool definitions (the plugin adds capabilities, so its definitions are thicker than the official preset's).
|
|
123
|
+
3. `bash` is faster, but the gain is cancelled out by side effects from `bash` itself and the other enhanced tools.
|
|
124
|
+
4. On Linux the combined performance is on par with the official standard-mode tools, with a richer feature set.
|
|
125
|
+
|
|
126
|
+
So on Linux this plugin cannot promise lower token usage or better performance; Windows is not suitable for running the standard benchmark, so the only claim there is a **personal impression** that it is more pleasant to use than PowerShell.
|
|
127
|
+
|
|
128
|
+
Full data and the per-task matrix: [docs/benchmark/sbml-compare-ops.md](docs/benchmark/sbml-compare-ops.md) (Chinese).
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- **dsh CLI**: installed globally, `npm i -g @deepseek-ai/dsh`
|
|
133
|
+
- **Node.js** ≥ 22.19 or ≥ 24
|
|
134
|
+
- **Git Bash** (recommended): serves as the bash execution environment on Windows
|
|
135
|
+
- Targets DeepSeek Harness `dsh` v0.2.0-rc.2 (pre-release; interfaces may change)
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
138
|
+
|
|
139
|
+
- No standalone `pwsh` tool: shell work is handled by the more capable persistent `bash`
|
|
140
|
+
|
|
141
|
+
**Safe rm sends deleted files to the system trash.** Where they land, and whether you can see them:
|
|
142
|
+
|
|
143
|
+
| | Windows | macOS | Linux |
|
|
144
|
+
|---|---|---|---|
|
|
145
|
+
| **Location** | Recycle Bin | Trash `~/.Trash` | `$XDG_DATA_HOME/Trash`; defaults to `~/.local/share/Trash` |
|
|
146
|
+
| **Another volume / external disk** | That volume's Recycle Bin | That volume's `.Trashes` | `.Trash-<uid>` at that mount point |
|
|
147
|
+
| **Visible in the OS trash** | Yes | Yes | Same volume: yes; another volume: open that location |
|
|
148
|
+
| **File name** | Unchanged | Unchanged | A UUID; the original name comes from `.trashinfo` |
|
|
149
|
+
| **Put back** | Yes | Yes | Depends on the file manager |
|
|
150
|
+
| **Implementation** | Shell API | System API | Third-party XDG implementation |
|
|
151
|
+
| **Known limits** | No Recycle Bin on network drives | Requires macOS 10.12+ | Some desktops do not list it — restore from the location above |
|
|
152
|
+
|
|
153
|
+
## Build
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
pnpm install
|
|
157
|
+
pnpm build # type declarations (tsc) + tsdown bundle + asset copy
|
|
158
|
+
pnpm typecheck
|
|
159
|
+
pnpm test # includes real-bash cases; needs Git Bash on Windows, auto-skips when missing
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
[MIT](LICENSE); third-party component licenses in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ DeepSeek Harness 基础工具增强:持久 bash、结构化 read、多模式 e
|
|
|
22
22
|
|
|
23
23
|
## 功能特性
|
|
24
24
|
|
|
25
|
-
- **bash**:持久 shell,`cd`、`export` 跨调用保持状态;冗长日志(git/npm/cargo…)自动压缩;超长输出只保留头尾,完整内容落盘可回读;长命令自动转后台;可选拦截 `cat`、`grep`、`find`、`sed -i
|
|
25
|
+
- **bash**:持久 shell,`cd`、`export` 跨调用保持状态;冗长日志(git/npm/cargo…)自动压缩;超长输出只保留头尾,完整内容落盘可回读;长命令自动转后台;可选拦截 `cat`、`grep`、`find`、`sed -i`,引导改用专用工具(默认关闭)
|
|
26
26
|
- **安全删除**:`rm` 默认把文件移入系统回收站而非永久删除,误删可恢复;需要真删时用 `command rm`
|
|
27
27
|
- **read**:按行区间精准读取(支持 `:N-M`、`:raw`、多区间);大代码文件默认返回结构摘要,细节按需展开;zip/tar 归档、SQLite、notebook、PDF 直接读;PNG / JPEG / WebP / GIF 图片直读,超大图自动缩放;可抓取网页内容(含网页内图片);SPA 站点(内容由 JS 动态渲染,如 excalidraw.com)自动改用本机浏览器渲染后抓取
|
|
28
28
|
- **write**:原子写入,返回修改 diff;支持补丁式写入,可直接写 zip/tar 归档成员与 SQLite 数据
|
|
@@ -87,12 +87,45 @@ dsh plugin --profile web add link:<本仓库路径>
|
|
|
87
87
|
|
|
88
88
|
写入通过宿主自己的 `ctx.configEditor` 完成,因此自带 profile 锁、并发保护、原子写与失败回滚。
|
|
89
89
|
|
|
90
|
-
> 从 0.1.9 或更早升级过来的话:`~/.dsh/.agent-presets/` 目录**已经没有任何代码读它**(0.1.7 起预设改为补丁层声明),可以安全删除;设置页检测到它会给出提示。
|
|
91
|
-
|
|
92
90
|
## 配置
|
|
93
91
|
|
|
94
92
|
开箱即用,无需配置。需要微调时常用项有:后台阈值(`autoBackgroundMs`)、超时(`defaultTimeoutMs` / `maxTimeoutMs`)、输出截断窗口、编辑默认模式(`editMode`,默认 `replace`)、结构化摘要开关(`readSummarizeEnabled`)。抓取相关:可切换网页转 Markdown 的后端(`fetchReader`,含浏览器 JS 渲染——SPA 页面),设置面板里可一键「探测浏览器」查看本机可用的 Chrome/Edge。
|
|
95
93
|
|
|
94
|
+
## 性能
|
|
95
|
+
|
|
96
|
+
标准模式/标准模式增强下,在 Linux 上跑 SWE-bench Multilingual(20 题 × 3 轮):
|
|
97
|
+
|
|
98
|
+
| 指标 | 官方工具面 | 本插件(命令拦截开) | 本插件(命令拦截关,默认) |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| 解题率 | 83.3% | **83.3%** | 81.7%(低 1 题,正常波动) |
|
|
101
|
+
| 步数 / 题 | 24.4 | 29.7(+21.7%) | 25.3(+3.8%) |
|
|
102
|
+
| 总 token / 题 | **729,434** | 954,100(+30.8%) | 779,799(+6.9%) |
|
|
103
|
+
| 系统提示词 / 步(含工具定义) | **4,642** | 6,305(+35.8%) | 6,305(+35.8%) |
|
|
104
|
+
| 系统提示词累计 / 题(单步 × 步数) | **113,265** | 187,259(+65.3%) | 159,622(+40.9%) |
|
|
105
|
+
| 净 token / 题(除系统提示词) | **616,169** | 766,841(+24.5%) | 620,177(+0.7%) |
|
|
106
|
+
| 任务结束后平均上下文长度(token) | **42,817** | 48,645(+13.6%) | 44,781(+4.6%) |
|
|
107
|
+
| `read` 调用次数 | 225 | 459 | **198** |
|
|
108
|
+
| `bash` 每次调用耗时 | 3,140 ms | **2,391 ms** | 2,045 ms |
|
|
109
|
+
| `bash` 每次调用返回 token | 790 tok | **343 tok** | 638 tok |
|
|
110
|
+
| 平均耗时 / 题(未计入 token 速率波动、网络速度带来的误差) | **123 s** | 135 s | 151 s |
|
|
111
|
+
|
|
112
|
+
注:
|
|
113
|
+
|
|
114
|
+
1. 平均耗时是端到端实测,未计入 `token` 速率波动、网络速度带来的误差,绝对值只作横向参考。
|
|
115
|
+
2. 本节性能只代表在该基准测试下的表现(该基准任务占用上下文较低,因此无法代表大任务情况)
|
|
116
|
+
3. 由大模型计算统计核对,数据可能有疏漏,但不影响整体结论
|
|
117
|
+
|
|
118
|
+
结论:
|
|
119
|
+
|
|
120
|
+
1. 解题率与官方持平(命令拦截关的那组低 1 题,属正常波动)
|
|
121
|
+
2. `bash` 本身更省,省下的 token 基本能抵消工具定义变厚带来的系统提示词增量(功能更强,工具定义比官方预设更厚)
|
|
122
|
+
3. `bash` 更快,但带来的增益会被 `bash` 自身以及其他增强工具带来的副作用抹平
|
|
123
|
+
4. 在 `linux` 上,综合性能和官方默认标准模式工具相当,但功能更丰富
|
|
124
|
+
|
|
125
|
+
因此本插件工具在 `linux` 上,无法承诺更省 `token` 或具有更强的性能;`windows` 不适合跑标准测试,只能从**个人体感**说比 `powershell` 更好用。
|
|
126
|
+
|
|
127
|
+
完整的四组数据与逐题矩阵见 [docs/benchmark/sbml-compare-ops.md](docs/benchmark/sbml-compare-ops.md)。
|
|
128
|
+
|
|
96
129
|
## 环境要求
|
|
97
130
|
|
|
98
131
|
- **dsh CLI**:需全局安装,`npm i -g @deepseek-ai/dsh`
|
package/lib/client.js
CHANGED
package/lib/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ __installBunShim();
|
|
|
3
3
|
import { n as fs_promises_shim_exports, t as exists } from "./fs-promises-shim-B2YFWkcd.mjs";
|
|
4
4
|
import { i as setRuntimeLogger, n as systemChromiumCandidatesForTest, r as runtimeLogger } from "./launch-BIWNyrBB.mjs";
|
|
5
5
|
import { $ as isEnoent, A as formatCount, C as warn, D as installBunShim, F as $flag, N as $env, O as formatAge, P as $envpos, Q as hasFsCode, U as getAutoQaDbPath, V as VERSION, X as getRemoteDir, Y as getPuppeteerDir, at as findOnPath, b as debug, c as combineSignals, d as $which, et as isEnotdir, it as untilAborted, k as formatBytes$1, l as exec, m as readImageMetadata, nt as peekFile, q as getInstallId, tt as isProbablyBinary, u as render, x as error } from "./src-AELUUb5U.mjs";
|
|
6
|
-
import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-
|
|
6
|
+
import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-eJcxzVy7.mjs";
|
|
7
7
|
import { t as sanitizeText } from "./sanitize-text-BCf5KmCf.mjs";
|
|
8
8
|
import { n as isRecord$5 } from "./type-guards-CttAANEx.mjs";
|
|
9
9
|
import { n as ToolError, r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
|
|
@@ -4202,7 +4202,7 @@ const TOOL_PLUS_FIELDS = [
|
|
|
4202
4202
|
{
|
|
4203
4203
|
name: "interceptorEnabled",
|
|
4204
4204
|
kind: "boolean",
|
|
4205
|
-
default:
|
|
4205
|
+
default: false,
|
|
4206
4206
|
labelKey: "interceptorEnabled",
|
|
4207
4207
|
hintKey: "interceptorEnabledHint",
|
|
4208
4208
|
group: "behavior",
|
|
@@ -4958,7 +4958,7 @@ const Config = z.object({
|
|
|
4958
4958
|
outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)).volatile(),
|
|
4959
4959
|
outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)).volatile(),
|
|
4960
4960
|
minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)).volatile(),
|
|
4961
|
-
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled",
|
|
4961
|
+
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", false)).volatile(),
|
|
4962
4962
|
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)).volatile(),
|
|
4963
4963
|
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)).volatile(),
|
|
4964
4964
|
rmSafe: z.boolean().default(fieldDefault("rmSafe", true)).volatile(),
|
|
@@ -5069,7 +5069,7 @@ function resolveConfig(input) {
|
|
|
5069
5069
|
outputSinkTailBytes: config.outputSinkTailBytes ?? fieldDefault("outputSinkTailBytes", 51200),
|
|
5070
5070
|
outputSinkHeadBytes: config.outputSinkHeadBytes ?? fieldDefault("outputSinkHeadBytes", 20480),
|
|
5071
5071
|
minimizer: resolveMinimizer(),
|
|
5072
|
-
interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled",
|
|
5072
|
+
interceptorEnabled: config.interceptorEnabled ?? fieldDefault("interceptorEnabled", false),
|
|
5073
5073
|
nonInteractiveEnv: config.nonInteractiveEnv ?? fieldDefault("nonInteractiveEnv", true),
|
|
5074
5074
|
snapshotEnabled: config.snapshotEnabled ?? fieldDefault("snapshotEnabled", true),
|
|
5075
5075
|
rmSafe: config.rmSafe ?? fieldDefault("rmSafe", true),
|
|
@@ -11479,7 +11479,7 @@ let specialHandlersPromise;
|
|
|
11479
11479
|
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
11480
11480
|
*/
|
|
11481
11481
|
function loadSpecialHandlers() {
|
|
11482
|
-
specialHandlersPromise ??= import("./scrapers-
|
|
11482
|
+
specialHandlersPromise ??= import("./scrapers-C6oBIusN.mjs").then((m) => m.specialHandlers);
|
|
11483
11483
|
return specialHandlersPromise;
|
|
11484
11484
|
}
|
|
11485
11485
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-plus package entry — pi-natives compatibility bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* \`@oh-my-pi/pi-natives\` ships a Bun-only \`import.meta.dir\` in
|
|
5
|
+
* native/loader-state.js. A postinstall patch alone is not enough under pnpm:
|
|
6
|
+
* the file is a hardlink to the content-addressable store, and a later
|
|
7
|
+
* install that repairs or re-links the store can restore the buggy bytes.
|
|
8
|
+
*
|
|
9
|
+
* This entry runs the shared patch helper first, then dynamically imports the
|
|
10
|
+
* real plugin entry. It must not statically import index.mjs (or any module
|
|
11
|
+
* that reaches pi-natives): static imports are evaluated before this body.
|
|
12
|
+
*
|
|
13
|
+
* The helper writes through a temp file + rename, so it breaks the store
|
|
14
|
+
* hardlink and keeps this profile's copy private.
|
|
15
|
+
*/
|
|
16
|
+
import { ensurePiNativesPatched } from "../scripts/patch-pi-natives.mjs";
|
|
17
|
+
|
|
18
|
+
const result = ensurePiNativesPatched({ detach: true });
|
|
19
|
+
if (result.error) {
|
|
20
|
+
console.warn("[tool-plus] pi-natives compat: " + result.error);
|
|
21
|
+
} else if (result.patched > 0 || result.detached > 0) {
|
|
22
|
+
console.log(
|
|
23
|
+
"[tool-plus] pi-natives compat: patched=" + result.patched + ", detached=" + result.detached,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Runtime-computed URL: keep this dynamic so a bundler never inlines the
|
|
28
|
+
// plugin graph (and its static pi-natives imports) ahead of the patch above.
|
|
29
|
+
const mod = await import(new URL("./index.mjs", import.meta.url).href);
|
|
30
|
+
|
|
31
|
+
export const name = mod.name;
|
|
32
|
+
export const inject = mod.inject;
|
|
33
|
+
export const Config = mod.Config;
|
|
34
|
+
export const apply = mod.apply;
|
|
@@ -3,7 +3,7 @@ __installBunShim();
|
|
|
3
3
|
import { n as fs_promises_shim_exports } from "./fs-promises-shim-B2YFWkcd.mjs";
|
|
4
4
|
import { $ as isEnoent, B as USER_AGENT, C as warn, G as getDocsRsCacheDir, N as $env, c as combineSignals, j as formatNumber, k as formatBytes, l as exec, s as Snowflake, y as parseFrontmatter } from "./src-AELUUb5U.mjs";
|
|
5
5
|
import { W as tryParseJson } from "./utils-cZX3f_Ls.mjs";
|
|
6
|
-
import { a as fetchBinary, c as getParallelExtractContent, i as convertWithMarkit, l as ensureTool, m as settings, n as asRecord, o as extractWithParallel, r as asString, s as findParallelApiKey, t as asNumber } from "./utils-
|
|
6
|
+
import { a as fetchBinary, c as getParallelExtractContent, i as convertWithMarkit, l as ensureTool, m as settings, n as asRecord, o as extractWithParallel, r as asString, s as findParallelApiKey, t as asNumber } from "./utils-eJcxzVy7.mjs";
|
|
7
7
|
import { n as isRecord } from "./type-guards-CttAANEx.mjs";
|
|
8
8
|
import { r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
|
|
9
9
|
import { c as getLocalizedText, d as looksLikeHtml, i as decodeHtmlEntities, l as htmlToBasicMarkdown, o as formatIsoDate, r as buildResult, s as formatMediaDuration, u as loadPage } from "./types-BLrLohwK.mjs";
|
|
@@ -6267,7 +6267,7 @@ const settings = {
|
|
|
6267
6267
|
};
|
|
6268
6268
|
//#endregion
|
|
6269
6269
|
//#region package.json
|
|
6270
|
-
var version = "0.1.
|
|
6270
|
+
var version = "0.1.16-beta.0";
|
|
6271
6271
|
/** `.tmp` files older than this are treated as orphaned writes and swept. */
|
|
6272
6272
|
const TMP_ORPHAN_MAX_AGE_MS = 3e5;
|
|
6273
6273
|
function markitConversionCacheKey(bytes, extension) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoso/dsh-tool-plus",
|
|
3
3
|
"description": "Oh My Pi bash runtime ported as a deepseek-harness tool plugin: persistent shell session, output minimizer, managed background jobs, command interception",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.16-beta.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
"node": "^22.19.0 || >=24.0.0",
|
|
37
37
|
"dsh": ">=0.2.1-0 <0.3.0-0"
|
|
38
38
|
},
|
|
39
|
-
"main": "lib/
|
|
39
|
+
"main": "lib/pi-natives-bootstrap.mjs",
|
|
40
40
|
"types": "lib/types/index.d.ts",
|
|
41
41
|
"icon": "./assets/icon.png",
|
|
42
42
|
"exports": {
|
|
43
43
|
".": {
|
|
44
44
|
"types": "./lib/types/index.d.ts",
|
|
45
|
-
"default": "./lib/
|
|
45
|
+
"default": "./lib/pi-natives-bootstrap.mjs"
|
|
46
46
|
},
|
|
47
47
|
"./client": {
|
|
48
48
|
"types": "./lib/types/client/index.d.ts",
|
|
@@ -1,108 +1,241 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Self-heal
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Self-heal \`@oh-my-pi/pi-natives\` for Node runtimes.
|
|
4
|
+
*
|
|
5
|
+
* The upstream package locates its prebuilt binaries with the Bun-only
|
|
6
|
+
* \`import.meta.dir\` (native/loader-state.js). Node has no such property, so
|
|
7
|
+
* \`path.join(undefined, "..", "native")\` throws ERR_INVALID_ARG_TYPE before
|
|
8
|
+
* any tool can load.
|
|
9
|
+
*
|
|
10
|
+
* Two callers share this module:
|
|
11
|
+
* - the package postinstall (plain npm / yarn / npx installs), and
|
|
12
|
+
* - lib/pi-natives-bootstrap.mjs, which runs before the real plugin entry
|
|
13
|
+
* so a pnpm store repair/re-link cannot break the next dsh start.
|
|
14
|
+
*
|
|
15
|
+
* Copy-on-patch is intentional. pnpm links package files from its
|
|
16
|
+
* content-addressable store with hardlinks; writing the linked file in place
|
|
17
|
+
* mutates the store and every profile that shares that inode. Writing a temp
|
|
18
|
+
* file and renaming it over the target gives this profile a private inode and
|
|
19
|
+
* leaves the store untouched. When the file is already patched but still
|
|
20
|
+
* shared, the same operation detaches it once.
|
|
21
|
+
*
|
|
22
|
+
* Resolution prefers \`import.meta.resolve("@oh-my-pi/pi-natives")\` because the
|
|
23
|
+
* package is ESM-only; createRequire.resolve throws
|
|
24
|
+
* ERR_PACKAGE_PATH_NOT_EXPORTED for it. The historical ancestor walk remains
|
|
25
|
+
* as a fallback for unknown layouts, and can be forced with a searchRoot.
|
|
26
|
+
*
|
|
27
|
+
* Idempotent; the CLI never throws.
|
|
28
|
+
*
|
|
29
|
+
* Usage: node scripts/patch-pi-natives.mjs [searchRoot]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
existsSync,
|
|
34
|
+
readdirSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
realpathSync,
|
|
37
|
+
renameSync,
|
|
38
|
+
rmSync,
|
|
39
|
+
statSync,
|
|
40
|
+
writeFileSync,
|
|
41
|
+
} from "node:fs";
|
|
42
|
+
import { dirname, join, resolve } from "node:path";
|
|
43
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
44
|
+
|
|
45
|
+
const PKG_SUBPATH = join("node_modules", "@oh-my-pi", "pi-natives");
|
|
46
|
+
const TARGET_REL = join("native", "loader-state.js");
|
|
47
|
+
// Exact buggy token; \b keeps an already-fixed \`import.meta.dirname\` intact.
|
|
48
|
+
const BUGGY = /import\.meta\.dir\b/g;
|
|
49
|
+
const PATCHED_LITERAL = "import.meta.dirname";
|
|
50
|
+
const PKG_NAME = "@oh-my-pi/pi-natives";
|
|
51
|
+
|
|
52
|
+
function log(message) {
|
|
53
|
+
console.log("[patch-pi-natives] " + message);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function warn(message) {
|
|
57
|
+
console.warn("[patch-pi-natives] " + message);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Walk upwards from start, yielding every candidate package directory. */
|
|
61
|
+
function* candidatePackageDirs(start) {
|
|
62
|
+
let current = resolve(start);
|
|
63
|
+
for (;;) {
|
|
64
|
+
const candidate = join(current, PKG_SUBPATH);
|
|
65
|
+
if (existsSync(candidate)) yield candidate;
|
|
66
|
+
const parent = dirname(current);
|
|
67
|
+
if (parent === current) return;
|
|
68
|
+
current = parent;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function firstMatchedFile(packageDir) {
|
|
73
|
+
const direct = join(packageDir, TARGET_REL);
|
|
74
|
+
if (existsSync(direct)) return direct;
|
|
75
|
+
const stack = [packageDir];
|
|
76
|
+
while (stack.length > 0) {
|
|
77
|
+
const entry = stack.pop();
|
|
78
|
+
for (const item of readdirSync(entry)) {
|
|
79
|
+
const full = join(entry, item);
|
|
80
|
+
const stat = statSync(full);
|
|
81
|
+
if (stat.isDirectory()) stack.push(full);
|
|
82
|
+
else if (item === "loader-state.js") return full;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Walk up from a resolved entry until the pi-natives package root. */
|
|
89
|
+
function packageRootFor(entry) {
|
|
90
|
+
let dir = dirname(entry);
|
|
91
|
+
for (;;) {
|
|
92
|
+
const manifest = join(dir, "package.json");
|
|
93
|
+
if (existsSync(manifest)) {
|
|
94
|
+
try {
|
|
95
|
+
const pkg = JSON.parse(readFileSync(manifest, "utf8"));
|
|
96
|
+
if (pkg.name === PKG_NAME) return dir;
|
|
97
|
+
} catch {
|
|
98
|
+
// Keep walking: a malformed package.json is not our target.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const parent = dirname(dir);
|
|
102
|
+
if (parent === dir) return undefined;
|
|
103
|
+
dir = parent;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string | undefined} searchRoot
|
|
109
|
+
* @param {boolean} preferExact
|
|
110
|
+
* @returns {{ file: string | undefined, inspected: number }}
|
|
111
|
+
*/
|
|
112
|
+
function resolveTarget(searchRoot, preferExact) {
|
|
113
|
+
if (preferExact) {
|
|
114
|
+
try {
|
|
115
|
+
const entry = fileURLToPath(import.meta.resolve(PKG_NAME));
|
|
116
|
+
const packageRoot = packageRootFor(entry);
|
|
117
|
+
if (packageRoot !== undefined) {
|
|
118
|
+
const direct = join(packageRoot, TARGET_REL);
|
|
119
|
+
if (existsSync(direct)) return { file: direct, inspected: 1 };
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Fall through to the ancestor walk.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const start = searchRoot ? resolve(searchRoot) : dirname(fileURLToPath(import.meta.url));
|
|
127
|
+
let inspected = 0;
|
|
128
|
+
for (const packageDir of candidatePackageDirs(start)) {
|
|
129
|
+
inspected += 1;
|
|
130
|
+
const file = firstMatchedFile(packageDir);
|
|
131
|
+
if (file !== undefined) return { file, inspected };
|
|
132
|
+
}
|
|
133
|
+
return { file: undefined, inspected };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Replace the target with a private inode. rename() removes this path's link
|
|
138
|
+
* to the old inode (the store and other profiles keep theirs); the temp file
|
|
139
|
+
* becomes the new content.
|
|
140
|
+
*/
|
|
141
|
+
function atomicReplace(file, content) {
|
|
142
|
+
const real = realpathSync(file);
|
|
143
|
+
const mode = statSync(real).mode;
|
|
144
|
+
const tmp = real + "." + process.pid + "." + Date.now() + ".tmp";
|
|
145
|
+
writeFileSync(tmp, content, { mode });
|
|
146
|
+
try {
|
|
147
|
+
renameSync(tmp, real);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const code = error && error.code;
|
|
150
|
+
if (code !== "EEXIST" && code !== "EPERM" && code !== "EXDEV") throw error;
|
|
151
|
+
rmSync(real, { force: true });
|
|
152
|
+
renameSync(tmp, real);
|
|
153
|
+
} finally {
|
|
154
|
+
rmSync(tmp, { force: true });
|
|
155
|
+
}
|
|
156
|
+
return real;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {{
|
|
161
|
+
* target?: string,
|
|
162
|
+
* searchRoot?: string,
|
|
163
|
+
* detach?: boolean,
|
|
164
|
+
* copyOnPatch?: boolean,
|
|
165
|
+
* }} [options]
|
|
166
|
+
* @returns {{ inspected: number, patched: number, detached: number, files: string[], error?: string }}
|
|
167
|
+
*/
|
|
168
|
+
export function ensurePiNativesPatched(options = {}) {
|
|
169
|
+
const { target, searchRoot, detach = true, copyOnPatch = true } = options;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
let found;
|
|
173
|
+
if (target) {
|
|
174
|
+
found = { file: existsSync(target) ? target : undefined, inspected: existsSync(target) ? 1 : 0 };
|
|
175
|
+
} else {
|
|
176
|
+
found = resolveTarget(searchRoot, searchRoot === undefined);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (found.file === undefined) {
|
|
180
|
+
return {
|
|
181
|
+
inspected: found.inspected,
|
|
182
|
+
patched: 0,
|
|
183
|
+
detached: 0,
|
|
184
|
+
files: [],
|
|
185
|
+
error: "no " + PKG_NAME + "/" + TARGET_REL + " found",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const real = realpathSync(found.file);
|
|
190
|
+
const source = readFileSync(real, "utf8");
|
|
191
|
+
const next = source.replace(BUGGY, PATCHED_LITERAL);
|
|
192
|
+
const patched = next !== source;
|
|
193
|
+
const shared = statSync(real).nlink > 1;
|
|
194
|
+
const shouldDetach = !patched && detach && shared;
|
|
195
|
+
|
|
196
|
+
if (!patched && !shouldDetach) {
|
|
197
|
+
return { inspected: found.inspected, patched: 0, detached: 0, files: [real] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (copyOnPatch) atomicReplace(real, next);
|
|
201
|
+
else writeFileSync(real, next);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
inspected: found.inspected,
|
|
205
|
+
patched: patched ? 1 : 0,
|
|
206
|
+
detached: shouldDetach ? 1 : 0,
|
|
207
|
+
files: [real],
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
inspected: 0,
|
|
212
|
+
patched: 0,
|
|
213
|
+
detached: 0,
|
|
214
|
+
files: [],
|
|
215
|
+
error: error && error.message ? error.message : String(error),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function main() {
|
|
221
|
+
const searchRoot = process.argv[2] ? resolve(process.argv[2]) : undefined;
|
|
222
|
+
const result = ensurePiNativesPatched({ detach: true, searchRoot });
|
|
223
|
+
if (result.error) {
|
|
224
|
+
warn("WARN " + result.error);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
for (const file of result.files) {
|
|
228
|
+
if (result.patched > 0) log(file + ": patched import.meta.dir -> import.meta.dirname");
|
|
229
|
+
else if (result.detached > 0) log(file + ": detached hardlink (private copy)");
|
|
230
|
+
else log(file + ": already patched");
|
|
231
|
+
}
|
|
232
|
+
log("done: patched=" + result.patched + ", detached=" + result.detached);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
236
|
+
try {
|
|
237
|
+
main();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
warn("WARN self-heal skipped: " + (error && error.message ? error.message : String(error)));
|
|
240
|
+
}
|
|
241
|
+
}
|