TwinsEarth/dsh-windows2macos ↗★ 1

@twinsearth/w2m-dsh-plugin

跨网络和区域在多台设备上同步驱动同一个项目 适合需要在Windows和macOS多台设备间协同开发同一项目的用户。

包名
@twinsearth/w2m-dsh-plugin
兼容性
待验证
Harness 依赖范围
*
版本
0.3.8
许可证
MIT
最近更新
2026年10月8日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:TwinsEarth/dsh-windows2macos

DSH: Windows2MacOS

One DeepSeek account. One instruction. Every machine you own runs the same project.

English | 中文

CI DSH plugin version dependencies license platforms


The problem

You have a Windows desktop and a Mac laptop. You are working on one project. You want to say "run the test suite" once, and have both machines run it — then see whether they agree.

DeepSeek Harness cannot do this today, and not by accident:

  • one dsh process serves one machine — a profile is a local pnpm workspace;
  • its transport is loopback only (the web server accepts 127.0.0.1/0.0.0.0 and the CLI refuses --host 0.0.0.0, because it ships no TLS or origin policy);
  • the official agent-team feature is explicit that it does not support teammates with separate working directories or several processes coordinating over one team.

So the cross-machine capability has to be built. This is that build.

What you get

        you, on any machine, talking to DSH
                       │
                       ▼
   ┌──────────────────────────────────┐        ┌──────────────────────────┐
   │  Windows · DSH session           │        │  macOS · DSH session     │
   │   w2m plugin (8 tools)           │        │   w2m plugin (8 tools)   │
   │        │                         │        │        │                 │
   │   Localside agent                │        │   Localside agent        │
   │    · three anchors               │        │    · three anchors       │
   │    · local spool                 │        │    · local spool         │
   │    · executes argv               │        │    · executes argv       │
   └──────────┬───────────────────────┘        └──────────┬───────────────┘
              │  outbound POST + SSE                      │
              ▼                                           ▼
        ┌──────────────────────────────────────────────────────────┐
        │  Rabbit relay — devices, tasks, leases, six-state report │
        └──────────────────────────────────────────────────────────┘

Both machines only ever make outbound connections. No public IP, no port forwarding, no SSH server on Windows (which, measured, is not installed by default — ssh.exe exists, sshd does not).

Two coordination modes

ModeWhat it doesWhen to use
replicateevery machine runs the same command; results compared side by side"does this change pass on both platforms?"
splitwork divided by index (explicit or modulo sharding)one large test suite, several machines

Six aggregation states

Not four — because "the environments differ" and "the results differ" are different findings, and collapsing them turns noise into bug reports.

StateMeaning
consistentevery machine agreed on every comparable field
divergentmachines disagreed — the report names the field and each machine's value
divergent-platformonly toolchain/platform differ, and the difference is confined to stdout text — expected
failedevery machine failed
partialsome succeeded, some failed
unverifiablean anchor did not match, so comparison is refused

Install

This package can run three ways, and all three start the same binaries.

node bin/w2m-rabbit.mjs ...        # from a clone (what the examples below use)
npx @twinsearth/w2m-dsh-plugin ... # if it is on a registry
w2m-rabbit ...                     # once installed, via its bin entry

The examples use the clone form because that is what is verified in this repository. Nothing needs to be installed to use the relay or the agent: they are plain Node scripts with no runtime dependencies.

1. The relay (once, on any always-on machine)

node bin/w2m-rabbit.mjs --host 0.0.0.0 --port 8787 --state ~/.dsh/xclient/rabbit

It prints a pairing code. Pair the first machine, and the relay immediately prints a new code — so you can pair the second machine without restarting anything. (For scripted onboarding, start it with pairingCodeReusable: true and one code stays valid for its whole TTL.)

2. Localside (on every machine that should run the project)

node bin/w2m-localside.mjs \
  --rabbit http://:8787 \
  --pair PAIR-XXXXXXXX \
  --project /path/to/your/project \
  --name win-desktop \
  --state ~/.dsh/xclient/localside-win

--allowed-commands is a default-deny whitelist as a JSON array. Nothing runs unless it matches a prefix:

  --allowed-commands '["node --test","git status --porcelain"]'

⚠️ v0.0.1 note: Localside is verified as a standalone process. Running it in-process from the plugin (autoStartAgent: true) is implemented but has not been exercised end-to-end in this release — start it as its own process.

3. The DSH plugin

dsh plugin --profile 
 add @twinsearth/w2m-dsh-plugin@0.0.1

Then give it the relay URL in that profile's cordis.patch.yml:

- id: w2m
  config:
    rabbitUrl: http://127.0.0.1:8787
    stateDir: !!js (process.env.DSH_HOME + '/xclient')
    machineName: win-desktop

Restart DSH. You should see eight tools: w2m_devices, w2m_run, w2m_wait, w2m_report, w2m_status, w2m_update, w2m_history, w2m_stats.

dsh may not be on your PATH. On a packaged install it lives under resources/runtime/cli/bin/. If dsh is not found, call it by path.

No npm package yet? Install straight from the release tarball — it needs no registry at all:

dsh plugin --profile 
 add \
  https://github.com/TwinsEarth/dsh-windows2macos/releases/download/v0.0.1/twinsearth-w2m-dsh-plugin-0.0.1.tgz

Use

Then just talk to DSH:

List my machines, then run node --test on all of them at the same commit and tell me whether the output matches.

The model calls w2m_devices → w2m_run → w2m_wait → w2m_report and hands you the comparison.

Why you can trust the comparison

A commit SHA alone is not enough. Measured: on a dirty working tree, git checkout --detach exits 0 and keeps the local changes — HEAD matches the base while the working tree does not. Compare on SHA alone and you will report a logic difference that is really a "these are not the same files" difference.

So every result carries three anchors:

AnchorWhat it proves
base_commitboth machines started from the same commit
pre_tree_fingerprintboth had the same working-tree bytes, including untracked files
command_hashboth ran the same argv, in the same shell mode, in the same relative cwd

The fingerprint algorithm is git-temp-index-tree/v1: a throwaway index file (git read-tree → git add -A → git write-tree) that never touches your working tree. Each call gets its own temporary index — sharing one GIT_INDEX_FILE across processes makes them fail with idx.lock: File exists.

git stash create is not used: measured, it drops untracked files and returns an empty string in both the "clean" and "only untracked files" cases, so the two are indistinguishable.

Prerequisite: put this in your project root, or the two machines will never fingerprint alike:

* text=auto eol=lf

Measured: with that line, a core.autocrlf=true checkout and a core.autocrlf=false checkout produce byte-identical working trees. Without it, one is CRLF and the other LF.

Across networks and regions

New in 0.1.2. Before this version the relay only worked at the root of a host: endpoints were built with new URL('/v1/stream', rabbitUrl), which discards a path prefix, so anything mounted under /w2m answered 404 to everything. Sub-path deployment now works, and sending work requires its own credential. See CHANGELOG.

Machines only ever make outbound connections, so the relay can be anywhere both sides can reach. Three shapes are supported and documented end to end in docs/DEPLOY.md:

ShapeTLS terminated byrabbitUrl looks like
Tailscale / WireGuard (recommended)the network — WireGuard encryptshttp://100.x.y.z:8787
Public VPS + domainthe relay (--tls-cert/--tls-key) or a proxyhttps://w2m.example.com
Tunnel (Cloudflare Tunnel, ngrok)the tunnel servicehttps://.example.com or with a sub-path

Plain http:// over a Tailscale address is not a mistake. WireGuard already provides end-to-end encryption and authenticates both peers; adding TLS on top would add certificate plumbing without adding a property you do not already have. That is why shape A is the recommended default rather than a compromise.

If your proxy or tunnel mounts the relay under a prefix — https://host/w2m/ — then start it with --base-path /w2m and put the same prefix in rabbitUrl. Sub-path deployment works because URLs are joined by concatenation; see CHANGELOG for the bug that made this impossible before 0.1.2.

Two credentials, on purpose

CredentialHeld byAuthorises
device_tokeneach machinetaking work, reporting results
operator_tokenonly yousending work (POST /v1/task)

A machine that can be told what to do should not automatically be able to tell the others. The relay writes the operator token to /operator-token.txt on first start and prints it once; the plugin takes it as operatorToken. Starting the relay with --operator-token '' removes the requirement — it warns loudly, and that is only appropriate for a trusted LAN.

Surviving a restart

devices.json plus ledger.jsonl mean a restarted relay keeps its paired machines and its task history, which matters once the relay lives on a VPS you will eventually reboot. A corrupt ledger line is skipped and reported; a corrupt device file starts empty and says so rather than silently forgetting every machine. --no-persist restores in-memory behaviour.

Staying up to date

New in 0.2.3. A machine running this plugin checks its own GitHub releases at 00:00, 03:00 and 05:00 Beijing time and installs a newer version if there is one. This is off by default — an updater that replaces the installed plugin is opt-in.

Turn it on in the profile patch:

# ~/.dsh/profiles/
/cordis.patch.yml
- insert:
    - id: w2m-tools
      name: '@twinsearth/w2m-dsh-plugin/tools'
      config:
        rabbitUrl: 'http://100.x.y.z:8787'
        autoUpdate: true          # default false
        # autoUpdateTimes: ['00:00:00', '03:00:00', '05:00:00']   # Beijing wall clock
        # autoUpdateTimeZone: Asia/Shanghai                       # default
        # autoUpdateDryRun: true                                  # verify, install nothing
        # updateRepo: TwinsEarth/dsh-windows2macos                # default

Ask it what it is doing from any session with w2m_update (action: "status" to inspect, action: "check" to run one cycle now).

What it does and does not do:

  • Compares the newest GitHub release against the version it was built as. It installs only a strictly newer, non-prerelease version — a downgrade is worse than a missed update, and a release candidate outranks the release it precedes by SemVer, so the gate is explicit.
  • Downloads the tarball and verifies the SHA-256 published in that release's SHA256SUMS before anything is written. A mismatch installs nothing.
  • Backs up the profile's package.json and pnpm-lock.yaml, then installs through dsh plugin --profile add (falling back to the runtime's own pnpm). If the install fails, the manifest is restored byte-for-byte.
  • It does not restart DSH. The new version loads on the next start; the running process keeps the code it loaded. w2m_status reports restart_required once an install has happened, and the installed tarball lives in /.w2m-update/ so the dependency does not dangle.
  • It does not hot-swap the running plugin. Nothing may rewrite the module a live Cordis container already loaded, so claiming otherwise would be a lie in the status output.

Honest limits.

  • A missed slot runs once on the next start if it was missed by less than 90 minutes (a machine asleep across one slot). Wider than that and the check waits for the next slot rather than firing at an arbitrary hour.
  • If pnpm fails after it has already changed node_modules, restoring the manifest is not enough to guarantee DSH still starts. The result then carries rollbackComplete: false and a reconciliation command; w2m_status surfaces it as reconciliation_needed. This is the one state that needs a human.
  • A negative result is never reported as success: a GitHub lookup that fails is recorded as an error, not as "already up to date", because those two look identical in a log and only one of them is a problem.

Security

Read this before exposing the relay to anything.

  • Admission is a single-use, rotating pairing code, then a per-device bearer token stored in device.json (mode 0600). Pairing attempts are rate limited (5 per IP per minute by default, successes included).
  • Sending work needs the operator token, not a device token.
  • --trust-proxy is off by default and should stay off unless the relay really is behind a proxy you control: it makes the relay believe X-Forwarded-For, and believing that header without a proxy in front lets any client forge its address and walk past the rate limit.
  • The relay holds no model credentials and no working copy. It can forge tasks, which is exactly why the machine-side whitelist is the load-bearing control.
  • Commands are never shell strings. argv arrays are passed to spawn(cmd, args, { shell: false }), so nothing a caller types can be reinterpreted as shell syntax.
  • Default-deny whitelist. A machine runs nothing that does not match --allowed-commands.
  • Read-only by default. replicate tasks run with write: false; there is no code path that writes to your project in that mode.
  • TLS is your choice, but a deliberate one. Shape A gets it from WireGuard; shapes B and C get it from the relay's --tls-cert/--tls-key or from the proxy. Running shape B or C over plain http:// would put the operator token and every result on the wire in clear text — do not.
  • The plugin does not read .credentials.yaml.

Verified, and not verified

This project's rule is that claims carry their evidence.

The same suites run on three platforms in CI — see the badge above, or .github/workflows/ci.yml. The matrix is windows-latest × node 20/22, macos-latest × node 22, ubuntu-latest × node 22. That matters here more than in most projects: a Windows-and-macOS coordination tool whose macOS half had never been executed would be a claim, not a product.

SuiteTestsWhat it covers
relay91pairing, auth, SSE with ready-first and seq replay, leases with heartbeat renewal and expiry, dedupe, all six aggregation states, report generation, operation token, rate limiting, persistence and restart reco