dsh-plugin-git-commit-push 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.en.md +473 -0
- package/README.md +381 -2
- package/SKILL.md +79 -0
- package/capture-git-format.mjs +93 -0
- package/cordis.patch.yml +35 -0
- package/e2e-check.mjs +26 -0
- package/git-commit-push.config.json +17 -0
- package/icon.svg +1 -0
- package/index.js +908 -0
- package/lib/analyze.js +517 -0
- package/lib/config.js +300 -0
- package/lib/git.js +562 -0
- package/lib/profile-edit.mjs +118 -0
- package/lib/schema.js +111 -0
- package/lib/skill.js +95 -0
- package/lib/survey.js +225 -0
- package/lib/test-fixture.mjs +64 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +81 -4
- package/self-test-git.mjs +442 -0
- package/self-test.mjs +913 -0
- package/setup.ps1 +226 -0
- package/setup.sh +190 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ygzhang-lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# dsh-plugin-git-commit-push
|
|
2
|
+
|
|
3
|
+
English | [中文](README.md)
|
|
4
|
+
|
|
5
|
+
DSH (DeepSeek Harness) Git commit and push plugin, one call to complete: summarize changes, automatically generate commit messages for each changed file according to Conventional Commits, ask for tagging if necessary, and then push to the Git remote repository configured for the current project.
|
|
6
|
+
|
|
7
|
+
- Users can complete a commit and push with just a 0 Token through the `/git-commit-push` slash command (without going through the model, as the script automatically generates simple and clear commits according to the rules);
|
|
8
|
+
- DSH can utilize the `git_commit_push` tool, requiring minimal tokens to accomplish high-quality annotations and complete a single commit and push.
|
|
9
|
+
This plugin serves as a tool-based replacement for the earlier `.agents/skills/git-commit-push` Skill. It condenses the process of running dozens of Shell scripts and reading through a pile of raw Git output, which is typically required for pure Skill work, into **1-2 tool invocations + a compact card**.
|
|
10
|
+
|
|
11
|
+
Installing gives you three surfaces:
|
|
12
|
+
|
|
13
|
+
| Surface | Name | Used by |
|
|
14
|
+
| ------------- | ------------------------------------------------ | ----------------------------------------------- |
|
|
15
|
+
| Tool | `git_commit_push` (`prepare` / `apply` / `auto`) | the model |
|
|
16
|
+
| Slash command | `/git-commit-push` | you, **with no model round-trip at all** |
|
|
17
|
+
| Skill | `git-commit-push` | the model, loading the full procedure on demand |
|
|
18
|
+
|
|
19
|
+
- Platforms: **Windows and macOS / Linux** (see "Platforms")
|
|
20
|
+
- Requirements: DSH `>=0.2.0-rc.1 <0.3.0`, Node `>=20`, git `>=2.36`
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
### A. Plugins page (recommended)
|
|
25
|
+
|
|
26
|
+
DSH → **Settings → Plugins** → type the package name into the install field:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
dsh-plugin-git-commit-push
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then **restart DSH**. The same page can **enable / disable / uninstall** it — which is exactly why this
|
|
33
|
+
package declares `dsh.bundle.patch` and makes itself a DSH _bundle_. Without that declaration the page
|
|
34
|
+
answers every request with **"这个包没有声明组合包,不能作为插件管理"** (host code `not-bundle`).
|
|
35
|
+
|
|
36
|
+
### B. Command line
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
dsh plugin --profile <profile> add dsh-plugin-git-commit-push # install
|
|
40
|
+
dsh plugin --profile <profile> remove dsh-plugin-git-commit-push # uninstall
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`<profile>` is your profile name (`web`, `headless`, a custom one). Both directions edit the profile's
|
|
44
|
+
`package.json` (dependency + `dsh.profile.bundles`) and **need a DSH restart**.
|
|
45
|
+
|
|
46
|
+
### C. From a checkout (development / offline)
|
|
47
|
+
|
|
48
|
+
Clone the repository and use the installer to wire the checkout into a profile as a `link:` dependency:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
# Windows (default profile: desktop)
|
|
52
|
+
powershell -ExecutionPolicy Bypass -File .\setup.ps1
|
|
53
|
+
powershell -ExecutionPolicy Bypass -File .\setup.ps1 -Profile web
|
|
54
|
+
powershell -ExecutionPolicy Bypass -File .\setup.ps1 -Uninstall
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# macOS / Linux
|
|
59
|
+
sh setup.sh # default profile: desktop
|
|
60
|
+
sh setup.sh web # a specific profile
|
|
61
|
+
sh setup.sh web --uninstall
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The scripts do two things (idempotent, with backups): add the checkout as a `link:` dependency and add the
|
|
65
|
+
package name to `dsh.profile.bundles`, then run `pnpm install` inside the profile. **The mount row is not
|
|
66
|
+
written by the scripts** — it comes from this package's own bundle patch (see "The DSH bundle contract").
|
|
67
|
+
They also **remove** the mount row an older revision wrote into the profile's `cordis.patch.yml`, because
|
|
68
|
+
Loader `insert` is append-only and the same id mounted twice would register the plugin twice.
|
|
69
|
+
|
|
70
|
+
> Do not mix the three ways: a package should be mounted exactly once per profile.
|
|
71
|
+
|
|
72
|
+
### After installing
|
|
73
|
+
|
|
74
|
+
Once DSH is restarted:
|
|
75
|
+
|
|
76
|
+
- the model can call `git_commit_push`, and you can type `/git-commit-push`;
|
|
77
|
+
- Settings → Plugins lists this package (title "Git 提交与推送", with an icon) and can enable/disable/uninstall it;
|
|
78
|
+
- the model's skill catalog contains `git-commit-push`;
|
|
79
|
+
- Settings shows this plugin's **configuration form** (11 fields, applied live with no restart).
|
|
80
|
+
|
|
81
|
+
## Trigger policy (read this first)
|
|
82
|
+
|
|
83
|
+
**Only two situations may use it; it never fires on its own:**
|
|
84
|
+
|
|
85
|
+
1. **the user typed the `/git-commit-push` slash command** — the command runs it, no model involved;
|
|
86
|
+
2. **the user explicitly asked** to commit and/or push ("commit", "push", "提交", "推送").
|
|
87
|
+
|
|
88
|
+
**Finishing an edit, completing a task, an almost-over session, or a user saying "save it" are NOT
|
|
89
|
+
triggers.** Editing files is not a request to commit them. When in doubt, ask instead of committing.
|
|
90
|
+
|
|
91
|
+
The rule is stated in three model-visible places: the `git_commit_push` tool description (the first thing
|
|
92
|
+
a model reads when choosing a tool), [SKILL.md](./SKILL.md) (registered as an embedded skill at mount
|
|
93
|
+
time), and the command description. The plugin cannot enforce it inside `execute` — by then the decision
|
|
94
|
+
has already been made.
|
|
95
|
+
|
|
96
|
+
## Is "0 tokens for commit + push" accurate?
|
|
97
|
+
|
|
98
|
+
**Partly — it needs qualification.** The honest split is by path:
|
|
99
|
+
|
|
100
|
+
| Path | Model tokens | Why |
|
|
101
|
+
| ---------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| **1. `/git-commit-push` slash command** | **0 (really)** | No model request is made at all. Command discovery, execution and UI output cost no model tokens, and the result is rendered in the UI only — it never enters the transcript. This is the **only** truly 0-token path. |
|
|
103
|
+
| **2. `git_commit_push(auto)` tool call** | a few | The model has to emit the call (arguments + thinking). The plugin writes the message itself, so there is **no** second round-trip; the card is ~200 tokens. One round trip. |
|
|
104
|
+
| **3. `prepare` + `apply` (default)** | ~2 round trips | Only the `prepare` card (~200 tokens) and the `apply` `message` argument enter the context — **no diff is read**. |
|
|
105
|
+
|
|
106
|
+
So: "the slash command costs 0 tokens" is accurate; "the tool costs 0 tokens" is **not** — the model still
|
|
107
|
+
pays for each call. What the plugin saves is **comparative**: reading raw git output (thousands of tokens →
|
|
108
|
+
~200) and several round trips.
|
|
109
|
+
|
|
110
|
+
One **fixed cost** must also be stated: while the plugin is installed and the tool is visible to the model,
|
|
111
|
+
its schema (a ~1.5 KB description plus parameters) enters **every** request — roughly **600 tokens**, paid
|
|
112
|
+
even when you never commit. If you only want `/git-commit-push` and never let the model call the tool, moving
|
|
113
|
+
the tool out of the model-visible surface (or `deferLoading` it) removes that cost.
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
git_commit_push({ action: "prepare" }) # survey only, no writes
|
|
119
|
+
git_commit_push({ action: "apply", message: "feat(x): …" }) # commit + push
|
|
120
|
+
git_commit_push({ action: "auto" }) # commit with the rule-generated message
|
|
121
|
+
git_commit_push({ action: "apply", message: "…", tag: "v1.2.3" })
|
|
122
|
+
git_commit_push({ action: "prepare", cwd: "/path/to/repo" }) # session cwd is not the repository
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`/git-commit-push` variants (**0 tokens, no model involved**):
|
|
126
|
+
`/git-commit-push`, `/git-commit-push --prepare`, `/git-commit-push --no-push`, `/git-commit-push --en`, `/git-commit-push --tag=v1.2.3`, `/git-commit-push fix login timeout`.
|
|
127
|
+
|
|
128
|
+
### What the card looks like
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
🔎 **改动预览(未提交)** · `main` · 3 files · +48 / -12
|
|
132
|
+
status: 1 added / 2 modified
|
|
133
|
+
added src/foo/bar.ts +40/-0 → feat(foo): add bar component
|
|
134
|
+
modified src/foo/baz.ts +8/-10 → fix(foo): correct parseThing decision
|
|
135
|
+
deleted src/old.ts → refactor: remove old
|
|
136
|
+
tag evidence: version 1.2.3 → 1.2.4 (package.json)
|
|
137
|
+
recent style: "feat(ui): add theme switch" "fix(api): correct retry decision"
|
|
138
|
+
subject: `feat(foo): update bar component`
|
|
139
|
+
(preview only — nothing committed, nothing pushed)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
And after a commit:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
✅ **Git 提交并推送成功** · `main` · `a1b2c3d`
|
|
146
|
+
信息:feat(foo): 更新 bar 组件
|
|
147
|
+
提交 3 个文件 · +48 / -12
|
|
148
|
+
- feat(foo): 新增 bar 组件 · src/foo/bar.ts
|
|
149
|
+
- fix(foo): 修正 parseThing 判断 · src/foo/baz.ts
|
|
150
|
+
- refactor: 移除 old · src/old.ts
|
|
151
|
+
标签:v1.2.4
|
|
152
|
+
推送:已推送(含标签)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Four outcomes are unmistakable, so a card can no longer be mistaken for "nothing happened":
|
|
156
|
+
|
|
157
|
+
| First line | Meaning |
|
|
158
|
+
| ------------------------------- | --------------------------------------------------------------------- |
|
|
159
|
+
| `✅ **Git 提交并推送成功**` | committed and pushed |
|
|
160
|
+
| `✅ **Git 提交成功(未推送)**` | committed; `autoPush` is off or this call said `--no-push` |
|
|
161
|
+
| `⚠️ **已提交,但推送失败**` | the commit is local, the push failed (reason in `说明:`) |
|
|
162
|
+
| `❌ **提交失败**` | nothing was committed; your changes are untouched in the working tree |
|
|
163
|
+
|
|
164
|
+
Other states: `ℹ️ **没有需要提交的改动**`, `⚠️ **当前目录不是 Git 仓库**` (with the candidate repositories listed),
|
|
165
|
+
`❌ **未初始化 Git**`, `❌ **找不到 git**`.
|
|
166
|
+
|
|
167
|
+
### Multiple files: one commit, one note per file
|
|
168
|
+
|
|
169
|
+
Several files are still **one commit**, but its body carries one typed Conventional note per file instead of a single
|
|
170
|
+
sentence pretending to cover all of them:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
feat(api): 更新 decideRetry
|
|
174
|
+
|
|
175
|
+
- feat(api): 更新 decideRetry · src/api/retry.ts
|
|
176
|
+
- docs: 更新文档 guide · docs/guide.md
|
|
177
|
+
- test(api): 新增 retry.test.ts · src/api/retry.test.ts
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- Every note describes **that one file**: the type comes from the file itself (`docs/` → `docs:`, `*.test.ts` →
|
|
181
|
+
`test:`), the scope is its directory (dropped when it would only repeat the type, so `docs(docs)` never appears),
|
|
182
|
+
and the summary prefers a symbol the file's own diff declares (`update decideRetry`).
|
|
183
|
+
- Supply only a **subject** (a single-line `message`) and the per-file notes are appended for you; supply your own
|
|
184
|
+
body (a multi-line `message`) and it is used **verbatim**, with no generated notes added.
|
|
185
|
+
- A **single-file commit has no body** — its subject says it all.
|
|
186
|
+
- The body names at most `maxFilesShown` files (12 by default); the rest collapse into `- …另有 N 个文件`.
|
|
187
|
+
|
|
188
|
+
(Card text is localized: `defaultLanguage: "zh"` or `"en"` — including the per-file notes. The card _format_ is
|
|
189
|
+
Chinese-and-English-mixed by design: the verdict line and labels are Chinese in both locales, the file notes follow
|
|
190
|
+
the message language.)
|
|
191
|
+
|
|
192
|
+
## Settings
|
|
193
|
+
|
|
194
|
+
**Prefer DSH's own settings form.** The plugin exports a Cordis `Config` schema, so its row accepts a `config`
|
|
195
|
+
mapping: DSH's settings service derives a namespace from it, the Settings page renders the fields, and
|
|
196
|
+
`@deepseek-ai/dsh-config-editor` writes the choice into the active profile's `cordis.patch.yml` — on the very
|
|
197
|
+
row this bundle inserts — applying it through the Loader. Every field is declared **volatile**, so a change
|
|
198
|
+
does not remount the plugin and needs no DSH restart: it takes effect on the next tool call.
|
|
199
|
+
|
|
200
|
+
**Three sources, highest first:**
|
|
201
|
+
|
|
202
|
+
| Layer | Location | Written by |
|
|
203
|
+
| ----- | --------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
|
204
|
+
| 1 | the row's `config` (the profile's `cordis.patch.yml`) | DSH's settings form |
|
|
205
|
+
| 2 | `<DSH_HOME>/git-commit-push.config.json` (default `~/.dsh/git-commit-push.config.json`) | you, by hand; the fallback for `link:`/offline installs |
|
|
206
|
+
| 3 | the shipped template `git-commit-push.config.json` | the package |
|
|
207
|
+
| — | built-in defaults | whatever none of the above sets |
|
|
208
|
+
|
|
209
|
+
A missing file is not an error. A file that exists but is not valid JSON is **reported** — the result card
|
|
210
|
+
gains a "配置未生效: …" line rather than silently doing nothing. Omitted keys keep their defaults.
|
|
211
|
+
|
|
212
|
+
| Key | Default | Meaning | In the form |
|
|
213
|
+
| --------------------------- | -------- | -------------------------------------------------------------------------------------- | ----------- |
|
|
214
|
+
| `autoPush` | `true` | push after a successful commit | ✅ |
|
|
215
|
+
| `autoAdd` | `true` | `git add -A` before committing | ✅ |
|
|
216
|
+
| `tagOnVersionChange` | `true` | ask about a tag when a version file changed | ✅ |
|
|
217
|
+
| `tagOnBreaking` | `true` | ask when a public declaration was removed | ✅ |
|
|
218
|
+
| `tagOnFileCount` | `10` | ask when ≥ N files changed (`0` disables) | ✅ |
|
|
219
|
+
| `tagPrefix` | `"v"` | suggested tag prefix | ✅ |
|
|
220
|
+
| `askBeforeTag` | `true` | `false` tags silently (the only "no question" switch) | ✅ |
|
|
221
|
+
| `askTimeoutMs` | `120000` | how long the tag question waits | ✅ |
|
|
222
|
+
| `defaultLanguage` | `"zh"` | language of the generated message (`zh`/`en`) | ✅ |
|
|
223
|
+
| `maxFilesShown` | `12` | how many paths the card lists | ✅ |
|
|
224
|
+
| `pinnedIdentity.name/email` | empty | applied per commit with `-c user.name/-c user.email`; your git config is never written | ✅ |
|
|
225
|
+
|
|
226
|
+
> Where exactly the form lives depends on your DSH version (the plugins/config entry in Settings). The
|
|
227
|
+
> **Settings → Plugins → Plugin list** tab is explicitly **read-only** (it lets users inspect plugins
|
|
228
|
+
> _without changing their configuration_); the editable form comes from the settings service. Field
|
|
229
|
+
> descriptions are Chinese, matching the default message language — DSH has no per-field localization yet.
|
|
230
|
+
|
|
231
|
+
**Why a JSON file still exists**: the `Config` schema needs `@deepseek-ai/schemastery`, declared here as an
|
|
232
|
+
ordinary **dependency** so an npm install brings it along. With a `link:` (source checkout) install pnpm does
|
|
233
|
+
not install a link target's dependencies and the host's module resolution may not reach it either. In that
|
|
234
|
+
case the plugin **still works** — it just has no form (`Config === undefined`), and the JSON file is the only
|
|
235
|
+
way in. That is why the file stays, and why it outranks the shipped template.
|
|
236
|
+
|
|
237
|
+
Environment: `DSH_HOME` is the DSH home (default `~/.dsh`, Windows `%USERPROFILE%\.dsh`); it also decides
|
|
238
|
+
where the user settings file lives.
|
|
239
|
+
|
|
240
|
+
## The `git-commit-push` skill
|
|
241
|
+
|
|
242
|
+
[SKILL.md](./SKILL.md) is registered as an **embedded skill** (`ctx.skills.register(...)`) when the
|
|
243
|
+
plugin mounts, so there is nothing to copy into a skills directory after an npm install. The skill name
|
|
244
|
+
matches the package name (minus the `dsh-plugin-` prefix): the tool, the command, the skill and the npm
|
|
245
|
+
package share one name instead of two.
|
|
246
|
+
|
|
247
|
+
- To **override** it, keep a project-level skill of the same name (`.agents/skills/git-commit-push/SKILL.md`).
|
|
248
|
+
The registry ranks **project > runtime registration**, so your copy wins.
|
|
249
|
+
- A host without a skill registry still works: the skill and the `/git-commit-push` command are **optional
|
|
250
|
+
capabilities** (awaited through a scoped `ctx.inject`), and losing either never affects the
|
|
251
|
+
`git_commit_push` tool.
|
|
252
|
+
- **Coming from an early revision**: if you manually installed the old SKILL.md into
|
|
253
|
+
`~/.agents/skills/git-commit/`, that is a user-level skill under a _different_ name and will appear next
|
|
254
|
+
to the bundled `git-commit-push`. The package now owns that content, so deleting that directory is
|
|
255
|
+
recommended (or keep it and put your own rules there — different names, so neither shadows the other).
|
|
256
|
+
|
|
257
|
+
## Safety boundary
|
|
258
|
+
|
|
259
|
+
**Never**: modify `.gitignore`, write git config (including `user.name`/`user.email`), `push --force`,
|
|
260
|
+
`reset --hard`, `git clean`, `checkout -- <path>`, or `commit --no-verify`.
|
|
261
|
+
[lib/git.js](./lib/git.js) is the only place that talks to git and its command set is fixed — adding a
|
|
262
|
+
destructive verb means changing that file first.
|
|
263
|
+
|
|
264
|
+
**Handled automatically**: `push -u origin <branch>` when there is no upstream; a rejected push
|
|
265
|
+
(the remote moved) retries once after `pull --rebase`; on a rebase conflict it aborts **only the rebase it
|
|
266
|
+
started itself** (probing `rebase-merge`/`rebase-apply` first, so your own rebase progress is never
|
|
267
|
+
discarded) and reports honestly; an existing or invalid tag name is skipped with an explanation.
|
|
268
|
+
|
|
269
|
+
**Tagging is conservative**: a tag is created only with your explicit consent (or `askBeforeTag: false`).
|
|
270
|
+
An explicitly passed `tag` argument is treated as an instruction and skips the question. No answerer
|
|
271
|
+
available, you are not present (a delegated call), or the wait timed out — all mean **do not tag**, and the
|
|
272
|
+
card tells you how to add it later with the `tag` argument.
|
|
273
|
+
|
|
274
|
+
**It does not guess**: when the session directory is not a repository it reports the candidate repositories
|
|
275
|
+
for you to pick with `cwd` instead of committing in one of them, and "git is not installed" is a different
|
|
276
|
+
failure from "this is not a repository".
|
|
277
|
+
|
|
278
|
+
## Platforms
|
|
279
|
+
|
|
280
|
+
| Capability | Windows | macOS / Linux |
|
|
281
|
+
| --------------------------- | ----------------------------------------- | ------------------------------------- |
|
|
282
|
+
| Runtime (the plugin itself) | ✅ | ✅ audited: no missed platform branch |
|
|
283
|
+
| Installers (path C) | `setup.ps1` (PowerShell) | `setup.sh` (POSIX sh) |
|
|
284
|
+
| Uninstall | `setup.ps1 -Uninstall` | `setup.sh <profile> --uninstall` |
|
|
285
|
+
| Profile manifest edit | both call the same `lib/profile-edit.mjs` | same |
|
|
286
|
+
|
|
287
|
+
Audit notes for `index.js` + `lib/*`:
|
|
288
|
+
|
|
289
|
+
- the only platform branch is `DEV_NULL` (Windows `NUL` vs `/dev/null`), used solely by the **test fixture**
|
|
290
|
+
to isolate git's global config; the plugin's real git calls deliberately keep your global config
|
|
291
|
+
(credential helper, `pull.rebase`, `core.autocrlf` live there, and clearing it would change how your
|
|
292
|
+
repositories behave);
|
|
293
|
+
- no hard-coded drive letters, no `C:\`, no dependency on `powershell`; modules use relative `./`
|
|
294
|
+
specifiers (safe on case-sensitive filesystems); temporary directories come from `os.tmpdir()`;
|
|
295
|
+
- `setup.sh` avoids two cross-platform traps on purpose: **no `sed -i`** (BSD/macOS and GNU differ) and
|
|
296
|
+
**no `readlink -f`** (absent on macOS), using POSIX `awk` and `cd`+`pwd` instead.
|
|
297
|
+
|
|
298
|
+
## Self-check
|
|
299
|
+
|
|
300
|
+
No DSH required, and it never touches your repositories (tests build their own repos under the system temp
|
|
301
|
+
directory and delete them):
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
npm test # = node self-test.mjs && node self-test-git.mjs
|
|
305
|
+
node self-test.mjs # pure logic + packaging/config/form/skill contracts (86 checks)
|
|
306
|
+
node self-test-git.mjs # real git: porcelain -z framing, rename attribution, version detection, end-to-end commit, per-file notes, card verdicts (24 checks)
|
|
307
|
+
node capture-git-format.mjs # prints raw git -z bytes, for diagnosing framing
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
On Windows with the runtime DSH ships:
|
|
311
|
+
|
|
312
|
+
```powershell
|
|
313
|
+
& "$env:USERPROFILE\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" self-test.mjs
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
One test in `self-test-git.mjs` is **deliberately independent of the implementation**: it asks git the same
|
|
317
|
+
question twice — once with `git status --porcelain -z` and once with the plain, non-NUL format — and
|
|
318
|
+
requires both to describe the same set of paths. A broken parser therefore fails the test instead of
|
|
319
|
+
agreeing with itself.
|
|
320
|
+
|
|
321
|
+
That test has a history worth keeping: the first `lib/survey.js` imported a function from a module that did
|
|
322
|
+
not export it. The ESM link error made the whole plugin graph unevaluable, so neither `git_commit_push` nor
|
|
323
|
+
`/git-commit-push` registered — and the pure-logic test never touched `survey.js`. Both test files now import
|
|
324
|
+
the complete module graph explicitly, and `apply()` validates the schema with `toolDefinitionProblems()`
|
|
325
|
+
before registering.
|
|
326
|
+
|
|
327
|
+
The last two sections of `self-test.mjs` check **packaging and runtime contracts** rather than algorithms:
|
|
328
|
+
|
|
329
|
+
- the declared `dsh.bundle.patch` exists and is non-empty, inserts **exactly one** mount row (two rows mount
|
|
330
|
+
the plugin twice), uses the stable id `git-commit-push` and names this package; neither installer writes a
|
|
331
|
+
mount row of its own;
|
|
332
|
+
- **the `files` whitelist covers every relative module the entry point imports** — the classic npm
|
|
333
|
+
publishing accident, where the package installs cleanly and then fails to load;
|
|
334
|
+
- the `exports` subpaths, `locale/*.json` and `icon` the Plugins page reads (relative path, allowed type,
|
|
335
|
+
≤256 KiB);
|
|
336
|
+
- every DSH peer is `optional` (otherwise pnpm tries to install a host package into the user's profile),
|
|
337
|
+
plus `dsh.manifestVersion` and `engines.dsh`;
|
|
338
|
+
- **the settings layers and the field table agree**: the field table, the built-in defaults and the shipped
|
|
339
|
+
template cannot drift, the UI layer (row config) beats the JSON file, `pinnedIdentity` merges per key, and
|
|
340
|
+
only a parsed value that differs from the default counts as "the user set it";
|
|
341
|
+
- **the form is either published or degraded**: when `@deepseek-ai/schemastery` resolves, `Config` must be
|
|
342
|
+
built with every field volatile; when it does not, `Config` must be `undefined` and the tool must still
|
|
343
|
+
register;
|
|
344
|
+
- settings precedence (user file > shipped template) and the rule that **a malformed config must be
|
|
345
|
+
reported**;
|
|
346
|
+
- the skill definition parsed from SKILL.md satisfies the registry's `validateRuntimeSkill` rules, and
|
|
347
|
+
`apply()` on a mock host really registers the tool, the command and the skill.
|
|
348
|
+
|
|
349
|
+
Two small readers exist for exactly the syntax this project writes (the YAML patch, the skill frontmatter):
|
|
350
|
+
the package ships zero dependencies, so the test will not pull in a YAML parser to read three lines — and a
|
|
351
|
+
line it cannot read fails the test instead of being ignored.
|
|
352
|
+
|
|
353
|
+
## Layout
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
index.js plugin entry: tool definition, /git-commit-push command, skill registration, orchestration
|
|
357
|
+
cordis.patch.yml the bundle patch: the single mount row (dsh.bundle.patch points at it)
|
|
358
|
+
icon.svg Plugins page icon (package.json "icon")
|
|
359
|
+
locale/en.json Plugins page display text (meta.title / meta.description, English)
|
|
360
|
+
locale/zh.json same, Chinese
|
|
361
|
+
lib/git.js the only git layer: fixed argv, timeouts, output caps, porcelain parsing, platform probing
|
|
362
|
+
lib/analyze.js change classification + rule-based Conventional Commits + card rendering
|
|
363
|
+
lib/survey.js one repository survey: status / numstat / log / bounded diff
|
|
364
|
+
lib/config.js settings + the field table: row config > user file > template > defaults, and reporting a broken file
|
|
365
|
+
lib/schema.js the Cordis Config (schemastery): the visual form + volatile fields, degrading gracefully when the library is unreachable
|
|
366
|
+
lib/skill.js parses SKILL.md into the runtime skill definition (frontmatter included)
|
|
367
|
+
lib/profile-edit.mjs profile manifest editor shared by both installers (idempotent, keeps unknown fields, no BOM, self-verifying)
|
|
368
|
+
setup.ps1 Windows install / uninstall (path C)
|
|
369
|
+
setup.sh macOS / Linux install / uninstall (path C)
|
|
370
|
+
self-test.mjs pure logic + packaging/config/form contracts (86 checks)
|
|
371
|
+
self-test-git.mjs real-git integration (24 checks, own temporary repository)
|
|
372
|
+
capture-git-format.mjs prints raw git -z bytes (framing diagnostics)
|
|
373
|
+
e2e-check.mjs calls run() directly, to verify the commit path without restarting DSH
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Design trade-offs. The tool definition is a **hand-written object** instead of `defineTool(...)`; the only
|
|
377
|
+
host import in the runtime is a deliberate exception — `lib/schema.js` uses `createRequire` to obtain
|
|
378
|
+
`@deepseek-ai/schemastery` so the plugin can declare a `Config`, and the whole thing is wrapped in a
|
|
379
|
+
`try/catch`: when it cannot be reached, `Config === undefined` (no form) and the plugin and its tool keep
|
|
380
|
+
working. Every other module still imports only Node built-ins and relative paths, because this package may be
|
|
381
|
+
`link:`ed from outside a profile or sit inside `node_modules`, and mounting must not fail because the host's
|
|
382
|
+
module resolution does not reach it. (`@deepseek-ai/schemastery` is an ordinary `dependencies` entry, so an
|
|
383
|
+
npm install brings it; a `link:` install does not install a link target's dependencies, which makes that
|
|
384
|
+
degradation path real rather than theoretical.)
|
|
385
|
+
|
|
386
|
+
The price is writing a **real JSON Schema by hand**: `parameters` needs `type: "object"` + `properties` +
|
|
387
|
+
`required: []`, and `output.schema`'s `required` must be an **array of strings** — `defineTool`'s
|
|
388
|
+
per-property `required: true` form is compiled by `defineTool` itself, and a hand-written definition
|
|
389
|
+
carrying it is rejected **at registration time**, taking the whole plugin down.
|
|
390
|
+
`toolDefinitionProblems()` is the regression test for that rule.
|
|
391
|
+
|
|
392
|
+
`exports` carries `./package.json` and `./locale/*` besides `.`: the Plugins page resolves
|
|
393
|
+
`<specifier>/package.json` and `<specifier>/locale/en.json` through Node's module resolver
|
|
394
|
+
(`readPluginMeta`), and an exports map with only `.` makes both lookups fail with
|
|
395
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`, degrading the title to the bare module specifier.
|
|
396
|
+
|
|
397
|
+
`peerDependencies` declares only `@deepseek-ai/dsh-tools`, marked **optional**: the peer exists so DSH's
|
|
398
|
+
compatibility gate (`evaluatePluginCompatibility`, which reads `peerDependencies`) can compare the host
|
|
399
|
+
version, and `optional` guarantees pnpm never downloads a host package to satisfy it. The plugin imports
|
|
400
|
+
nothing from it.
|
|
401
|
+
|
|
402
|
+
## The DSH bundle contract (what this package had to learn)
|
|
403
|
+
|
|
404
|
+
Everything below was verified against the implementation inside the shipped `dsh` bundle
|
|
405
|
+
(`packages/boot/plugin-manager`, `packages/boot/app-boot`, `packages/boot/package-manifest`,
|
|
406
|
+
`packages/skill/skill`) rather than guessed.
|
|
407
|
+
|
|
408
|
+
1. **A bundle is a `package.json` with `dsh.bundle.patch`** — one file path or an ordered list of paths,
|
|
409
|
+
relative to the package root. For a name selected in `dsh.profile.bundles` the launcher resolves it with
|
|
410
|
+
`bundlePatchFiles` / `bundlePatchPaths` and applies that patch as one layer. When `dsh.bundle` cannot be
|
|
411
|
+
resolved it throws `profile bundle "…" declares no dsh.bundle in its package.json` and **skips the
|
|
412
|
+
layer** (recorded in `skippedBundles`) instead of failing the boot.
|
|
413
|
+
2. **The Plugins page only manages bundles.** `listBundles()` lists a selected name with no `dsh.bundle` as
|
|
414
|
+
`error.code = "not-bundle"` — the sentence the page shows. Enable/disable only change membership in
|
|
415
|
+
`dsh.profile.bundles` (the dependency stays); uninstall touches the dependency. An unselected plain
|
|
416
|
+
dependency is not listed at all.
|
|
417
|
+
3. **The compatibility check only looks at `@deepseek-ai/dsh` and `@deepseek-ai/dsh-*` peers**, compares with
|
|
418
|
+
`includePrerelease`, and takes the runtime version from `dsh-app-boot`. `peerDependenciesMeta.optional`
|
|
419
|
+
does not affect it.
|
|
420
|
+
4. **`dsh.manifestVersion` and `engines.dsh` are declarative only** — installers and loaders do not enforce
|
|
421
|
+
them — but they are the public author fields documented by `@deepseek-ai/dsh-package-manifest`, so this
|
|
422
|
+
package declares both.
|
|
423
|
+
5. **Display metadata** comes from `readPluginMeta`, which resolves `<name>/package.json`,
|
|
424
|
+
`<name>/locale/*.json` and the `icon` field (relative path, SVG/PNG/JPEG/WebP, ≤256 KiB, must stay inside
|
|
425
|
+
the package directory) through Node; `locale/en.json` is the reference file.
|
|
426
|
+
6. **Embedded skills** use `ctx.skills.register({ name, description, content, … })`: `name` must match
|
|
427
|
+
`/^[a-z0-9]+(?:-[a-z0-9]+)*$/`, `description` and `content` must be non-empty strings (re-validated on
|
|
428
|
+
load by `validateDefinition`), the registry fills in the `runtime` provider, and precedence is
|
|
429
|
+
**project > runtime > user**.
|
|
430
|
+
7. **Visual configuration means exporting a `Config` schema** (schemastery, zod-style). DSH's settings
|
|
431
|
+
service (`@deepseek-ai/dsh-settings` + `@deepseek-ai/dsh-config-editor`) derives a namespace and a form for
|
|
432
|
+
entries that declare one (`SettingsNamespaceView.autoGenerate`), and writes land in that entry's `config`
|
|
433
|
+
in the profile patch; a plugin can opt out with `settings.configure({ auto: false })`. A field marked
|
|
434
|
+
`.volatile()` only commits new values and announces `loader/volatile-update` (the Loader compares with
|
|
435
|
+
`equalExceptVolatile`) — **no remount** — while an ordinary field change remounts the row. Volatile
|
|
436
|
+
placement is strict: a fixed object path, never a dict value, array item, map key or union/lazy branch
|
|
437
|
+
(`validateVolatileSchema` throws otherwise).
|
|
438
|
+
8. **The settings page is not a general plugin settings UI**: `@deepseek-ai/dsh-settings` shows only volatile
|
|
439
|
+
fields of active, uniquely addressable entries, the **Plugin list** tab is read-only, and the
|
|
440
|
+
`pluginManager/*` RPCs only install/enable/disable/uninstall — so the one requirement for "visually
|
|
441
|
+
configurable" is that the plugin declares `Config` itself.
|
|
442
|
+
|
|
443
|
+
Two more traps worth knowing:
|
|
444
|
+
|
|
445
|
+
- Loader `insert` is append-only and does not deduplicate — the same `id` inserted twice mounts the plugin
|
|
446
|
+
twice, so there must be exactly one mount.
|
|
447
|
+
- A profile's `pnpm-workspace.yaml` normally sets `autoInstallPeers: false` and `nodeLinker: hoisted`, so
|
|
448
|
+
peers are never auto-installed and an unmet one is a warning. That is the second reason this package marks
|
|
449
|
+
its peer optional.
|
|
450
|
+
|
|
451
|
+
## Maintainers: publishing to npm
|
|
452
|
+
|
|
453
|
+
```sh
|
|
454
|
+
npm login # or NPM_TOKEN in CI
|
|
455
|
+
npm test # prepublishOnly runs it too
|
|
456
|
+
npm publish # publishConfig pins the registry to registry.npmjs.org
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
- `publishConfig.registry` is set explicitly because a machine-level `~/.npmrc` pointing at a read-only
|
|
460
|
+
mirror (npmmirror) would otherwise send `npm publish` to the wrong place.
|
|
461
|
+
- Mirrors take a while to sync after a publish, so a user installing immediately may not see the new version.
|
|
462
|
+
- Bump the version by SemVer; `dsh.manifestVersion` is the **manifest format** identifier and is unrelated
|
|
463
|
+
to the package version, so it does not move with it.
|
|
464
|
+
- When runtime behaviour changes, update `engines.dsh` and the `@deepseek-ai/dsh-tools` peer range together —
|
|
465
|
+
they decide whether the Plugins page reports an incompatibility.
|
|
466
|
+
- `@deepseek-ai/schemastery` in `dependencies` is a **real dependency** (the settings form needs it) and pnpm
|
|
467
|
+
installs it for the user. Before widening its range, check that the target DSH version still has the schema
|
|
468
|
+
API used here (`.default/.description/.min/.volatile`); otherwise the form takes the degraded path
|
|
469
|
+
(`Config === undefined` — everything still works, there is just no visual configuration).
|
|
470
|
+
|
|
471
|
+
## License
|
|
472
|
+
|
|
473
|
+
MIT © ygzhang-lab. See [LICENSE](./LICENSE).
|