dsh-workspace-merge
No description
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 11, 2026
- Updated
- Sep 11, 2026
Introduction
dsh-workspace-merge
One DSH session, several project roots.
A DSH session is rooted at exactly one working directory — that directory is the sandbox boundary, the workspace key, and the base for every relative path. That default is right almost always, but it makes a very common layout awkward: the PC and mobile clients of one product, an iOS and a HarmonyOS app that must stay in step, a shared library and its consumers. They live in sibling directories, and "change the same file in both places" degenerates into absolute paths and repeated calls that quietly drift apart.
This bundle adds a named workspace group — an ordered set of local project
roots — and eight ws_* tools that treat one relative path across every root of
the group as a single operation, with a per-root outcome instead of one opaque
failure.
one call: ws_edit src/components/AmountInput.vue
│
┌───────────────────────┼───────────────────────┐
▼ ▼ ▼
[pc] ecam-ui [mobile] ecam-mobile [shared] lib-ui
changed · 1 replacement changed · 1 replacement not-found
Install
dsh plugin add dsh-workspace-merge
Or add it to a profile by hand: install the package under $DSH_HOME/profiles/web
and list dsh-workspace-merge in dsh.profile.bundles.
Configure
A group is a name plus roots. id is the short label every other tool uses;
path may be absolute or ~-prefixed.
// ws_group_set {group:"ecam", description:"PC + mobile", roots:[
// {id:"pc", path:"~/Desktop/JLXY/ecam-ui"},
// {id:"mobile", path:"~/Desktop/JLXY/ecam-mobile-ui"}
// ]}
That writes $DSH_HOME/workspace-groups.json:
{
"version": 1,
"groups": {
"ecam": {
"description": "PC + mobile",
"roots": [
{ "id": "pc", "path": "/Users/you/Desktop/JLXY/ecam-ui" },
{ "id": "mobile", "path": "/Users/you/Desktop/JLXY/ecam-mobile-ui" }
]
}
}
}
The file is plain JSON — hand-edit it if you prefer.
Group selection. A call may pass group explicitly. Otherwise the group
whose root contains the session directory wins (the longest matching root breaks
a tie between nested roots); with no match, a single defined group is
unambiguous, and any other count requires an explicit name.
Tools
| Tool | Purpose |
|---|---|
ws_groups | List groups, their root ids, and the active group for this session. |
ws_group_set / ws_group_remove | Maintain the registry (project files are never touched). |
ws_ls | List one relative directory across every root — locate the same component everywhere at once. |
ws_read | Read the same relative path from every root and return each copy. |
ws_diff | Line-diff the copies (first root is the baseline) before editing anything. |
ws_edit | Apply ONE literal replacement to every root, per-root outcome. |
ws_write | Write the SAME full content to every root — new shared file, or force copies back in sync. |
ws_edit follows the built-in edit rule: old_string must occur exactly once
unless replace_all is set. ws_edit and ws_write accept dry_run to preview,
and roots: ["pc"] to narrow a call to one project. Per-root statuses are
explicit — changed / unchanged / not-found / ambiguous / missing /
invalid / denied — so a partial application is visible, never silent.
Sandbox behaviour
Every mutation resolves the session's sandboxPolicy first, and this bundle does
not widen it:
danger-full-access— writes resolve unfenced, as everywhere else.workspace-write— each target is canonicalized (a missing file is re-anchored under its nearest existing ancestor, so a symlinked parent cannot smuggle the write out) and must fall inside the policy's writable roots.read-only— all mutations are refused.
A target outside the standing roots is refused with the official
[sandbox: file access denied under … mode] marker plus the escalation hint, and
can be retried once with sandbox_permissions + justification, exactly like the
built-in write and edit tools. The group registry itself (ws_group_set,
ws_group_remove) obeys the same policy — a read-only session cannot quietly
reconfigure groups.
Notes and limits
- A group is not a monorepo. It changes where the
ws_*tools point; it does not change the session's working directory, so the built-inread/edit/write/bashstill resolve against the session root only. Use a group when one change must land in several projects in the same step. - Writes go through the harness filesystem service (
ctx.fs): the pre-write version is registered with the officialfs/observedevent, so the fs-observation policy issuesreplaceIfVersionrather than refusing the write asFS_NOT_OBSERVED, and other plugins can veto throughfs/write-intent. When that service is not mounted, the plainnode:fspath is used after the same policy decision — and the result says so (via: node:fs (fs service failed: …)) instead of hiding it, so an integration problem is visible rather than silent. ws_lsskips.gitandnode_modules, and caps output at 200 paths per root.ws_diffaligns lines up to 4000 per side; larger files get a size summary instead of a diff.
Test
npm test
31 assertions over a throwaway DSH home and two throwaway roots: registration,
registry CRUD, group selection, read/diff/list, dry-run, per-root mutation
statuses, path-escape refusal, root-id validation, and the sandbox cases
(danger-full-access, workspace-write deny, read-only deny, registry-write
policy). The test links the harness's own @deepseek-ai/dsh-tools and
@deepseek-ai/dsh-sandbox from the DevEco/DSH install (see node_modules), so
npm test expects a local DSH installation.
The stub context reproduces two cordis rules that cost this bundle a real boot each, and fails loudly if a future edit breaks them again:
- a service not declared in
injectis not a readable property —ctx.systemPromptthrows rather than returning undefined, which takes the whole row (and every tool registered before the throw) down with it, silently; optional services must be read withctx.get(name); - section placement is centrally allocated —
getSectionOrder()returns undefined for a name the harness does not know, and a section with a non-finite order throws. This bundle uses theTOOLS_SDKplacement with a numeric fallback.
Diagnostics
WSM_DEBUG=1 records the plugin's startup phases (module evaluated → apply →
tools service available → registration result) to stderr and
<tmpdir>/dsh-workspace-merge-debug.log. Use it when a row is in the composed
profile but its tools never appear in a session.
License
MIT © 宋朝阳