- 홈
- 플러그인
- 개발 및 플러그인 도구
- dsh-plugin-git-sync
dsh-plugin-git-sync
daoyan2142/dsh-plugin-git-sync
Packs the third-party plugins installed in a DSH profile into tarballs and pushes them to a GitHub or Gitee repository; another machine pulls the manifest, compares versions and installs only what is missing or out of date. Needs a personal access token with write access to that repository (kept in the DSH credentials service); the tarballs contain each plugin's installed source, so use a private repository.
설치
dsh plugin --profile web add github:daoyan2142/dsh-plugin-git-syncREADME
dsh-plugin-git-sync
Carry your plugin set to every machine. Pack the third-party plugins installed in a DSH profile
into .tgz files, push them to a GitHub or Gitee repository, then pull, compare and install them on
another machine.
中文文档 · Changelog · Manifest schema
Why
Plugins installed with dsh plugin add live inside one profile's node_modules. Nothing about them
is portable: a new machine starts from an empty profile, and rebuilding it means remembering every
plugin, every version and every install spec by hand.
This plugin treats the installed plugin set as data worth carrying. Each third-party plugin is
packed with its source, stored under sync/ in a repository you already control, and restored on
the other machine with a version comparison and a SHA-256 check.
Because the repository carries the tarballs and not just a list of names:
- Machine B does not depend on the original registry still serving that version.
- Plugins installed from a local folder or a downloaded
.tgztravel just as well — their source goes into the package. - Every package is hashed on push and verified again before install.
How it works
Machine A Git repository Machine B
───────────────────────────────────── ───────────────────────────── ─────────────────────────────
pluginManager.listBundles()
└─ pack each package dir ──────────► sync/packages/*.tgz ─────────► download
├─ sha256 ────────────────────► sync/manifest.json ─────────► compare with local versions
└─ upload (chunked if large) └─ sha256 ─ installBundle(tgz)
| Step | Machine A | Machine B |
|---|---|---|
| 1 | listBundles() → third-party, installed only | download sync/manifest.json |
| 2 | resolve each package directory, pack a package/-rooted tar.gz | compare manifest versions with local versions |
| 3 | SHA-256 each tarball, split if above the host limit | classify: install / same / differ |
| 4 | upload sync/packages/* then sync/manifest.json last | download the tarball (or its parts), verify SHA-256 |
| 5 | — | pluginManager.installBundle(tgz), restart DSH |
The manifest is written last, so a repository whose upload was interrupted never advertises packages it does not have.
Requirements
- DSH 0.2.x with the
pluginManagerservice (listBundles,installBundle). Developed and tested on 0.2.2-rc.1. - Node.js >= 22.12.
- A GitHub or Gitee account and a repository. Private is recommended — see Security.
Install
From the market (once listed):
Plugins → Discover → search "Git Sync" → Install
From npm, or from a release tarball:
# from npm (once published)
dsh plugin --profile web add dsh-plugin-git-sync
# from a .tgz you built or downloaded from Releases
dsh plugin --profile web add ./dsh-plugin-git-sync-1.0.0.tgz
The package is plain ESM with no build step and no runtime dependencies, so the tarball installs exactly as fast as the npm package.
After installing, DSH must pick up the new Host module — restart it, or install through the market, which hot-mounts the plugin for you.
Configuration
Open Settings → Git 插件同步. The card has four things to fill in:
| Field | Value | Notes |
|---|---|---|
| Platform | GitHub or Gitee | Switching platforms switches the three fields below as a set. |
| Token (PAT) | a personal access token for that platform | Stored in the DSH credentials service; the card only ever shows configured / not configured. |
| Repository | owner/repo, a full URL, or git@host:owner/repo.git | Pasting a URL auto-selects the matching platform. The repository must already exist. |
| Branch | main, master, or anything else | Must already exist: an empty repository has no branch to commit into. Initialize it with a README first. |
One set of settings per platform
GitHub and Gitee each keep their own repository, branch and token. Switching the platform segment never overwrites the other side, so a Gitee mirror and a GitHub repository can coexist:
settings.json
platform: "gitee" ← the platform the card currently shows
profiles:
github: { repo: "…", branch: "main" }
gitee: { repo: "…", branch: "master" }
Tokens
Create a token with write access to contents of that one repository:
- GitHub — Fine-grained token → Repository access: only select repositories → Permissions:
Contents: Read and write. A classic token with
repoalso works. - Gitee — 设置 → 私人令牌 → scope projects.
Click 保存 next to the token field; it is written through the DSH credentials service under the
reference GIT_SYNC_GITHUB_TOKEN or GIT_SYNC_GITEE_TOKEN, never into settings.json, never into
the repository, and never into an API response. 清除 removes it.
Where the settings live
| What | Where |
|---|---|
| Non-secret settings | <profile>/.git-sync/settings.json |
| Tokens | ~/.dsh/.credentials.yaml (refs:, written by the credentials service) |
| Uploaded packages | sync/ in your GitHub/Gitee repository |
Use it
Machine A — first push
- Pick the platform, paste the token, click 保存.
- Enter
owner/repoand the branch, click 保存设置. - Click 验证仓库 — it reads the repository and confirms the token, name and branch resolve.
- Click 导出并推送. Every installed third-party plugin is packed and uploaded; the result line reports how many packages went up.
Machine B — restore
- Same platform, same token (or a second token with the same access), same repository and branch.
- Click 拉取并对比. The card shows three groups:
- install — not installed here;
- same — same version, nothing to do;
- differ — installed at another version.
- Click 安装全部缺失插件 to install the first group.
- For differ, each row shows
local → remote; click 覆盖 to replace that one plugin. - Restart DSH so the newly installed Host modules load.
Everyday update flow
Bump or add a plugin on machine A, push again, then 拉取并对比 on machine B and overwrite the rows whose version moved. Export is additive and idempotent: every package is written again on each push (same path, same bytes, same hash when nothing changed), and the manifest is rewritten last. The plugin never reads the remote repository first to work out what changed.
What export does not do
- It never deletes. A plugin removed on machine A stays in the repository and is still offered to
machine B. Remove it there by hand, or delete
sync/packages/<name>-<version>.tgzin the repository and clear it frommanifest.json. - It never touches the repository outside
sync/.
The repository layout
sync/
├─ manifest.json ← the comparison file; written last
└─ packages/
├─ some-plugin-1.2.0.tgz
├─ another-plugin-0.4.1.tgz
├─ @scope/name → scope__name-1.0.0.tgz
└─ big-plugin-3.4.5.tgz.part001 … part003 ← large packages are split
A scoped package's / becomes __ so that every file stays in one flat directory.
Large packages
The GitHub and Gitee contents APIs reject very large single files, so a package above the limit is
uploaded as .partNNN pieces and listed under parts in the manifest:
| Platform | Piece size | Why |
|---|---|---|
| GitHub | 768 KiB | stays under the contents API inline limit |
| Gitee | 4 MiB | Gitee's per-file limit |
On install the pieces are concatenated in order, checked against the manifest's sha256, and only
then handed to installBundle — a truncated upload is refused rather than installed.
The manifest
sync/manifest.json is the only file read during 拉取并对比.
{
"schemaVersion": 1,
"platform": "github",
"branch": "main",
"generatedAt": "2026-01-01T00:00:00.000Z",
"packages": {
"some-plugin": {
"version": "1.2.0",
"official": false,
"tgz": "sync/packages/some-plugin-1.2.0.tgz",
"sha256": "9f2c…",
"source": "registry"
},
"big-plugin": {
"version": "3.4.5",
"official": false,
"sha256": "41ab…",
"parts": [
"sync/packages/big-plugin-3.4.5.tgz.part001",
"sync/packages/big-plugin-3.4.5.tgz.part002",
"sync/packages/big-plugin-3.4.5.tgz.part003"
],
"bytes": 2097152
}
}
}
| Field | Meaning |
|---|---|
version | the version recorded when the package was packed |
tgz | path of the single-file package |
parts | ordered paths when the package was split (mutually exclusive with tgz) |
sha256 | hash of the complete tarball, parts concatenated |
bytes | complete tarball size |
source | how the plugin was installed on machine A: registry, file or link |
official | always false; official bundles are never exported or compared |
Comparison rules
| Remote | Local | Group | What the button does |
|---|---|---|---|
| present | absent | install | download, verify, installBundle |
| present | same version string | same | nothing |
| present | different version | differ | 覆盖 replaces that plugin |
| absent | present | — | left alone; the local extra is never removed |
Versions are compared as exact strings, not as semver ranges: 1.2.0 and 1.2.0 match, everything
else lands in differ. Official bundles are filtered out at export, so the repository only ever
contains third-party plugins.
Security
- The repository holds other people's plugin source. Keep it private, and mind the license of each plugin you push. Exporting copies the package directory as it is installed on your machine.
- The token never leaves the credentials service. It is resolved in the Host process at request
time and used for one API call; it is not written to
settings.json, not rendered in the UI, and not returned by any endpoint. - Requests are admitted by DSH's own connection gate (
connection.requestRejection), the same one that guards the built-in/api— a browser without an admitted connection cannot call the plugin's endpoints. - Nothing is installed without a checksum match, and nothing is installed without a click.
Host API surface, all POST on the same origin:
| Endpoint | Purpose |
|---|---|
/git-sync/api/state | current platform, repository, branch, token presence |
/git-sync/api/saveSettings | persist platform + repository + branch |
/git-sync/api/saveToken · clearToken | write / remove the token for one platform |
/git-sync/api/verify | check that the repository is reachable with the token |
/git-sync/api/export | pack and upload every third-party plugin |
/git-sync/api/pull | download the manifest and compare with local state |
/git-sync/api/apply | download, verify and install a list of manifest entries |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
token not configured | the token was saved for the other platform | switch the platform segment, save the token there |
Bad credentials (GitHub) | GitHub selected, Gitee token pasted; or token expired / lacks Contents write | check the platform, regenerate the token with the right scope |
HTTP 400 on export | branch does not exist (empty repository), or a file is over the API limit | initialize the repository with a README; 1.0.0 splits large packages automatically |
remote manifest not found | nothing has been pushed to that branch yet | push from machine A, or check the branch name (main vs master) |
pluginManager service is unavailable | a profile without the plugin manager (headless/SDK) | run this on a desktop or web profile |
| Installed, but the plugin behaves like the old version | DSH loads plugin code at startup | restart DSH |
checksum-mismatch | an upload was truncated or edited in the repository | push again from machine A |
repo must be "owner/repo" | you are on a build older than 1.0.0 | upgrade; URLs and SSH remotes are accepted |
Limits
- Installing or replacing plugin code requires a DSH restart to take effect.
- Export uploads one file per request, and every request is a commit: a repository with large packages gains many commits. GitHub's Git Data API could batch them; Gitee has no equivalent.
- Removals are not propagated (see What export does not do).
- The settings card is currently Chinese-only; the locale files are in place and the UI text is being moved onto them.
Version history
1.0.0 — first public release (2026-11-09).
- Export: every installed third-party plugin packed with its source and pushed to GitHub or Gitee.
- Pull & compare: install / same / differ, with per-row overwrite.
- Chunked upload for large packages (768 KiB per piece on GitHub, 4 MiB on Gitee), SHA-256 verified before every install.
- Per-platform repository, branch and token — switching platforms never overwrites the other side.
- Tokens handled by the DSH credentials service; the Host API sits behind the same connection gate as
/api.
Full notes, plus the earlier 0.x development builds: CHANGELOG.md.
Development
# install the working copy into a profile (no build step involved)
dsh plugin --profile web add .
# or pack it first and install the tarball
npm pack # → dsh-plugin-git-sync-1.0.0.tgz
dsh plugin --profile web add ./dsh-plugin-git-sync-1.0.0.tgz
The Host half is index.js plus lib/; the settings card is client.js (a plain module, loaded by
the client module loader). There is no build step, no bundler and no runtime dependency.
License
MIT.