Zum Hauptinhalt springen
D

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.

Installation

dsh plugin --profile web add github:daoyan2142/dsh-plugin-git-sync

README

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

version license dsh


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 .tgz travel 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)
StepMachine AMachine B
1listBundles() → third-party, installed onlydownload sync/manifest.json
2resolve each package directory, pack a package/-rooted tar.gzcompare manifest versions with local versions
3SHA-256 each tarball, split if above the host limitclassify: install / same / differ
4upload sync/packages/* then sync/manifest.json lastdownload 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 pluginManager service (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:

FieldValueNotes
PlatformGitHub or GiteeSwitching platforms switches the three fields below as a set.
Token (PAT)a personal access token for that platformStored in the DSH credentials service; the card only ever shows configured / not configured.
Repositoryowner/repo, a full URL, or git@host:owner/repo.gitPasting a URL auto-selects the matching platform. The repository must already exist.
Branchmain, master, or anything elseMust 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 repo also 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

WhatWhere
Non-secret settings<profile>/.git-sync/settings.json
Tokens~/.dsh/.credentials.yaml (refs:, written by the credentials service)
Uploaded packagessync/ in your GitHub/Gitee repository

Use it

Machine A — first push

  1. Pick the platform, paste the token, click 保存.
  2. Enter owner/repo and the branch, click 保存设置.
  3. Click 验证仓库 — it reads the repository and confirms the token, name and branch resolve.
  4. Click 导出并推送. Every installed third-party plugin is packed and uploaded; the result line reports how many packages went up.

Machine B — restore

  1. Same platform, same token (or a second token with the same access), same repository and branch.
  2. Click 拉取并对比. The card shows three groups:
    • install — not installed here;
    • same — same version, nothing to do;
    • differ — installed at another version.
  3. Click 安装全部缺失插件 to install the first group.
  4. For differ, each row shows local → remote; click 覆盖 to replace that one plugin.
  5. 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>.tgz in the repository and clear it from manifest.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:

PlatformPiece sizeWhy
GitHub768 KiBstays under the contents API inline limit
Gitee4 MiBGitee'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
    }
  }
}
FieldMeaning
versionthe version recorded when the package was packed
tgzpath of the single-file package
partsordered paths when the package was split (mutually exclusive with tgz)
sha256hash of the complete tarball, parts concatenated
bytescomplete tarball size
sourcehow the plugin was installed on machine A: registry, file or link
officialalways false; official bundles are never exported or compared

Comparison rules

RemoteLocalGroupWhat the button does
presentabsentinstalldownload, verify, installBundle
presentsame version stringsamenothing
presentdifferent versiondiffer覆盖 replaces that plugin
absentpresent—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:

EndpointPurpose
/git-sync/api/statecurrent platform, repository, branch, token presence
/git-sync/api/saveSettingspersist platform + repository + branch
/git-sync/api/saveToken · clearTokenwrite / remove the token for one platform
/git-sync/api/verifycheck that the repository is reachable with the token
/git-sync/api/exportpack and upload every third-party plugin
/git-sync/api/pulldownload the manifest and compare with local state
/git-sync/api/applydownload, verify and install a list of manifest entries

Troubleshooting

SymptomCauseFix
token not configuredthe token was saved for the other platformswitch the platform segment, save the token there
Bad credentials (GitHub)GitHub selected, Gitee token pasted; or token expired / lacks Contents writecheck the platform, regenerate the token with the right scope
HTTP 400 on exportbranch does not exist (empty repository), or a file is over the API limitinitialize the repository with a README; 1.0.0 splits large packages automatically
remote manifest not foundnothing has been pushed to that branch yetpush from machine A, or check the branch name (main vs master)
pluginManager service is unavailablea profile without the plugin manager (headless/SDK)run this on a desktop or web profile
Installed, but the plugin behaves like the old versionDSH loads plugin code at startuprestart DSH
checksum-mismatchan upload was truncated or edited in the repositorypush again from machine A
repo must be "owner/repo"you are on a build older than 1.0.0upgrade; 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.

Ähnliche Plugins