dsh-wsl-workspace 0.2.0 → 0.2.2
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/README.md +7 -3
- package/README.zh.md +6 -2
- package/image-3.png +0 -0
- package/lib/index.js +49 -19
- package/lib/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +43 -23
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-wsl-workspace
|
|
2
2
|
|
|
3
3
|
[中文](README.zh.md)
|
|
4
|
-
|
|
5
|
-
Add a WSL workspace from the DeepSeek Harness web GUI and run the whole agent session — bash commands and file reads/writes — inside a local WSL distribution with Linux paths. Nothing needs to be installed inside WSL.
|
|
4
|
+

|
|
5
|
+
Add a WSL workspace from the DeepSeek Harness web GUI and run the whole agent session — bash commands and file reads/writes — inside a local WSL distribution with Linux paths. Nothing needs to be installed inside WSL. The session can reach both WSL and Windows at the same time: bash commands run inside the WSL distribution, while Windows files stay accessible via `/mnt/<drive>` (for example `/mnt/c/Users/...`).
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ After restarting `dsh web`, a W button appears beside Settings at the sidebar fo
|
|
|
25
25
|
|
|
26
26
|
Click the W button beside Settings at the sidebar foot to open the "Add WSL workspace" dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example `/home/me/proj`) — use the Check button to verify the path exists before creating the workspace. The dialog follows the DeepSeek Harness UI language. The username field is optional: leave it empty to run commands as the distribution's default user, or name a Linux user of that distribution to run the session as that user instead (equivalent to `wsl.exe -u <username>`). The username only changes the bash tool's run identity — the file tools go through the Windows-side WSL share and are unaffected. Each workspace's username is kept in `<dshHome>/wsl-workspaces.json`; delete the entry (or recreate the workspace from the dialog) to return to the default user.
|
|
27
27
|
|
|
28
|
-
Click "Create & open" to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and `read`/`write`/`edit` operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically (the WSL variant entries in the picker are
|
|
28
|
+
Click "Create & open" to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and `read`/`write`/`edit` operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically (the WSL variant entries in the picker are bilingual, e.g. `WSL · Standard mode(标准模式)`), and Windows files stay reachable from inside the session under `/mnt/<drive>` (for example `/mnt/c/Users/...`).
|
|
29
29
|

|
|
30
30
|
## Behavior notes
|
|
31
31
|
|
|
@@ -41,3 +41,7 @@ MIT — see [LICENSE](LICENSE) and [NOTICE](NOTICE). The NOTICE precisely lists:
|
|
|
41
41
|
- **Design references (no source copied)**: [dsh-bash-terminal](https://github.com/MAXeaglet/dsh-bash-terminal) (MIT, wsl argv / WSLENV approach), [dsh-side-panel](https://github.com/ccq1/dsh-side-panel) (BSD-3-Clause, host-route pattern), [vpshub](https://github.com/Sdongmaker/vpshub) (MIT, roadmap reference).
|
|
42
42
|
|
|
43
43
|
Keep `LICENSE` and `NOTICE` when redistributing.
|
|
44
|
+
|
|
45
|
+
## Acknowledgments
|
|
46
|
+
|
|
47
|
+
Special thanks to [dsh-deep-whale](https://github.com/Small-tailqwq/dsh-deep-whale) (DSH Web 鲸鱼娘 skin series · 深海女仆工坊 maid-atelier, CC BY-NC-SA 4.0): the whale girl skin plugin brings a full set of adorable skins to the DeepSeek Harness Web UI and makes daily use of DSH a warmer experience.
|
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md)
|
|
4
4
|

|
|
5
|
-
在 DeepSeek Harness Web GUI 中「添加 WSL 工作区」:让 agent 会话的 bash 命令与文件读写都运行在本机 WSL 发行版里,路径均为 Linux 形式,WSL
|
|
5
|
+
在 DeepSeek Harness Web GUI 中「添加 WSL 工作区」:让 agent 会话的 bash 命令与文件读写都运行在本机 WSL 发行版里,路径均为 Linux 形式,WSL 内无需安装任何工具链。会话可同时访问 WSL 与 Windows 两个系统——bash 命令在 WSL 发行版内执行,Windows 文件随时可通过 `/mnt/<drive>`(如 `/mnt/c/Users/...`)访问。
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
@@ -24,7 +24,7 @@ dsh plugin --profile web add D:\path\to\dsh-wsl-workspace
|
|
|
24
24
|
## 使用
|
|
25
25
|
点侧栏底部 Settings 旁的 W 按钮,打开「添加 WSL 工作区」对话框。先从下拉框选择一个发行版,再浏览目录树或直接输入 Linux 绝对路径(如 `/home/me/proj`),可以点「检查」确认路径存在。对话框文案跟随 DSH 界面语言。用户名是可选项:留空则以该发行版的默认用户运行,填写该发行版里的某个 Linux 用户名则以该用户运行(等价于 `wsl.exe -u <用户名>`)。用户名只影响 bash 命令的运行身份,文件工具通过 Windows 侧的 WSL 共享访问、不受其影响;每个工作区填写的用户名保存在 `<dshHome>/wsl-workspaces.json`,删除对应条目(或重开对话框重建工作区)即可恢复默认用户。
|
|
26
26
|
|
|
27
|
-
点「创建并打开」后,新会话随即运行在 WSL:`bash` 工具在所选发行版内执行命令,`read`/`write`/`edit` 读写 WSL 文件,模型看到的所有路径都是 Linux 形式。模式选择器照常可用——标准、PTC、极简、创造都会自动落到对应的 WSL 变体(选择器里的 WSL
|
|
27
|
+
点「创建并打开」后,新会话随即运行在 WSL:`bash` 工具在所选发行版内执行命令,`read`/`write`/`edit` 读写 WSL 文件,模型看到的所有路径都是 Linux 形式。模式选择器照常可用——标准、PTC、极简、创造都会自动落到对应的 WSL 变体(选择器里的 WSL 变体条目为中英双语,如 `WSL · Standard mode(标准模式)`);会话内仍可通过 `/mnt/<drive>`(如 `/mnt/c/Users/...`)访问 Windows 文件。
|
|
28
28
|

|
|
29
29
|
## 行为与权限说明
|
|
30
30
|
|
|
@@ -40,3 +40,7 @@ MIT,详见 [LICENSE](LICENSE) 与 [NOTICE](NOTICE),NOTICE 精确列明:
|
|
|
40
40
|
- **设计参考(未复制源码)**:[dsh-bash-terminal](https://github.com/MAXeaglet/dsh-bash-terminal)(MIT,wsl argv/WSLENV 思路)、[dsh-side-panel](https://github.com/ccq1/dsh-side-panel)(BSD-3-Clause,Host 路由模式)、[vpshub](https://github.com/Sdongmaker/vpshub)(MIT,路线图参考)。
|
|
41
41
|
|
|
42
42
|
发布/再分发时请保留 LICENSE 与 NOTICE。
|
|
43
|
+
|
|
44
|
+
## 致谢
|
|
45
|
+
|
|
46
|
+
特别感谢 [dsh-deep-whale](https://github.com/Small-tailqwq/dsh-deep-whale)(DSH Web 鲸鱼娘皮肤系列 · 深海女仆工坊 maid-atelier,CC BY-NC-SA 4.0):鲸鱼娘皮肤插件为 DeepSeek Harness Web 界面带来了一整套可爱的皮肤,让 DSH 的日常使用更有温度。
|
package/image-3.png
ADDED
|
Binary file
|
package/lib/index.js
CHANGED
|
@@ -207,21 +207,50 @@ function variantIdFor(sourceId) {
|
|
|
207
207
|
/** The HTTP route this plugin serves (a relative, same-origin path). */
|
|
208
208
|
const DEFAULT_ROUTE = "/wsl-workspace/api";
|
|
209
209
|
/**
|
|
210
|
-
*
|
|
211
|
-
* built-in copy
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
210
|
+
* Bilingual display labels for the shipped source modes, matching the app's
|
|
211
|
+
* own built-in copy in each language — note the `code` preset is "PTC 模式"
|
|
212
|
+
* in the Chinese copy but "Code mode" in English. The DSH picker localizes
|
|
213
|
+
* only the four built-in ids itself; `wsl-*` variant ids render the
|
|
214
|
+
* preset.yml text verbatim, so the plugin writes one bilingual string so
|
|
215
|
+
* both locales can identify each variant. Custom presets keep their own
|
|
216
|
+
* name.
|
|
215
217
|
*/
|
|
216
|
-
const
|
|
217
|
-
standard:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
const MODE_DISPLAY_LABELS = {
|
|
219
|
+
standard: {
|
|
220
|
+
en: "Standard mode",
|
|
221
|
+
zh: "标准模式"
|
|
222
|
+
},
|
|
223
|
+
code: {
|
|
224
|
+
en: "Code mode",
|
|
225
|
+
zh: "PTC 模式"
|
|
226
|
+
},
|
|
227
|
+
minimal: {
|
|
228
|
+
en: "Minimal mode",
|
|
229
|
+
zh: "极简模式"
|
|
230
|
+
},
|
|
231
|
+
cordis: {
|
|
232
|
+
en: "Creator mode",
|
|
233
|
+
zh: "创造模式"
|
|
234
|
+
}
|
|
221
235
|
};
|
|
222
|
-
/**
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Quote a value as a single-line YAML single-quoted scalar. Plain scalars
|
|
238
|
+
* cannot contain `: ` (colon + space), which plain English sentences do —
|
|
239
|
+
* written unquoted they make the whole preset.yml unparsable, dropping the
|
|
240
|
+
* name, description and order together.
|
|
241
|
+
*/
|
|
242
|
+
function yamlScalar(value) {
|
|
243
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
244
|
+
}
|
|
245
|
+
/** The variant name for one shipped mode (bilingual) or a custom preset. */
|
|
246
|
+
function variantName(presetId, sourceName) {
|
|
247
|
+
const labels = MODE_DISPLAY_LABELS[presetId];
|
|
248
|
+
return labels === void 0 ? `WSL · ${sourceName}` : `WSL · ${labels.en}(${labels.zh})`;
|
|
249
|
+
}
|
|
250
|
+
/** The variant description for one shipped mode (bilingual) or a custom preset. */
|
|
251
|
+
function variantDescription(presetId) {
|
|
252
|
+
const labels = MODE_DISPLAY_LABELS[presetId];
|
|
253
|
+
return `WSL execution world for ${labels === void 0 ? presetId : `${labels.en}(${labels.zh})`}: bash and file tools run inside the WSL distribution.`;
|
|
225
254
|
}
|
|
226
255
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
227
256
|
/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */
|
|
@@ -378,18 +407,19 @@ async function materializeVariants(agentPresets, dshHome, shellPath, fsPath) {
|
|
|
378
407
|
const dir = join(userRoot, variantId);
|
|
379
408
|
mkdirSync(dir, { recursive: true });
|
|
380
409
|
writeFileSync(join(dir, "agent.cordis.yml"), transformed, "utf8");
|
|
381
|
-
|
|
410
|
+
const labels = MODE_DISPLAY_LABELS[preset.id];
|
|
411
|
+
let name = variantName(preset.id, preset.id);
|
|
382
412
|
let orderLine = "";
|
|
383
413
|
try {
|
|
384
414
|
const meta = readFileSync(join(dirname(preset.path), "preset.yml"), "utf8");
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
415
|
+
if (labels === void 0) {
|
|
416
|
+
const match = /^name:\s*(.+)$/m.exec(meta);
|
|
417
|
+
if (match?.[1] !== void 0 && match[1].trim() !== "") name = variantName(preset.id, match[1].trim());
|
|
418
|
+
}
|
|
389
419
|
const orderMatch = /^order:\s*(\d+)\s*$/m.exec(meta);
|
|
390
420
|
if (orderMatch?.[1] !== void 0) orderLine = `order: ${orderMatch[1]}\n`;
|
|
391
421
|
} catch {}
|
|
392
|
-
writeFileSync(join(dir, "preset.yml"), `name: ${name}\n` + orderLine + `description: ${variantDescription(
|
|
422
|
+
writeFileSync(join(dir, "preset.yml"), `name: ${yamlScalar(name)}\n` + orderLine + `description: ${yamlScalar(variantDescription(preset.id))}\n`, "utf8");
|
|
393
423
|
generated.add(variantId);
|
|
394
424
|
}
|
|
395
425
|
for (const entry of readdirSync(userRoot, { withFileTypes: true })) {
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/host/variants.ts","../src/index.ts"],"sourcesContent":["/**\n * WSL preset-variant generator. For every healthy source preset the roster\n * supplies, a `wsl-<id>` variant is materialized under the roster's user\n * root: the source composition with its shell/filesystem world replaced by\n * the WSL providers, so any mode (standard, minimal, code, cordis, user\n * presets) can run on top of a WSL execution world. The execution world is\n * therefore orthogonal to the mode instead of a mode itself.\n *\n * The transformation is text-level on the top-level rows of the composition\n * (the shape all shipped presets share), with surgical edits for the known\n * special groups; unknown shapes are kept verbatim where possible.\n * @module dsh-wsl-workspace/host/variants\n */\n\n/** Top-level rows that name the execution world and are replaced by the variant's own. */\nconst WORLD_ROWS = new Set(['tool-bash', 'tool-pwsh', 'tool-fs', 'tool-fs-search', 'filesystem', 'persistent-shell'])\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(shellPath: string, fsPath: string, includeEditor: boolean): string {\n return [\n '# ── WSL execution world (dsh-wsl-workspace variant) ─────────────────────',\n '# The shell and fs services are provided entry-locally (the isolate',\n '# realm); host services (tools registry, shell-env, jobs) fall through.',\n '# tool-fs-search is intentionally absent: the packaged ripgrep runs on',\n '# the Windows host and cannot open Linux paths; WSL sessions search with',\n '# shell tools instead.',\n '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ' - id: tool-bash',\n \" name: '@deepseek-ai/dsh-tool-bash'\",\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n ...(includeEditor\n ? [\n ' - id: str-replace-editor',\n \" name: '@deepseek-ai/dsh-tool-str-replace-editor'\",\n ' config:',\n ' maxOutputChars: 16000',\n ]\n : []),\n '',\n ].join('\\n')\n}\n\n/**\n * The persistent-shell group re-pointed at WSL: the PTY spawns wsl.exe's\n * bash instead of a host `bash` (which does not exist on Windows).\n */\nfunction persistentShellGroup(): string {\n return [\n '# ── persistent shell over WSL (variant) ─────────────────────────────────',\n '- id: persistent-shell',\n ' name: cordis:group',\n ' group: true',\n ' isolate:',\n ' terminals: true',\n ' config:',\n ' - id: pty',\n \" name: '@deepseek-ai/dsh-terminal'\",\n '',\n ' - id: terminal-bash',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' timeoutMs: 300000',\n \" shellPath: 'wsl.exe'\",\n \" shellArgs: ['-e', 'bash', '-l']\",\n '',\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' timeoutMs: 300000',\n ' description: |-',\n ' Run commands in a bash shell inside the WSL distribution',\n ' * When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.',\n \" * You don't have access to the internet via this tool.\",\n ' * You do have access to a mirror of common linux and python packages via apt and pip.',\n ' * State is persistent across command calls and discussions with the user.',\n \" * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\",\n ' * Please avoid commands that may produce a very large amount of output.',\n \" * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.\",\n '',\n ].join('\\n')\n}\n\n/** The sentence appended to a standard-like persona when the variant runs in WSL. */\nconst PERSONA_APPEND = ' Your working directory {{cwd}} is inside a WSL (Windows Subsystem for Linux) distribution: the bash tool and the file read/write/edit tools use Linux paths, and the Windows filesystem is reachable as /mnt/<drive> for file migration.'\n\n/** The top-level rows of one composition, as (startLine, endLineExclusive) spans. */\nfunction topLevelSpans(lines: readonly string[]): { start: number; end: number }[] {\n const spans: { start: number; end: number }[] = []\n let start = -1\n for (let index = 0; index < lines.length; index++) {\n if (lines[index]?.startsWith('- id: ') === true) {\n if (start >= 0) spans.push({ start, end: index })\n start = index\n }\n }\n if (start >= 0) spans.push({ start, end: lines.length })\n return spans\n}\n\n/** The row id of a top-level span, or undefined when the first line is malformed. */\nfunction spanId(lines: readonly string[], span: { start: number; end: number }): string | undefined {\n return /^- id: ([A-Za-z0-9_.-]+)/.exec(lines[span.start] ?? '')?.[1]\n}\n\n/** Whether a top-level span is a `persona` row with an appendable folded text. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end).join('\\n')\n if (!block.includes('complete: true') && /text: [>|-]/.test(block)) {\n // Append only when the folded text actually has content lines.\n const textLine = block.split('\\n').find(line => /^(\\s*)text: [>|-]/.test(line))\n if (textLine !== undefined) {\n const indent = /^(\\s*)/.exec(textLine)?.[1]?.length ?? 0\n return block.split('\\n').some(line => line.length > indent && /^\\s+/.test(line) && !line.includes(':'))\n }\n }\n return false\n}\n\n/** Append the WSL sentence to a persona row's folded text (in place of its last text line). */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = lines.slice(span.start, span.end)\n const textIndex = block.findIndex(line => /^(\\s*)text: [>|-]/.test(line))\n if (textIndex < 0) return [...block]\n const indent = /^(\\s*)/.exec(block[textIndex] ?? '')?.[1]?.length ?? 0\n let lastText = -1\n for (let index = textIndex + 1; index < block.length; index++) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n if (line.length > indent && /^\\s+/.test(line)) lastText = index\n }\n if (lastText < 0) return [...block]\n const updated = [...block]\n const textIndent = /^(\\s*)/.exec(block[lastText] ?? '')?.[1] ?? ' '\n updated.splice(lastText + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return updated\n}\n\n/**\n * Transform one source preset composition into its WSL variant: drop the\n * execution-world rows, keep everything else verbatim, and append the WSL\n * world group (plus the persistent-shell group when the source had one).\n * @param source - the source composition text.\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(source: string, shellPath: string, fsPath: string): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n let sawEditor = false\n let sawPersistent = false\n let personaAppended = false\n for (const span of spans) {\n const id = spanId(lines, span)\n if (id === undefined) {\n kept.push(...lines.slice(span.start, span.end))\n continue\n }\n if (WORLD_ROWS.has(id)) {\n if (id === 'persistent-shell') sawPersistent = true\n continue\n }\n if (id === 'persona' && !personaAppended && appendablePersona(lines, span)) {\n kept.push(...appendPersona(lines, span))\n personaAppended = true\n continue\n }\n kept.push(...lines.slice(span.start, span.end))\n if (id === 'str-replace-editor') sawEditor = true\n }\n if (source.includes('str-replace-editor')) sawEditor = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor))\n if (sawPersistent) result.push(persistentShellGroup())\n return result.join('\\n').replace(/\\n{3,}/g, '\\n\\n').replace(/\\n+$/, '\\n')\n}\n\n/** Whether an id is one of this plugin's own preset directories. */\nexport function isWslVariantId(id: string): boolean {\n return id === 'wsl' || /^wsl-[a-z0-9-]+$/.test(id)\n}\n\n/** The variant id for one source preset id. */\nexport function variantIdFor(sourceId: string): string {\n return `wsl-${sourceId.toLowerCase()}`\n}\n","/**\r\n * Host half of dsh-wsl-workspace. Three responsibilities:\r\n *\r\n * 1. Materialize a `wsl-<mode>` variant for every healthy roster preset\r\n * under `<dshHome>/.agent-presets/` (the roster's auto-scanned user\r\n * root), so the WSL execution world — `shell-wsl` + `fs-wsl` behind one\r\n * entry-local realm, with `tool-bash`/`tool-fs` consumers — composes with\r\n * ANY mode instead of being a mode itself; the legacy standalone `wsl`\r\n * preset directory and stale variants are removed on boot. The preset\r\n * rows name THIS package's built lib files by absolute path, which the\r\n * preset mount resolves to `file:` URLs without relying on bare specifier\r\n * resolution from the preset's home directory.\r\n *\r\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\r\n * distribution discovery, one-level directory listing, path checks, and\r\n * the per-workspace username store — all over the 9P UNC share.\r\n * Loopback-only, matching the sensitivity of the privileged configuration\r\n * surface.\r\n *\r\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @module dsh-wsl-workspace\r\n */\r\n\r\nimport { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, join } from 'node:path'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { homedir } from 'node:os'\r\nimport { joinUnc, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\r\nimport { canonicalWslUnc, getWorkspaceUsername, setWorkspaceUsername } from './shared/wsl-credentials.ts'\r\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\r\nimport { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/variants.ts'\r\n\r\n/** The HTTP route this plugin serves (a relative, same-origin path). */\r\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\r\n\r\n/**\r\n * English display names for the shipped source modes, matching the app's own\r\n * built-in copy. The DSH picker localizes only the four built-in ids itself;\r\n * `wsl-*` variant ids render the preset.yml text verbatim, so the plugin\r\n * writes English names for the four shipped modes (custom presets keep\r\n * theirs).\r\n */\r\nconst MODE_DISPLAY_NAMES: Readonly<Record<string, string>> = {\r\n standard: 'Standard mode',\r\n code: 'Code mode',\r\n minimal: 'Minimal mode',\r\n cordis: 'Creator mode',\r\n}\r\n\r\n/** One English sentence describing a WSL variant's execution world. */\r\nfunction variantDescription(display: string): string {\r\n return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`\r\n}\r\n\r\n/** Plugin config. */\r\nexport interface Config {\r\n /** The route under which the dialog data API is served. */\r\n route?: string\r\n}\r\n\r\n/** The shape after schemastery applied the defaults. */\r\ntype ResolvedConfig = Required<Config>\r\n\r\n/** The `webServer.register` route contract this plugin consumes. */\r\ninterface WebServerRoute {\r\n kind: 'exact'\r\n path: string\r\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\r\n}\r\n\r\ninterface WebServerService {\r\n register(route: WebServerRoute): () => void\r\n}\r\n\r\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\r\ninterface ShellEnvService {\r\n register(contributor: {\r\n name: string\r\n variables: Readonly<Record<string, { description: string }>>\r\n resolve(execution: {\r\n agent?: { session: { header: { cwd?: string } } }\r\n }): Readonly<Partial<Record<string, string>>>\r\n }): () => void\r\n}\r\n\r\n/** One directory entry the dialog lists. */\r\ninterface WslDirEntryWire {\r\n name: string\r\n kind: 'directory' | 'file' | 'other'\r\n}\r\n\r\n/** One directory level plus its breadcrumb ancestry. */\r\ninterface WslDirListingWire {\r\n path: string\r\n parent: string | null\r\n entries: WslDirEntryWire[]\r\n}\r\n\r\n/** The wire envelope every method answers with. */\r\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\r\n\r\nconst MAX_BODY_BYTES = 1024 * 1024\r\n\r\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\r\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\r\n\r\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\r\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\r\n\r\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\r\nfunction isLoopback(address: string | undefined): boolean {\r\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\r\n}\r\n\r\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\r\nfunction hostNameOf(host: string): string {\r\n if (host.startsWith('[')) {\r\n const end = host.indexOf(']')\r\n return end >= 0 ? host.slice(1, end) : host\r\n }\r\n return host.split(':')[0] ?? ''\r\n}\r\n\r\n/** True when the request's `Host` header names a loopback host. */\r\nfunction isLoopbackHost(host: string | undefined): boolean {\r\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\r\n}\r\n\r\n/**\r\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\r\n * an attacker-controlled segment containing separators or `..` would escape\r\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\r\n * @param value - the raw wire value.\r\n * @returns the validated distro name.\r\n */\r\nfunction requireDistro(value: unknown): string {\r\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\r\n throw new Error('distro must be a valid WSL distribution name')\r\n }\r\n return value\r\n}\r\n\r\n/** Human text for an unknown rejection. */\r\nfunction messageOf(value: unknown): string {\r\n return value instanceof Error ? value.message : String(value)\r\n}\r\n\r\n/** Write one JSON envelope. */\r\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\r\n res.writeHead(status, {\r\n 'content-type': 'application/json; charset=utf-8',\r\n 'cache-control': 'no-store',\r\n 'x-content-type-options': 'nosniff',\r\n })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/** Collect and parse the request body, bounded. */\r\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\r\n const chunks: Buffer[] = []\r\n let size = 0\r\n for await (const chunk of req) {\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\r\n size += buffer.length\r\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\r\n chunks.push(buffer)\r\n }\r\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\r\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error('request body must be a JSON object')\r\n }\r\n return parsed as Record<string, unknown>\r\n}\r\n\r\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\r\nfunction requireLinuxPath(value: unknown, label: string): string {\r\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\r\n throw new Error(`${label} must be an absolute Linux path`)\r\n }\r\n return normalizeLinuxPath(value)\r\n}\r\n\r\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\r\nfunction requireWslUnc(value: unknown): string {\r\n if (typeof value !== 'string') throw new Error('path must be a string')\r\n const canonical = canonicalWslUnc(value)\r\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\r\n return canonical\r\n}\r\n\r\n/** Resolve one directory listing over the 9P share. */\r\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\r\n const unc = joinUnc(distro, linuxPath)\r\n const dirents = readdirSync(unc, { withFileTypes: true })\r\n const entries: WslDirEntryWire[] = dirents\r\n .slice(0, 1000)\r\n .map((dirent): WslDirEntryWire => {\r\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\r\n ? 'directory'\r\n : dirent.isFile() ? 'file' : 'other'\r\n return { name: dirent.name, kind }\r\n })\r\n .sort((a, b) => {\r\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\r\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\r\n return a.name.localeCompare(b.name)\r\n })\r\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\r\n return { path: linuxPath, parent, entries }\r\n}\r\n\r\n/** Route one method dispatch. */\r\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\r\n switch (method) {\r\n case 'listDistros': {\r\n const distros = await listDistros()\r\n const fallback = await defaultDistro()\r\n if (fallback !== undefined && distros.includes(fallback)) {\r\n return [fallback, ...distros.filter(name => name !== fallback)]\r\n }\r\n return distros\r\n }\r\n case 'listDir': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n return listWslDir(distro, path)\r\n }\r\n case 'check': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n const unc = joinUnc(distro, path)\r\n try {\r\n const info = statSync(unc)\r\n return { exists: true, isDirectory: info.isDirectory() }\r\n } catch {\r\n return { exists: false, isDirectory: false }\r\n }\r\n }\r\n case 'setUser': {\r\n const path = requireWslUnc(params.path)\r\n const username = params.username\r\n if (username === undefined || username === '') {\r\n setWorkspaceUsername(path, undefined)\r\n } else {\r\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\r\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\r\n }\r\n setWorkspaceUsername(path, username)\r\n }\r\n return null\r\n }\r\n default:\r\n throw new Error(`unknown method \"${method}\"`)\r\n }\r\n}\r\n\r\n/** The `ctx.agentPresets` roster face this plugin consumes (optional service). */\r\ninterface AgentPresetsService {\r\n list(): Promise<{ id: string; broken?: string; path: string }[]>\r\n read(id: string): Promise<string>\r\n}\r\n\r\n/**\r\n * Materialize a `wsl-<mode>` variant for every healthy source preset, and\r\n * remove this plugin's managed residue: stale variants whose source\r\n * disappeared, plus the legacy standalone `wsl` preset directory (the\r\n * execution world now composes with modes; a standalone WSL mode no longer\r\n * exists). Managed files: rewritten on every boot.\r\n * @param agentPresets - the roster service.\r\n * @param dshHome - the harness home (user preset root parent).\r\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\r\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\r\n */\r\nasync function materializeVariants(\r\n agentPresets: AgentPresetsService,\r\n dshHome: string,\r\n shellPath: string,\r\n fsPath: string,\r\n): Promise<void> {\r\n const presets = await agentPresets.list()\r\n const userRoot = join(dshHome, '.agent-presets')\r\n const generated = new Set<string>()\r\n for (const preset of presets) {\r\n if (preset.broken !== undefined) continue\r\n if (isWslVariantId(preset.id)) continue\r\n const variantId = variantIdFor(preset.id)\r\n const source = await agentPresets.read(preset.id)\r\n const transformed = transformPresetForWsl(source, shellPath, fsPath)\r\n const dir = join(userRoot, variantId)\r\n mkdirSync(dir, { recursive: true })\r\n writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')\r\n let name = `WSL · ${preset.id}`\r\n let orderLine = ''\r\n try {\r\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\r\n const match = /^name:\\s*(.+)$/m.exec(meta)\r\n // Shipped modes show English display names (the picker reads the file\r\n // text verbatim for wsl-* ids); custom presets keep their own name.\r\n const displayName = MODE_DISPLAY_NAMES[preset.id]\r\n if (displayName !== undefined) {\r\n name = `WSL · ${displayName}`\r\n } else if (match?.[1] !== undefined && match[1].trim() !== '') {\r\n name = `WSL · ${match[1].trim()}`\r\n }\r\n // Inherit the source's declared order so the WSL variants line up with\r\n // the local modes in the roster (standard, PTC, minimal, cordis).\r\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\r\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\r\n } catch {\r\n // Absent or unreadable display metadata falls back to the id-based name.\r\n }\r\n writeFileSync(\r\n join(dir, 'preset.yml'),\r\n `name: ${name}\\n`\r\n + orderLine\r\n + `description: ${variantDescription(MODE_DISPLAY_NAMES[preset.id] ?? preset.id)}\\n`,\r\n 'utf8',\r\n )\r\n generated.add(variantId)\r\n }\r\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\r\n if (!entry.isDirectory()) continue\r\n if (entry.name === 'wsl') {\r\n // The legacy standalone WSL mode: folded into the variants above.\r\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n continue\r\n }\r\n if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue\r\n if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n }\r\n}\r\n\r\n/** Function-plugin plugin contract. */\r\nexport const name = 'dsh-wsl-workspace'\r\n\r\n/** Required services. */\r\nexport const inject = ['webServer']\r\n\r\n/** Validated plugin config (schemastery applied the defaults). */\r\nexport const Config: z<Config> = z.object({\r\n route: z.string().default(DEFAULT_ROUTE),\r\n})\r\n\r\n/**\r\n * Apply the host half: materialize the `wsl` preset plus a `wsl-<mode>`\r\n * variant for every healthy roster preset, register the data route, and\r\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @param ctx - the host plugin context.\r\n * @param config - the validated configuration.\r\n */\r\nexport function apply(ctx: Context, config: Config): void {\r\n const resolved = config as ResolvedConfig\r\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\r\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\r\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\r\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\r\n\r\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\r\n if (agentPresets !== undefined) {\r\n ctx.effect(() => {\r\n void materializeVariants(agentPresets, dshHome, shellPath, fsPath).catch((error) => {\r\n // Variant generation is best-effort over a live roster: a missing or\r\n // unreadable source preset must not take the whole plugin down, but\r\n // the failure is surfaced loudly rather than hidden.\r\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\r\n })\r\n return () => {}\r\n }, 'dsh-wsl-workspace: WSL preset variants')\r\n }\r\n\r\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\r\n if (shellEnv !== undefined) {\r\n ctx.effect(() => shellEnv.register({\r\n name: 'wsl-workspace-distro',\r\n variables: {\r\n DSH_WSL_DISTRO: {\r\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\r\n },\r\n DSH_WSL_USER: {\r\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\r\n },\r\n },\r\n resolve(execution) {\r\n const cwd = execution.agent?.session.header.cwd\r\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\r\n if (unc === null) return {}\r\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\r\n return username === undefined || username === ''\r\n ? { DSH_WSL_DISTRO: unc.distro }\r\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\r\n },\r\n }), 'dsh-wsl-workspace: per-session distro env fact')\r\n }\r\n\r\n const webServer = ctx.get('webServer') as unknown as WebServerService\r\n ctx.effect(() => webServer.register({\r\n kind: 'exact',\r\n path: resolved.route,\r\n handler: async (req, res) => {\r\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\r\n json(res, 403, { ok: false, error: 'loopback-only' })\r\n return\r\n }\r\n if (req.method !== 'POST') {\r\n json(res, 405, { ok: false, error: 'method not allowed' })\r\n return\r\n }\r\n let body: Record<string, unknown>\r\n try {\r\n body = await readBody(req)\r\n } catch (error) {\r\n json(res, 400, { ok: false, error: messageOf(error) })\r\n return\r\n }\r\n const method = typeof body.method === 'string' ? body.method : ''\r\n const params = body.params === undefined ? {} : body.params\r\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\r\n json(res, 400, { ok: false, error: 'params must be an object' })\r\n return\r\n }\r\n try {\r\n const value = await dispatch(method, params as Record<string, unknown>)\r\n json(res, 200, { ok: true, value })\r\n } catch (error) {\r\n json(res, 200, { ok: false, error: messageOf(error) })\r\n }\r\n },\r\n }), 'dsh-wsl-workspace: dialog data route')\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/host/variants.ts","../src/index.ts"],"sourcesContent":["/**\n * WSL preset-variant generator. For every healthy source preset the roster\n * supplies, a `wsl-<id>` variant is materialized under the roster's user\n * root: the source composition with its shell/filesystem world replaced by\n * the WSL providers, so any mode (standard, minimal, code, cordis, user\n * presets) can run on top of a WSL execution world. The execution world is\n * therefore orthogonal to the mode instead of a mode itself.\n *\n * The transformation is text-level on the top-level rows of the composition\n * (the shape all shipped presets share), with surgical edits for the known\n * special groups; unknown shapes are kept verbatim where possible.\n * @module dsh-wsl-workspace/host/variants\n */\n\n/** Top-level rows that name the execution world and are replaced by the variant's own. */\nconst WORLD_ROWS = new Set(['tool-bash', 'tool-pwsh', 'tool-fs', 'tool-fs-search', 'filesystem', 'persistent-shell'])\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(shellPath: string, fsPath: string, includeEditor: boolean): string {\n return [\n '# ── WSL execution world (dsh-wsl-workspace variant) ─────────────────────',\n '# The shell and fs services are provided entry-locally (the isolate',\n '# realm); host services (tools registry, shell-env, jobs) fall through.',\n '# tool-fs-search is intentionally absent: the packaged ripgrep runs on',\n '# the Windows host and cannot open Linux paths; WSL sessions search with',\n '# shell tools instead.',\n '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ' - id: tool-bash',\n \" name: '@deepseek-ai/dsh-tool-bash'\",\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n ...(includeEditor\n ? [\n ' - id: str-replace-editor',\n \" name: '@deepseek-ai/dsh-tool-str-replace-editor'\",\n ' config:',\n ' maxOutputChars: 16000',\n ]\n : []),\n '',\n ].join('\\n')\n}\n\n/**\n * The persistent-shell group re-pointed at WSL: the PTY spawns wsl.exe's\n * bash instead of a host `bash` (which does not exist on Windows).\n */\nfunction persistentShellGroup(): string {\n return [\n '# ── persistent shell over WSL (variant) ─────────────────────────────────',\n '- id: persistent-shell',\n ' name: cordis:group',\n ' group: true',\n ' isolate:',\n ' terminals: true',\n ' config:',\n ' - id: pty',\n \" name: '@deepseek-ai/dsh-terminal'\",\n '',\n ' - id: terminal-bash',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' timeoutMs: 300000',\n \" shellPath: 'wsl.exe'\",\n \" shellArgs: ['-e', 'bash', '-l']\",\n '',\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' timeoutMs: 300000',\n ' description: |-',\n ' Run commands in a bash shell inside the WSL distribution',\n ' * When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.',\n \" * You don't have access to the internet via this tool.\",\n ' * You do have access to a mirror of common linux and python packages via apt and pip.',\n ' * State is persistent across command calls and discussions with the user.',\n \" * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\",\n ' * Please avoid commands that may produce a very large amount of output.',\n \" * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.\",\n '',\n ].join('\\n')\n}\n\n/** The sentence appended to a standard-like persona when the variant runs in WSL. */\nconst PERSONA_APPEND = ' Your working directory {{cwd}} is inside a WSL (Windows Subsystem for Linux) distribution: the bash tool and the file read/write/edit tools use Linux paths, and the Windows filesystem is reachable as /mnt/<drive> for file migration.'\n\n/** The top-level rows of one composition, as (startLine, endLineExclusive) spans. */\nfunction topLevelSpans(lines: readonly string[]): { start: number; end: number }[] {\n const spans: { start: number; end: number }[] = []\n let start = -1\n for (let index = 0; index < lines.length; index++) {\n if (lines[index]?.startsWith('- id: ') === true) {\n if (start >= 0) spans.push({ start, end: index })\n start = index\n }\n }\n if (start >= 0) spans.push({ start, end: lines.length })\n return spans\n}\n\n/** The row id of a top-level span, or undefined when the first line is malformed. */\nfunction spanId(lines: readonly string[], span: { start: number; end: number }): string | undefined {\n return /^- id: ([A-Za-z0-9_.-]+)/.exec(lines[span.start] ?? '')?.[1]\n}\n\n/** Whether a top-level span is a `persona` row with an appendable folded text. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end).join('\\n')\n if (!block.includes('complete: true') && /text: [>|-]/.test(block)) {\n // Append only when the folded text actually has content lines.\n const textLine = block.split('\\n').find(line => /^(\\s*)text: [>|-]/.test(line))\n if (textLine !== undefined) {\n const indent = /^(\\s*)/.exec(textLine)?.[1]?.length ?? 0\n return block.split('\\n').some(line => line.length > indent && /^\\s+/.test(line) && !line.includes(':'))\n }\n }\n return false\n}\n\n/** Append the WSL sentence to a persona row's folded text (in place of its last text line). */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = lines.slice(span.start, span.end)\n const textIndex = block.findIndex(line => /^(\\s*)text: [>|-]/.test(line))\n if (textIndex < 0) return [...block]\n const indent = /^(\\s*)/.exec(block[textIndex] ?? '')?.[1]?.length ?? 0\n let lastText = -1\n for (let index = textIndex + 1; index < block.length; index++) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n if (line.length > indent && /^\\s+/.test(line)) lastText = index\n }\n if (lastText < 0) return [...block]\n const updated = [...block]\n const textIndent = /^(\\s*)/.exec(block[lastText] ?? '')?.[1] ?? ' '\n updated.splice(lastText + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return updated\n}\n\n/**\n * Transform one source preset composition into its WSL variant: drop the\n * execution-world rows, keep everything else verbatim, and append the WSL\n * world group (plus the persistent-shell group when the source had one).\n * @param source - the source composition text.\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(source: string, shellPath: string, fsPath: string): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n let sawEditor = false\n let sawPersistent = false\n let personaAppended = false\n for (const span of spans) {\n const id = spanId(lines, span)\n if (id === undefined) {\n kept.push(...lines.slice(span.start, span.end))\n continue\n }\n if (WORLD_ROWS.has(id)) {\n if (id === 'persistent-shell') sawPersistent = true\n continue\n }\n if (id === 'persona' && !personaAppended && appendablePersona(lines, span)) {\n kept.push(...appendPersona(lines, span))\n personaAppended = true\n continue\n }\n kept.push(...lines.slice(span.start, span.end))\n if (id === 'str-replace-editor') sawEditor = true\n }\n if (source.includes('str-replace-editor')) sawEditor = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor))\n if (sawPersistent) result.push(persistentShellGroup())\n return result.join('\\n').replace(/\\n{3,}/g, '\\n\\n').replace(/\\n+$/, '\\n')\n}\n\n/** Whether an id is one of this plugin's own preset directories. */\nexport function isWslVariantId(id: string): boolean {\n return id === 'wsl' || /^wsl-[a-z0-9-]+$/.test(id)\n}\n\n/** The variant id for one source preset id. */\nexport function variantIdFor(sourceId: string): string {\n return `wsl-${sourceId.toLowerCase()}`\n}\n","/**\r\n * Host half of dsh-wsl-workspace. Three responsibilities:\r\n *\r\n * 1. Materialize a `wsl-<mode>` variant for every healthy roster preset\r\n * under `<dshHome>/.agent-presets/` (the roster's auto-scanned user\r\n * root), so the WSL execution world — `shell-wsl` + `fs-wsl` behind one\r\n * entry-local realm, with `tool-bash`/`tool-fs` consumers — composes with\r\n * ANY mode instead of being a mode itself; the legacy standalone `wsl`\r\n * preset directory and stale variants are removed on boot. The preset\r\n * rows name THIS package's built lib files by absolute path, which the\r\n * preset mount resolves to `file:` URLs without relying on bare specifier\r\n * resolution from the preset's home directory.\r\n *\r\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\r\n * distribution discovery, one-level directory listing, path checks, and\r\n * the per-workspace username store — all over the 9P UNC share.\r\n * Loopback-only, matching the sensitivity of the privileged configuration\r\n * surface.\r\n *\r\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @module dsh-wsl-workspace\r\n */\r\n\r\nimport { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, join } from 'node:path'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { homedir } from 'node:os'\r\nimport { joinUnc, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\r\nimport { canonicalWslUnc, getWorkspaceUsername, setWorkspaceUsername } from './shared/wsl-credentials.ts'\r\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\r\nimport { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/variants.ts'\r\n\r\n/** The HTTP route this plugin serves (a relative, same-origin path). */\r\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\r\n\r\n/**\r\n * Bilingual display labels for the shipped source modes, matching the app's\r\n * own built-in copy in each language — note the `code` preset is \"PTC 模式\"\r\n * in the Chinese copy but \"Code mode\" in English. The DSH picker localizes\r\n * only the four built-in ids itself; `wsl-*` variant ids render the\r\n * preset.yml text verbatim, so the plugin writes one bilingual string so\r\n * both locales can identify each variant. Custom presets keep their own\r\n * name.\r\n */\r\nconst MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>> = {\r\n standard: { en: 'Standard mode', zh: '标准模式' },\r\n code: { en: 'Code mode', zh: 'PTC 模式' },\r\n minimal: { en: 'Minimal mode', zh: '极简模式' },\r\n cordis: { en: 'Creator mode', zh: '创造模式' },\r\n}\r\n\r\n/**\r\n * Quote a value as a single-line YAML single-quoted scalar. Plain scalars\r\n * cannot contain `: ` (colon + space), which plain English sentences do —\r\n * written unquoted they make the whole preset.yml unparsable, dropping the\r\n * name, description and order together.\r\n */\r\nfunction yamlScalar(value: string): string {\r\n return `'${value.replace(/'/g, \"''\")}'`\r\n}\r\n\r\n/** The variant name for one shipped mode (bilingual) or a custom preset. */\r\nfunction variantName(presetId: string, sourceName: string): string {\r\n const labels = MODE_DISPLAY_LABELS[presetId]\r\n return labels === undefined ? `WSL · ${sourceName}` : `WSL · ${labels.en}(${labels.zh})`\r\n}\r\n\r\n/** The variant description for one shipped mode (bilingual) or a custom preset. */\r\nfunction variantDescription(presetId: string): string {\r\n const labels = MODE_DISPLAY_LABELS[presetId]\r\n const display = labels === undefined ? presetId : `${labels.en}(${labels.zh})`\r\n return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`\r\n}\r\n\r\n/** Plugin config. */\r\nexport interface Config {\r\n /** The route under which the dialog data API is served. */\r\n route?: string\r\n}\r\n\r\n/** The shape after schemastery applied the defaults. */\r\ntype ResolvedConfig = Required<Config>\r\n\r\n/** The `webServer.register` route contract this plugin consumes. */\r\ninterface WebServerRoute {\r\n kind: 'exact'\r\n path: string\r\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\r\n}\r\n\r\ninterface WebServerService {\r\n register(route: WebServerRoute): () => void\r\n}\r\n\r\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\r\ninterface ShellEnvService {\r\n register(contributor: {\r\n name: string\r\n variables: Readonly<Record<string, { description: string }>>\r\n resolve(execution: {\r\n agent?: { session: { header: { cwd?: string } } }\r\n }): Readonly<Partial<Record<string, string>>>\r\n }): () => void\r\n}\r\n\r\n/** One directory entry the dialog lists. */\r\ninterface WslDirEntryWire {\r\n name: string\r\n kind: 'directory' | 'file' | 'other'\r\n}\r\n\r\n/** One directory level plus its breadcrumb ancestry. */\r\ninterface WslDirListingWire {\r\n path: string\r\n parent: string | null\r\n entries: WslDirEntryWire[]\r\n}\r\n\r\n/** The wire envelope every method answers with. */\r\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\r\n\r\nconst MAX_BODY_BYTES = 1024 * 1024\r\n\r\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\r\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\r\n\r\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\r\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\r\n\r\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\r\nfunction isLoopback(address: string | undefined): boolean {\r\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\r\n}\r\n\r\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\r\nfunction hostNameOf(host: string): string {\r\n if (host.startsWith('[')) {\r\n const end = host.indexOf(']')\r\n return end >= 0 ? host.slice(1, end) : host\r\n }\r\n return host.split(':')[0] ?? ''\r\n}\r\n\r\n/** True when the request's `Host` header names a loopback host. */\r\nfunction isLoopbackHost(host: string | undefined): boolean {\r\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\r\n}\r\n\r\n/**\r\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\r\n * an attacker-controlled segment containing separators or `..` would escape\r\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\r\n * @param value - the raw wire value.\r\n * @returns the validated distro name.\r\n */\r\nfunction requireDistro(value: unknown): string {\r\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\r\n throw new Error('distro must be a valid WSL distribution name')\r\n }\r\n return value\r\n}\r\n\r\n/** Human text for an unknown rejection. */\r\nfunction messageOf(value: unknown): string {\r\n return value instanceof Error ? value.message : String(value)\r\n}\r\n\r\n/** Write one JSON envelope. */\r\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\r\n res.writeHead(status, {\r\n 'content-type': 'application/json; charset=utf-8',\r\n 'cache-control': 'no-store',\r\n 'x-content-type-options': 'nosniff',\r\n })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/** Collect and parse the request body, bounded. */\r\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\r\n const chunks: Buffer[] = []\r\n let size = 0\r\n for await (const chunk of req) {\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\r\n size += buffer.length\r\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\r\n chunks.push(buffer)\r\n }\r\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\r\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error('request body must be a JSON object')\r\n }\r\n return parsed as Record<string, unknown>\r\n}\r\n\r\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\r\nfunction requireLinuxPath(value: unknown, label: string): string {\r\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\r\n throw new Error(`${label} must be an absolute Linux path`)\r\n }\r\n return normalizeLinuxPath(value)\r\n}\r\n\r\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\r\nfunction requireWslUnc(value: unknown): string {\r\n if (typeof value !== 'string') throw new Error('path must be a string')\r\n const canonical = canonicalWslUnc(value)\r\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\r\n return canonical\r\n}\r\n\r\n/** Resolve one directory listing over the 9P share. */\r\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\r\n const unc = joinUnc(distro, linuxPath)\r\n const dirents = readdirSync(unc, { withFileTypes: true })\r\n const entries: WslDirEntryWire[] = dirents\r\n .slice(0, 1000)\r\n .map((dirent): WslDirEntryWire => {\r\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\r\n ? 'directory'\r\n : dirent.isFile() ? 'file' : 'other'\r\n return { name: dirent.name, kind }\r\n })\r\n .sort((a, b) => {\r\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\r\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\r\n return a.name.localeCompare(b.name)\r\n })\r\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\r\n return { path: linuxPath, parent, entries }\r\n}\r\n\r\n/** Route one method dispatch. */\r\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\r\n switch (method) {\r\n case 'listDistros': {\r\n const distros = await listDistros()\r\n const fallback = await defaultDistro()\r\n if (fallback !== undefined && distros.includes(fallback)) {\r\n return [fallback, ...distros.filter(name => name !== fallback)]\r\n }\r\n return distros\r\n }\r\n case 'listDir': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n return listWslDir(distro, path)\r\n }\r\n case 'check': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n const unc = joinUnc(distro, path)\r\n try {\r\n const info = statSync(unc)\r\n return { exists: true, isDirectory: info.isDirectory() }\r\n } catch {\r\n return { exists: false, isDirectory: false }\r\n }\r\n }\r\n case 'setUser': {\r\n const path = requireWslUnc(params.path)\r\n const username = params.username\r\n if (username === undefined || username === '') {\r\n setWorkspaceUsername(path, undefined)\r\n } else {\r\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\r\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\r\n }\r\n setWorkspaceUsername(path, username)\r\n }\r\n return null\r\n }\r\n default:\r\n throw new Error(`unknown method \"${method}\"`)\r\n }\r\n}\r\n\r\n/** The `ctx.agentPresets` roster face this plugin consumes (optional service). */\r\ninterface AgentPresetsService {\r\n list(): Promise<{ id: string; broken?: string; path: string }[]>\r\n read(id: string): Promise<string>\r\n}\r\n\r\n/**\r\n * Materialize a `wsl-<mode>` variant for every healthy source preset, and\r\n * remove this plugin's managed residue: stale variants whose source\r\n * disappeared, plus the legacy standalone `wsl` preset directory (the\r\n * execution world now composes with modes; a standalone WSL mode no longer\r\n * exists). Managed files: rewritten on every boot.\r\n * @param agentPresets - the roster service.\r\n * @param dshHome - the harness home (user preset root parent).\r\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\r\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\r\n */\r\nasync function materializeVariants(\r\n agentPresets: AgentPresetsService,\r\n dshHome: string,\r\n shellPath: string,\r\n fsPath: string,\r\n): Promise<void> {\r\n const presets = await agentPresets.list()\r\n const userRoot = join(dshHome, '.agent-presets')\r\n const generated = new Set<string>()\r\n for (const preset of presets) {\r\n if (preset.broken !== undefined) continue\r\n if (isWslVariantId(preset.id)) continue\r\n const variantId = variantIdFor(preset.id)\r\n const source = await agentPresets.read(preset.id)\r\n const transformed = transformPresetForWsl(source, shellPath, fsPath)\r\n const dir = join(userRoot, variantId)\r\n mkdirSync(dir, { recursive: true })\r\n writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')\r\n const labels = MODE_DISPLAY_LABELS[preset.id]\r\n let name = variantName(preset.id, preset.id)\r\n let orderLine = ''\r\n try {\r\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\r\n if (labels === undefined) {\r\n // Custom presets keep their own display name; shipped modes use the\r\n // bilingual labels above so both locales can identify the variant.\r\n const match = /^name:\\s*(.+)$/m.exec(meta)\r\n if (match?.[1] !== undefined && match[1].trim() !== '') {\r\n name = variantName(preset.id, match[1].trim())\r\n }\r\n }\r\n // Inherit the source's declared order so the WSL variants line up with\r\n // the local modes in the roster (standard, PTC, minimal, cordis).\r\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\r\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\r\n } catch {\r\n // Absent or unreadable display metadata falls back to the id-based name.\r\n }\r\n writeFileSync(\r\n join(dir, 'preset.yml'),\r\n `name: ${yamlScalar(name)}\\n`\r\n + orderLine\r\n + `description: ${yamlScalar(variantDescription(preset.id))}\\n`,\r\n 'utf8',\r\n )\r\n generated.add(variantId)\r\n }\r\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\r\n if (!entry.isDirectory()) continue\r\n if (entry.name === 'wsl') {\r\n // The legacy standalone WSL mode: folded into the variants above.\r\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n continue\r\n }\r\n if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue\r\n if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n }\r\n}\r\n\r\n/** Function-plugin plugin contract. */\r\nexport const name = 'dsh-wsl-workspace'\r\n\r\n/** Required services. */\r\nexport const inject = ['webServer']\r\n\r\n/** Validated plugin config (schemastery applied the defaults). */\r\nexport const Config: z<Config> = z.object({\r\n route: z.string().default(DEFAULT_ROUTE),\r\n})\r\n\r\n/**\r\n * Apply the host half: materialize the `wsl` preset plus a `wsl-<mode>`\r\n * variant for every healthy roster preset, register the data route, and\r\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @param ctx - the host plugin context.\r\n * @param config - the validated configuration.\r\n */\r\nexport function apply(ctx: Context, config: Config): void {\r\n const resolved = config as ResolvedConfig\r\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\r\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\r\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\r\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\r\n\r\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\r\n if (agentPresets !== undefined) {\r\n ctx.effect(() => {\r\n void materializeVariants(agentPresets, dshHome, shellPath, fsPath).catch((error) => {\r\n // Variant generation is best-effort over a live roster: a missing or\r\n // unreadable source preset must not take the whole plugin down, but\r\n // the failure is surfaced loudly rather than hidden.\r\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\r\n })\r\n return () => {}\r\n }, 'dsh-wsl-workspace: WSL preset variants')\r\n }\r\n\r\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\r\n if (shellEnv !== undefined) {\r\n ctx.effect(() => shellEnv.register({\r\n name: 'wsl-workspace-distro',\r\n variables: {\r\n DSH_WSL_DISTRO: {\r\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\r\n },\r\n DSH_WSL_USER: {\r\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\r\n },\r\n },\r\n resolve(execution) {\r\n const cwd = execution.agent?.session.header.cwd\r\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\r\n if (unc === null) return {}\r\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\r\n return username === undefined || username === ''\r\n ? { DSH_WSL_DISTRO: unc.distro }\r\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\r\n },\r\n }), 'dsh-wsl-workspace: per-session distro env fact')\r\n }\r\n\r\n const webServer = ctx.get('webServer') as unknown as WebServerService\r\n ctx.effect(() => webServer.register({\r\n kind: 'exact',\r\n path: resolved.route,\r\n handler: async (req, res) => {\r\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\r\n json(res, 403, { ok: false, error: 'loopback-only' })\r\n return\r\n }\r\n if (req.method !== 'POST') {\r\n json(res, 405, { ok: false, error: 'method not allowed' })\r\n return\r\n }\r\n let body: Record<string, unknown>\r\n try {\r\n body = await readBody(req)\r\n } catch (error) {\r\n json(res, 400, { ok: false, error: messageOf(error) })\r\n return\r\n }\r\n const method = typeof body.method === 'string' ? body.method : ''\r\n const params = body.params === undefined ? {} : body.params\r\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\r\n json(res, 400, { ok: false, error: 'params must be an object' })\r\n return\r\n }\r\n try {\r\n const value = await dispatch(method, params as Record<string, unknown>)\r\n json(res, 200, { ok: true, value })\r\n } catch (error) {\r\n json(res, 200, { ok: false, error: messageOf(error) })\r\n }\r\n },\r\n }), 'dsh-wsl-workspace: dialog data route')\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wsl-workspace",
|
|
3
3
|
"description": "WSL workspace support for DeepSeek Harness: add a WSL workspace from the web GUI and run the whole agent session (bash + file tools) inside the WSL distribution, VS Code Remote-WSL style. No toolchain install inside WSL required.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"README.zh.md",
|
|
56
56
|
"image.png",
|
|
57
57
|
"image-1.png",
|
|
58
|
-
"image-2.png"
|
|
58
|
+
"image-2.png",
|
|
59
|
+
"image-3.png"
|
|
59
60
|
]
|
|
60
61
|
}
|
package/src/index.ts
CHANGED
|
@@ -39,21 +39,41 @@ import { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/vari
|
|
|
39
39
|
export const DEFAULT_ROUTE = '/wsl-workspace/api'
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
* built-in copy
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* Bilingual display labels for the shipped source modes, matching the app's
|
|
43
|
+
* own built-in copy in each language — note the `code` preset is "PTC 模式"
|
|
44
|
+
* in the Chinese copy but "Code mode" in English. The DSH picker localizes
|
|
45
|
+
* only the four built-in ids itself; `wsl-*` variant ids render the
|
|
46
|
+
* preset.yml text verbatim, so the plugin writes one bilingual string so
|
|
47
|
+
* both locales can identify each variant. Custom presets keep their own
|
|
48
|
+
* name.
|
|
47
49
|
*/
|
|
48
|
-
const
|
|
49
|
-
standard: 'Standard mode',
|
|
50
|
-
code: 'Code mode',
|
|
51
|
-
minimal: 'Minimal mode',
|
|
52
|
-
cordis: 'Creator mode',
|
|
50
|
+
const MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>> = {
|
|
51
|
+
standard: { en: 'Standard mode', zh: '标准模式' },
|
|
52
|
+
code: { en: 'Code mode', zh: 'PTC 模式' },
|
|
53
|
+
minimal: { en: 'Minimal mode', zh: '极简模式' },
|
|
54
|
+
cordis: { en: 'Creator mode', zh: '创造模式' },
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Quote a value as a single-line YAML single-quoted scalar. Plain scalars
|
|
59
|
+
* cannot contain `: ` (colon + space), which plain English sentences do —
|
|
60
|
+
* written unquoted they make the whole preset.yml unparsable, dropping the
|
|
61
|
+
* name, description and order together.
|
|
62
|
+
*/
|
|
63
|
+
function yamlScalar(value: string): string {
|
|
64
|
+
return `'${value.replace(/'/g, "''")}'`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The variant name for one shipped mode (bilingual) or a custom preset. */
|
|
68
|
+
function variantName(presetId: string, sourceName: string): string {
|
|
69
|
+
const labels = MODE_DISPLAY_LABELS[presetId]
|
|
70
|
+
return labels === undefined ? `WSL · ${sourceName}` : `WSL · ${labels.en}(${labels.zh})`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The variant description for one shipped mode (bilingual) or a custom preset. */
|
|
74
|
+
function variantDescription(presetId: string): string {
|
|
75
|
+
const labels = MODE_DISPLAY_LABELS[presetId]
|
|
76
|
+
const display = labels === undefined ? presetId : `${labels.en}(${labels.zh})`
|
|
57
77
|
return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`
|
|
58
78
|
}
|
|
59
79
|
|
|
@@ -294,18 +314,18 @@ async function materializeVariants(
|
|
|
294
314
|
const dir = join(userRoot, variantId)
|
|
295
315
|
mkdirSync(dir, { recursive: true })
|
|
296
316
|
writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')
|
|
297
|
-
|
|
317
|
+
const labels = MODE_DISPLAY_LABELS[preset.id]
|
|
318
|
+
let name = variantName(preset.id, preset.id)
|
|
298
319
|
let orderLine = ''
|
|
299
320
|
try {
|
|
300
321
|
const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
name = `WSL · ${match[1].trim()}`
|
|
322
|
+
if (labels === undefined) {
|
|
323
|
+
// Custom presets keep their own display name; shipped modes use the
|
|
324
|
+
// bilingual labels above so both locales can identify the variant.
|
|
325
|
+
const match = /^name:\s*(.+)$/m.exec(meta)
|
|
326
|
+
if (match?.[1] !== undefined && match[1].trim() !== '') {
|
|
327
|
+
name = variantName(preset.id, match[1].trim())
|
|
328
|
+
}
|
|
309
329
|
}
|
|
310
330
|
// Inherit the source's declared order so the WSL variants line up with
|
|
311
331
|
// the local modes in the roster (standard, PTC, minimal, cordis).
|
|
@@ -316,9 +336,9 @@ async function materializeVariants(
|
|
|
316
336
|
}
|
|
317
337
|
writeFileSync(
|
|
318
338
|
join(dir, 'preset.yml'),
|
|
319
|
-
`name: ${name}\n`
|
|
339
|
+
`name: ${yamlScalar(name)}\n`
|
|
320
340
|
+ orderLine
|
|
321
|
-
+ `description: ${variantDescription(
|
|
341
|
+
+ `description: ${yamlScalar(variantDescription(preset.id))}\n`,
|
|
322
342
|
'utf8',
|
|
323
343
|
)
|
|
324
344
|
generated.add(variantId)
|