dsh-wsl-workspace 0.7.2 → 0.7.3
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 +36 -0
- package/README.zh.md +8 -0
- package/TESTING.md +24 -11
- package/lib/client.js +4 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +124 -17
- package/lib/index.js.map +1 -1
- package/package.json +9 -4
- package/src/client/locales.ts +4 -4
- package/src/index.ts +222 -25
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["spans: { start: number; end: number }[]","kept: string[]","nodeSkillIo: WslSkillIo","entries: Dirent[] | undefined","children: [string, number][]","links: [string, number][]","roots: SkillRoot[]","frontier: [string, number][]","layer: [string, number][]","nextLayer: [string, number][]","result: SkillRoot[]","dirents: Dirent[]","entries: SkillEntry[]","joinWindowsPath","raw: string","value","name","collected: string[]","indent: string | undefined","candidates: WslSkillCandidate[]","signature: string[]","signature: string","parts: string[]","MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>>","chunks: Buffer[]","entries: WslDirEntryWire[]","kind: WslDirEntryWire['kind']","selfDescription: { version: string; releases: { id: string; status: string }[] } | undefined","name","Config: z<Config>","body: Record<string, unknown>"],"sources":["../src/host/variants.ts","../src/host/wsl-skills.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', 'str-replace-editor', 'tool-str-replace-editor', 'wsl-world', 'filesystem', 'persistent-shell', 'persistent-bash', 'persistent-pwsh', 'terminal-bash', 'terminal-pwsh', 'custom-bash', 'bootstrap-filesystem'])\n\n/** Execution-world rows that register the model-facing editor tool. */\nconst EDITOR_ROWS = new Set(['str-replace-editor', 'tool-str-replace-editor'])\n\n/** The row ids this generator mounts inside its own world group. */\nconst WSL_WORLD_PROVIDER_IDS = ['shell-wsl', 'fs-wsl']\n\n/**\n * Whether a top-level row is this generator's own WSL world group.\n *\n * A preset copied from a generated variant (a user renaming `wsl-standard` to\n * their own mode, say) carries the group under whatever id the copy kept, so\n * the mounted provider ids identify it even when the row id changed. Without\n * that check the variant would carry two world groups and the loader refuses\n * the whole preset with `duplicate loader entry id: wsl-world`.\n * @param block - the row's lines.\n * @returns true when the row mounts this generator's WSL providers.\n */\nfunction isWslWorldGroup(block: readonly string[]): boolean {\n const text = block.join('\\n')\n return WSL_WORLD_PROVIDER_IDS.some((id) => new RegExp(`^\\\\s+- id: ${id}$`, 'm').test(text))\n}\n\n/**\n * The persistent-shell rows: the host's own PTY registry and its config-driven\n * backend, told to run this plugin's relay (so the shell is a WSL one), plus\n * the persistent tool that consumes them.\n *\n * Two shapes matter here. The registry is an agent-owned service — the shipped\n * `persistent-shell` group keeps it in its own `terminals` realm for that\n * reason — so this is a nested group of the world rather than three flat rows.\n * And the persistent tool registers the tool name **`bash`**, exactly like the\n * one-shot `dsh-tool-bash` row it replaces: both cannot be mounted (the tools\n * registry rejects the duplicate and the whole preset fails to load), which is\n * also why DSH's own Minimal mode describes itself as \"a single-tool agent with\n * a persistent shell\". So a world with the relay paths swaps the shell tool\n * instead of adding one, and a world without them keeps the one-shot row.\n */\nfunction persistentShellRows(relayPath: string, nodePath: string): string[] {\n return [\n ' # Persistent shell: the host PTY registry and its backend, running',\n ' # this plugin\\'s relay, which hands the PTY to `wsl.exe … bash`',\n ' # (distribution and user come from the session). It registers the',\n ' # `bash` tool, so it takes the place of the one-shot row above.',\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 ' - id: terminal-wsl',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' backendType: wsl',\n ' shellDialect: bash',\n ` shellPath: '${nodePath.replace(/'/g, \"''\")}'`,\n ' shellArgs:',\n ` - '${relayPath.replace(/'/g, \"''\")}'`,\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' backendType: wsl',\n ...SHELL_DESCRIPTION_ROWS,\n ]\n}\n\n/**\n * The persistent tool's model-facing description, replacing the host default.\n *\n * The host tool's default says only that state persists, and its own Minimal\n * preset goes further in the wrong direction by suggesting `sleep 10 &`. Both\n * facts a WSL session needs are missing, and both were learned the hard way:\n *\n * - The shell is one process for the whole Agent, so a `cd` in one call decides\n * where the *next* call starts. A model that read the one-shot tool's contract\n * (\"each call runs in a fresh shell — pass `workdir` instead of using `cd`\")\n * will be surprised, and a probe left in `/tmp` makes every later relative path\n * resolve somewhere it never named.\n * - The host wraps each command as `eval -- $'…'`. A trailing `&` therefore\n * backgrounds the *whole* wrapped command: the tool's completion marker is\n * printed before the work runs, so the call reports no output and exit code 0\n * while the real output arrives later and can land inside the next call's\n * output window. `( … ) &` on its own line keeps the `&` on the subshell and\n * leaves the wrapper's sequencing intact.\n *\n * `description` is a supported key on every declared release (its `Config` schema\n * carries it from 0.1.0-rc.7 on), so the override is version-safe.\n */\nconst SHELL_DESCRIPTION_ROWS = [\n ' description: |-',\n ' Run commands in a persistent bash shell inside this WSL distribution. State, including',\n ' the current directory and exported environment variables, persists across calls: use',\n ' absolute paths or an explicit `cd` at the start of a command instead of relying on where',\n ' the previous call left the shell. The shell runs as the workspace\\'s Linux user; Windows',\n ' files are reachable as /mnt/<drive>, and no toolchain install is required.',\n ' * This tool takes `command` only. It has no `run_in_background` parameter, and passing',\n ' one is ignored - use the `bash_background` tool when the mode provides it, or background',\n ' a subshell as below.',\n ' * To leave work running without a tracked job, put it in a subshell with the `&` on its',\n ' own line: `( long-job > log 2>&1 ) &`. Then poll the log file in a later call.',\n ' * Never end a `&&` chain with `&`. That backgrounds the whole command, so the call',\n ' returns immediately with no output and exit code 0, and the real output arrives later,',\n ' possibly inside the next call\\'s output.',\n ' * Avoid commands that wait for stdin: an interactive foreground child can run until the',\n ' command timeout, and a timeout resets the shell and discards its state.',\n]\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(\n shellPath: string,\n fsPath: string,\n includeEditor: boolean,\n persistent?: { relayPath: string; nodePath: string; sandboxPath: string },\n searchPath?: string,\n jobsPath?: string,\n sawJobs = false,\n): 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 '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n // The Windows confinement runner cannot describe a Linux path, so the\n // world owns the capability (see the sandbox row below) whenever it mounts\n // a shell that would otherwise call it.\n ...(persistent === undefined ? [] : [' sandbox: true']),\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ...(persistent === undefined\n ? []\n : [\n ' - id: sandbox-wsl',\n ` name: '${persistent.sandboxPath.replace(/'/g, \"''\")}'`,\n ]),\n // The shell tool comes from the persistent-shell group when this world has\n // one (same `bash` name, so only one of the two may be mounted).\n ...(persistent === undefined\n ? [' - id: tool-bash', \" name: '@deepseek-ai/dsh-tool-bash'\"]\n : []),\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n // `grep`/`glob` run inside the distribution, not through the host suite's\n // Windows ripgrep: same tool names, same model-facing contract, Linux paths\n // and Linux symlinks. Older editor builds pass no cwd, so the provider\n // inherits it from the current tool execution.\n ...(searchPath === undefined\n ? []\n : [\n ' - id: search-wsl',\n ` name: '${searchPath.replace(/'/g, \"''\")}'`,\n ]),\n // The world's `bash` is the host's *persistent* tool, whose schema declares\n // only `command`: nothing here can start a tracked background job, so the\n // host's `job_*` tools would always answer \"no background jobs\" and a\n // `run_in_background` argument would be silently ignored. This row restores\n // the producer — but only for a source that also mounts the `job_*` control\n // tools, because a producer without a reader hands out ids nothing can use\n // (Minimal mode mounts neither). A world that keeps the one-shot bash row\n // needs no producer either: that tool carries `run_in_background` itself.\n ...(jobsPath === undefined || persistent === undefined || !sawJobs\n ? []\n : [\n ' - id: jobs-wsl',\n ` name: '${jobsPath.replace(/'/g, \"''\")}'`,\n ]),\n // The editor resolves through this entry-local WSL fs.\n // Anchored-family presets require this name during bootstrap.\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 ...(persistent === undefined ? [] : persistentShellRows(persistent.relayPath, persistent.nodePath)),\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 upstream local-skill provider row, whose watcher cannot watch a UNC share. */\nconst SKILL_FILESYSTEM_ROW = 'skill-filesystem'\n\n/**\n * Turn the local skill provider's watcher off inside a WSL variant.\n *\n * `@deepseek-ai/dsh-skill-filesystem` reports an INCOMPLETE observation when\n * its watcher fails to start, and `dsh-tool-skill` withholds the whole catalog\n * while a snapshot is incomplete. chokidar cannot watch\n * `\\\\wsl.localhost\\<distro>\\...`, so in a WSL session the model never sees the\n * catalog at all - `skill` still loads one by name, but nothing tells the model\n * which skills exist. With the watcher off the discovery completes (the\n * plugin's own provider rescans the share on demand, and the next session gets\n * a fresh catalog); the price is no live refresh inside one running session,\n * which never worked on this substrate anyway.\n * @param block - the row's lines.\n * @returns the row's lines with `watch: false` merged into its config.\n */\nfunction disableSkillWatch(block: readonly string[]): string[] {\n const lines = [...block]\n // An explicit `watch:` from the author wins.\n if (lines.some(line => /^\\s*watch:/.test(line))) return lines\n const configIndex = lines.findIndex(line => /^\\s*config:\\s*$/.test(line))\n if (configIndex >= 0) {\n const childIndent = `${/^(\\s*)/.exec(lines[configIndex] ?? '')?.[1] ?? ' '} `\n lines.splice(configIndex + 1, 0, `${childIndent}watch: false`)\n return lines\n }\n const rowIndent = /^(\\s*)/.exec(lines[0] ?? '')?.[1] ?? ''\n // Insert before the row block's trailing blank line, not after it.\n let insertAt = lines.length\n while (insertAt > 0 && (lines[insertAt - 1] ?? '').trim() === '') insertAt -= 1\n lines.splice(insertAt, 0, `${rowIndent} config:`, `${rowIndent} watch: false`)\n return lines\n}\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/** Persona config scalars that carry model-facing text, in append preference order. */\nconst PERSONA_TARGET_FIELDS = ['suffix', 'text', 'prefix'] as const\n\n/** One persona config scalar: where it sits, and whether it is a block header. */\ninterface PersonaTarget {\n /** Line index of the scalar inside the persona block. */\n index: number\n /** Field name: `suffix`, `text` or `prefix`. */\n field: string\n /** Indentation of the scalar's own line. */\n indent: number\n /** Block header (`>-`, `|-`, …) when the scalar is folded/literal, else ''. */\n header: string\n /** Unquoted inline value when the scalar is inline, else ''. */\n inline: string\n}\n\n/** Strip one layer of YAML quoting so a value can move into a block scalar. */\nexport function unquoteScalar(value: string): string {\n const single = /^'(.*)'$/s.exec(value)\n if (single !== null) return (single[1] ?? '').replace(/''/g, \"'\")\n const double = /^\"(.*)\"$/s.exec(value)\n return double !== null ? (double[1] ?? '') : value\n}\n\n/**\n * Locate the persona scalar this transform amends.\n *\n * DSH moved the model-facing prompt text across releases: `text` up to\n * v0.1.2-rc.1, and `prefix` + `suffix` from v0.1.3-alpha.2 on. `suffix` wins\n * when both exist because it carries the working-directory sentence, so the\n * appended note lands exactly where the legacy `text` block put it; `prefix`\n * is the last resort for a composition that has neither. A known field with an\n * empty value is not amendable.\n * @param block - the persona row's lines.\n * @returns the target scalar, or undefined when the row carries none.\n */\nfunction personaTarget(block: readonly string[]): PersonaTarget | undefined {\n for (const field of PERSONA_TARGET_FIELDS) {\n for (let index = 0; index < block.length; index += 1) {\n const match = new RegExp(`^(\\\\s*)${field}:\\\\s*(.*)$`).exec(block[index] ?? '')\n if (match === null) continue\n const indent = match[1]?.length ?? 0\n const rest = (match[2] ?? '').trim()\n if (rest === '' || /^[|>][+-]?$/.test(rest)) {\n return { index, field, indent, header: rest, inline: '' }\n }\n return { index, field, indent, header: '', inline: unquoteScalar(rest) }\n }\n }\n return undefined\n}\n\n/**\n * Index of the last line belonging to the block scalar whose header is at\n * `headerIndex`. A non-blank line at or above the header's indentation is the\n * next sibling key and ends the scalar — without that stop a persona carrying\n * both `suffix` and `prefix` blocks would take the note in the wrong one.\n * @param block - the persona row's lines.\n * @param headerIndex - index of the `field: >-` header line.\n * @param indent - the header's own indentation.\n * @returns the last content line's index, or -1 when the scalar is empty.\n */\nfunction lastBlockLine(block: readonly string[], headerIndex: number, indent: number): number {\n let last = -1\n for (let index = headerIndex + 1; index < block.length; index += 1) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n const lineIndent = /^(\\s*)/.exec(line)?.[1]?.length ?? 0\n if (lineIndent <= indent) break\n last = index\n }\n return last\n}\n\n/** Whether a top-level span is a `persona` row this transform may amend. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end)\n // `complete: true` asks the persona plugin for no runtime context at all.\n if (block.join('\\n').includes('complete: true')) return false\n const target = personaTarget(block)\n if (target === undefined) return false\n return target.header === ''\n ? target.inline !== ''\n : lastBlockLine(block, target.index, target.indent) >= 0\n}\n\n/**\n * Append the WSL sentence to a persona row's model-facing scalar, in place.\n *\n * A block scalar takes the sentence as a sibling line, which is what the\n * legacy `text: >-` form always did. An inline scalar cannot: it is folded into\n * a block scalar first, so the sentence joins it the same way (and the model\n * sees the same text it saw before v0.1.3-alpha.2).\n * @param lines - the whole composition's lines.\n * @param span - the persona row's span.\n * @returns the persona row's lines, amended when a target was found.\n */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = [...lines.slice(span.start, span.end)]\n const target = personaTarget(block)\n if (target === undefined) return block\n const pad = ' '.repeat(target.indent)\n if (target.header !== '') {\n const last = lastBlockLine(block, target.index, target.indent)\n if (last < 0) return block\n const textIndent = /^(\\s*)/.exec(block[last] ?? '')?.[1] ?? `${pad} `\n block.splice(last + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return block\n }\n const child = `${pad} `\n block.splice(\n target.index,\n 1,\n `${pad}${target.field}: >-`,\n `${child}${target.inline}`,\n `${child}${PERSONA_APPEND.trim()}`,\n )\n return block\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. A row id that appears twice in the source is kept once, and a\n * world group the source already carries (a preset copied from a generated\n * variant) is replaced rather than duplicated, so the variant always mounts\n * exactly one world pointing at this installation's providers.\n *\n * The source's own persistent-shell group is not re-added as such: it registers\n * the same `bash` tool name as the WSL world's `dsh-tool-bash`, and the tools\n * registry rejects duplicates within one preset layer — the whole variant fails\n * to mount and the session falls back to another preset. Its PTY *backend*, on\n * the other hand, is exactly what a stateful WSL shell needs, and it is\n * config-driven: given the relay in {@link persistentShellRows} it runs\n * `wsl.exe … bash` under the host's PTY, and its tool registers the separate\n * `persistent-bash` name, so the one-shot `bash` above stays as it is.\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 * @param persistent - the relay, interpreter and sandbox provider a stateful\n * shell needs; omit to generate a world without the persistent-shell rows.\n * @param searchPath - absolute path of the plugin's built in-distribution\n * `grep`/`glob` tools; mounted only for a source that had `tool-fs-search`.\n * @param jobsPath - absolute path of the plugin's built background-job producer\n * for the persistent shell; mounted only alongside that shell.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(\n source: string,\n shellPath: string,\n fsPath: string,\n persistent?: { relayPath: string; nodePath: string; sandboxPath: string },\n searchPath?: string,\n jobsPath?: string,\n): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n const seen = new Set<string>()\n let sawEditor = false\n let sawSearch = false\n let sawJobs = 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 // Two top-level rows with one id are a loader error (\"duplicate loader\n // entry id: <id>\"), which makes the whole variant unmountable, so the first\n // occurrence wins and a copied later one is dropped.\n if (seen.has(id)) continue\n seen.add(id)\n const block = lines.slice(span.start, span.end)\n if (WORLD_ROWS.has(id) || isWslWorldGroup(block)) {\n if (EDITOR_ROWS.has(id)) sawEditor = true\n if (id === 'tool-fs-search') sawSearch = true\n continue\n }\n if (id === SKILL_FILESYSTEM_ROW) {\n kept.push(...disableSkillWatch(block))\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(...block)\n }\n // An editor mounted inside a source group (a copied variant, say) also means\n // the composition expects one, so the injected world keeps its editor row. The\n // search tools and the background-job producer follow the same rule: a mode\n // without them (minimal) must not gain them, and a mode with them must not lose\n // them. `tool-jobs` is what registers `job_list`/`job_output`/`job_kill`.\n if (source.includes('str-replace-editor')) sawEditor = true\n if (source.includes('tool-fs-search')) sawSearch = true\n if (source.includes('tool-jobs')) sawJobs = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor, persistent, sawSearch ? searchPath : undefined, jobsPath, sawJobs))\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","/**\n * WSL workspace skill provider (host half).\n *\n * DSH's shipped skill-filesystem provider scans only the session cwd's\n * project root (the nearest `.git` ancestor) for `.dsh/skills` / `.agents/skills`\n * and never descends into nested projects. A WSL workspace whose project\n * folders live below the registered workspace root therefore shows an empty\n * skill catalog, even though the same layout works when the session cwd is\n * the project folder itself (issue #10).\n *\n * This provider mirrors the host's discovery rules for WSL UNC session\n * workspaces: it starts at the session cwd's nearest `.git` ancestor (the\n * host's project-root rule; the cwd itself when no ancestor has a `.git`\n * marker), then walks that root (depth- and budget-bounded). Directory\n * symlinks are followed when the substrate resolves them, and — the case the\n * `\\\\wsl.localhost` 9P share creates, where a link is listed but its Linux\n * target cannot be resolved Windows-side — the distribution itself resolves\n * them through bounded, concurrent `wsl.exe … readlink -f` calls. The walk\n * collects every\n * `.dsh/skills` and `.agents/skills` directory it finds — including nested\n * projects, linked-in projects anywhere on the Linux filesystem, and projects\n * reachable through more than one path — and publishes their skills with the\n * same project ranks and sources the host uses, so precedence and duplicate\n * resolution behave identically. Non-WSL lookups return nothing and leave\n * the host's own providers untouched.\n *\n * All filesystem reads go through `node:fs` against the `\\\\wsl.localhost\\…`\n * 9P share (the same substrate `WslFileSystem` uses); the distribution-side\n * resolution rides `wsl.exe` through `execFile` (no shell interpolation), and\n * an injectable IO face keeps the discovery logic unit-testable without a\n * live distro.\n *\n * @module dsh-wsl-workspace/host/wsl-skills\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { join as joinWindowsPath, posix } from 'node:path'\nimport { isAbsoluteLinuxPath, joinUnc, parseWslUnc, uncToLinux } from '../shared/paths.ts'\nimport { resolveLinuxSymlinks } from '../shared/links.ts'\n\n/** Project ranks copied from @deepseek-ai/dsh-skill-filesystem so WSL and host entries interleave identically. */\nconst PROJECT_DSH_RANK = 100\nconst PROJECT_AGENTS_RANK = 200\n\n/** How many directory levels below the workspace root are scanned. */\nconst MAX_SCAN_DEPTH = 4\n/** Maximum distinct skill directories published per lookup. */\nconst MAX_SKILL_ROOTS = 64\n/** Maximum directories visited per lookup (an absolute blast-radius cap). */\nconst MAX_VISITED_DIRECTORIES = 4096\n/**\n * How many Linux symlinks one lookup may hand to the distribution (a second\n * blast-radius cap, and a latency cap: each resolution is a short `wsl.exe`\n * call, so a tree with hundreds of links cannot stall the catalog).\n */\nconst MAX_LINK_RESOLUTIONS = 32\n/** How many parent levels above the session cwd are searched for a `.git` project marker. */\nconst MAX_ANCESTOR_WALK = 64\n/**\n * How many directories one discovery layer may probe at once.\n *\n * A `readdir` over the `\\\\wsl.localhost\\…` share measured 15.6 ms against 1.6 ms\n * for a `stat`, and a budget-sized walk is 4096 directories: probing one\n * directory at a time costs 20-30 s, which is what a turn in a large workspace\n * used to wait for. The layer stays bounded so the share is never flooded; node's\n * own filesystem thread pool is what ultimately caps the real parallelism.\n */\nconst WALK_CONCURRENCY = 16\n/** Maximum cached lookups (one entry per distinct scan root across sessions). */\nconst CACHE_MAX_ENTRIES = 32\n/**\n * How often a served scan root is re-checked for catalog changes: the skills\n * directories it published, plus one modification stamp per skill file. This is\n * the pass that makes a skill added — or a description edited — mid-session\n * visible on the model's next request.\n */\nconst REFRESH_POLL_MS = 3_000\n/**\n * How often the full re-discovery walk runs for a served scan root. Only a walk\n * can find a skills directory that did not exist before (a new nested project,\n * say), and it costs one `readdir` per visited directory, so it runs on its own\n * slower cadence instead of on every poll. It used to be the only pass, at\n * {@link REFRESH_POLL_MS}'s old value of 10 s.\n */\nconst DISCOVERY_POLL_MS = 30_000\n\n/** Kebab-case skill names, matching the host grammar. */\nconst SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n\n/** Directory names that never contain project skill roots (safe to prune while walking). */\nconst PRUNED_DIRECTORY_NAMES = new Set([\n '.git', '.hg', '.svn', '.bzr', 'node_modules', '.venv', 'venv', '.tox',\n '.pants.d', '.next', '.nuxt', 'dist', 'build', 'out', 'coverage',\n '__pycache__', '.mypy_cache', '.pytest_cache', '.ruff_cache', '.cache',\n '.idea', '.vscode', '.serverless', '.terraform', '.yarn', '.pnpm-store',\n])\n\n/** One `name: value` frontmatter line pair the parser understands. */\ninterface ParsedSkill {\n name: string\n description: string\n whenToUse?: string\n invocation: { modelInvocable: boolean; userInvocable: boolean }\n content: string\n}\n\n/** The provider's minimal skill-candidate contract (mirrors @deepseek-ai/dsh-skill). */\nexport interface WslSkillCandidate {\n readonly name: string\n readonly description: string\n readonly whenToUse?: string\n readonly invocation: { modelInvocable: boolean; userInvocable: boolean }\n readonly source: string\n readonly provider: string\n readonly rank: number\n readonly locator: { path: string; directory: string }\n readonly path: string\n}\n\n/** The provider's minimal skill-definition contract (candidate plus body). */\nexport interface WslSkillDefinition extends WslSkillCandidate {\n readonly content: string\n}\n\n/** Lookup options the registry passes to `list`/`get`. */\nexport interface WslSkillLookupOptions {\n readonly cwd?: string\n readonly signal?: AbortSignal\n}\n\n/** Registration-scoped lifecycle face passed to the provider constructor. */\nexport interface WslSkillProviderControl {\n readonly signal: AbortSignal\n readonly invalidate: () => void\n}\n\n/** The `ctx.skills` registry face this provider registers on (optional service). */\nexport interface WslSkillsRegistryFace {\n registerProvider(create: (control: WslSkillProviderControl) => {\n readonly name: string\n list(options: WslSkillLookupOptions): Promise<unknown>\n get(candidate: WslSkillCandidate, options: WslSkillLookupOptions): Promise<unknown>\n }): () => void\n}\n\n/** Injectable filesystem face (defaults to node:fs/promises on the real 9P share). */\nexport interface WslSkillIo {\n readdir(path: string, options: { withFileTypes: true }): Promise<Dirent[]>\n readFile(path: string, options: { encoding: 'utf8' }): Promise<string>\n /**\n * Stat one path. `mtimeMs`/`size` are optional because the change detector\n * uses them only where the substrate reports them: a face without them still\n * lists skills, it just cannot notice an edit to an existing skill file.\n */\n stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs?: number; size?: number }>\n /**\n * Resolve Linux symlink targets the share itself cannot follow, returning\n * each real path in the same UNC spelling (`undefined` where a link cannot\n * be resolved). Optional: a substrate that follows links locally leaves this\n * unimplemented, and the walk then never pays for a distribution round trip.\n */\n resolveLinks?(uncPaths: readonly string[]): Promise<(string | undefined)[]>\n}\n\n/** The node:fs/promises implementation the provider uses in production. */\nexport const nodeSkillIo: WslSkillIo = {\n readdir: async (path, options) => readdir(path, options),\n readFile: async (path, options) => readFile(path, options),\n stat: async path => stat(path),\n resolveLinks: async uncPaths => resolveLinuxSymlinks(uncPaths),\n}\n\n/** One discovered skill directory under a WSL workspace. */\ninterface SkillRoot {\n /** Absolute UNC path of the skills directory (`…\\.dsh\\skills`). */\n path: string\n /** Host source label ('project-dsh' | 'project-agents'). */\n source: 'project-dsh' | 'project-agents'\n /** Host project rank so same-name wins and precedence stay consistent. */\n rank: number\n}\n\n/** Whether a skills-directory entry is a directory-bundle or a flat markdown skill. */\ninterface SkillEntry {\n name: string\n kind: 'bundle' | 'flat'\n path: string\n}\n\n/**\n * Locate the nearest ancestor of `linuxDir` (the directory itself included)\n * containing a `.git` marker, mirroring the host skill-filesystem's\n * project-root rule. `.git` may be a directory or a worktree pointer file;\n * existence is enough. Bounded so a pathological path cannot spin the walk.\n * @param distro - the WSL distribution name.\n * @param linuxDir - the session cwd's absolute Linux path.\n * @param io - filesystem face.\n * @returns the project root's Linux path, or `undefined` when no ancestor carries a `.git`.\n */\nasync function nearestGitAncestor(distro: string, linuxDir: string, io: WslSkillIo): Promise<string | undefined> {\n let current = linuxDir\n for (let levels = 0; levels <= MAX_ANCESTOR_WALK; levels += 1) {\n try {\n await io.stat(joinUnc(distro, posix.join(current, '.git')))\n return current\n } catch {\n // No `.git` marker at this level; keep walking towards the filesystem root.\n }\n const parent = posix.dirname(current)\n if (parent === current) return undefined\n current = parent\n }\n return undefined\n}\n\n/** What one walk step learns about one directory. */\ninterface DirectoryProbe {\n /** The skill roots the directory publishes, `.dsh` before `.agents`. */\n readonly roots: SkillRoot[]\n /** Its child directories to descend into, as `[linux path, depth]`. */\n readonly children: [string, number][]\n /** Links its listing showed that the share itself could not follow. */\n readonly links: [string, number][]\n}\n\n/**\n * Everything the walk learns from one directory, in the order the round trips\n * are worth paying for: the listing first — it also says which skill markers can\n * exist there at all — then the markers it showed.\n * @param distro - the WSL distribution name.\n * @param dir - the directory's Linux path.\n * @param depth - its depth below the scan root.\n * @param io - filesystem face.\n * @returns the directory's roots, child directories and unresolved links.\n */\nasync function probeDirectory(distro: string, dir: string, depth: number, io: WslSkillIo): Promise<DirectoryProbe> {\n let entries: Dirent[] | undefined\n if (depth < MAX_SCAN_DEPTH) {\n try {\n entries = await io.readdir(joinUnc(distro, dir), { withFileTypes: true })\n } catch {\n // An unreadable directory (permissions, vanished mid-walk) prunes its\n // subtree, but the directory's own markers may still be worth probing.\n }\n }\n const listed = entries ?? []\n const allMarkers = PROJECT_SKILL_MARKERS.map(([marker]) => marker)\n const markers = entries === undefined\n ? allMarkers\n : allMarkers.filter(marker => listed.some(entry => entry.name === marker))\n const roots = await skillRootsOfDirectory(distro, dir, io, markers)\n const children: [string, number][] = []\n const links: [string, number][] = []\n for (const entry of listed) {\n if (PRUNED_DIRECTORY_NAMES.has(entry.name)) continue\n if (entry.name.startsWith('.') && entry.name !== '.dsh' && entry.name !== '.agents') continue\n if (entry.name === '.dsh' || entry.name === '.agents') continue\n const childPath = posix.join(dir, entry.name)\n if (entry.isDirectory()) {\n children.push([childPath, depth + 1])\n continue\n }\n if (!entry.isSymbolicLink()) continue\n // A project may be linked into the workspace via a directory symlink;\n // follow it when the target is a directory. Symlink cycles stay\n // bounded: every hop increments the depth (capped by MAX_SCAN_DEPTH)\n // and the walk as a whole by MAX_VISITED_DIRECTORIES.\n try {\n const target = await io.stat(joinUnc(distro, childPath))\n // A substrate that resolves Linux links itself answers here; a link\n // to a file is not a project directory either way.\n if (target.isDirectory()) children.push([childPath, depth + 1])\n continue\n } catch {\n // The `\\\\wsl.localhost` 9P share reports the link entry but cannot\n // resolve its Linux target; the distribution below can.\n }\n links.push([childPath, depth + 1])\n }\n return { roots, children, links }\n}\n\n/**\n * Scan a WSL workspace root for nested skill directories.\n *\n * A layer is probed concurrently but published in frontier order, so the\n * catalog never depends on which probe happened to finish first, and the budget\n * is claimed before any probe starts: concurrency must not change what is\n * visited, only how long it takes.\n * @param distro - the WSL distribution name.\n * @param linuxRoot - the workspace's absolute Linux path.\n * @param io - filesystem face.\n * @returns discovered skill directories, bounded by depth and budget.\n */\nasync function discoverSkillRoots(distro: string, linuxRoot: string, io: WslSkillIo): Promise<SkillRoot[]> {\n const roots: SkillRoot[] = []\n const visited = new Set<string>()\n let linksResolved = 0\n // BFS layers so the budget prunes the widest, most redundant levels first\n // (shallow skill dirs matter most): [path, depth] pairs.\n let frontier: [string, number][] = [[linuxRoot, 0]]\n while (frontier.length > 0 && roots.length < MAX_SKILL_ROOTS) {\n const layer: [string, number][] = []\n for (const item of frontier) {\n if (visited.size >= MAX_VISITED_DIRECTORIES) break\n if (visited.has(item[0])) continue\n visited.add(item[0])\n layer.push(item)\n }\n if (layer.length === 0) return roots\n const probes = new Array<DirectoryProbe | undefined>(layer.length)\n let next = 0\n await Promise.all(Array.from({ length: Math.min(WALK_CONCURRENCY, layer.length) }, async () => {\n for (let index = next; index < layer.length; index = next) {\n next += 1\n const [dir, depth] = layer[index]!\n probes[index] = await probeDirectory(distro, dir, depth, io)\n }\n }))\n const nextLayer: [string, number][] = []\n // Symlink targets the share could not follow, resolved for this whole\n // layer in one distribution round trip after the layer is enumerated.\n const links: [string, number][] = []\n for (const probe of probes) {\n if (probe === undefined) continue\n if (roots.length < MAX_SKILL_ROOTS) {\n roots.push(...probe.roots.slice(0, MAX_SKILL_ROOTS - roots.length))\n }\n nextLayer.push(...probe.children)\n links.push(...probe.links)\n }\n // The budget ran out inside this layer: stop where the walk stops, without\n // paying for this layer's link resolution either.\n if (visited.size >= MAX_VISITED_DIRECTORIES) return roots\n if (links.length > 0 && io.resolveLinks !== undefined && linksResolved < MAX_LINK_RESOLUTIONS) {\n const batch = links.slice(0, MAX_LINK_RESOLUTIONS - linksResolved)\n linksResolved += batch.length\n const resolved = await io.resolveLinks(batch.map(([path]) => joinUnc(distro, path)))\n for (let index = 0; index < batch.length; index += 1) {\n const real = resolved[index]\n if (real === undefined) continue\n // The walk continues at the link's real path, which also collapses a\n // project reachable both directly and through a link onto one visit.\n try {\n const info = await io.stat(real)\n if (info.isDirectory()) nextLayer.push([uncToLinux(real), batch[index]![1]])\n } catch {\n // The distribution resolved a target this share still cannot stat.\n }\n }\n }\n frontier = nextLayer\n }\n return roots\n}\n\n/** The two project skill markers, with the source and rank each publishes. */\nconst PROJECT_SKILL_MARKERS = [\n ['.dsh', 'project-dsh', PROJECT_DSH_RANK],\n ['.agents', 'project-agents', PROJECT_AGENTS_RANK],\n] as const\n\n/**\n * Publish the skill roots of one scanned directory (its `.dsh/skills` and\n * `.agents/skills`, each with the host's project ranks).\n * @param distro - the WSL distribution name.\n * @param linuxDir - the scanned directory's Linux path.\n * @param io - filesystem face.\n * @param markers - the markers worth probing. The walk narrows this to the ones\n * its listing actually showed, because each probe is a round trip and a\n * directory without a `.dsh` entry cannot hold `.dsh/skills`.\n * @returns the directory's skill roots that exist.\n */\nasync function skillRootsOfDirectory(\n distro: string,\n linuxDir: string,\n io: WslSkillIo,\n markers: readonly string[] = PROJECT_SKILL_MARKERS.map(([marker]) => marker),\n): Promise<SkillRoot[]> {\n const result: SkillRoot[] = []\n for (const [marker, source, rank] of PROJECT_SKILL_MARKERS) {\n if (!markers.includes(marker)) continue\n const path = joinUnc(distro, posix.join(linuxDir, marker, 'skills'))\n try {\n const info = await io.stat(path)\n if (info.isDirectory()) result.push({ path, source, rank })\n } catch {\n // Absent skills directory: nothing to publish.\n }\n }\n return result\n}\n\n/** List one skills directory's entries (directory bundles and flat `.md` skills). */\nasync function listSkillEntries(root: SkillRoot, io: WslSkillIo): Promise<SkillEntry[]> {\n let dirents: Dirent[]\n try {\n dirents = await io.readdir(root.path, { withFileTypes: true })\n } catch {\n return []\n }\n const entries: SkillEntry[] = []\n for (const entry of dirents) {\n if (entry.isDirectory()) {\n entries.push({ name: entry.name, kind: 'bundle', path: joinWindowsPath(root.path, entry.name, 'SKILL.md') })\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n entries.push({ name: entry.name.slice(0, -3), kind: 'flat', path: joinWindowsPath(root.path, entry.name) })\n }\n }\n return entries.sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * The published shape of one skills directory: its path, then each skill's name,\n * kind and modification stamp.\n *\n * The stamp is what makes an edit to an *existing* skill visible: the catalog the\n * model sees is rebuilt only when the registry's revision moves, and a directory\n * listing alone cannot tell a rewritten `SKILL.md` from an untouched one. One\n * `stat` per skill file (never a read) is the whole cost, and a file that cannot\n * be stamped reports the same `gone` marker on every pass, so a substrate that\n * does not expose modification times simply never triggers on content.\n * @param root - the skills directory.\n * @param entries - its entries, as just listed.\n * @param io - filesystem face.\n * @returns the deterministic shape string for this directory.\n */\nasync function shapeOfRoot(root: SkillRoot, entries: readonly SkillEntry[], io: WslSkillIo): Promise<string> {\n const stamps = await Promise.all(entries.map(async (entry) => {\n try {\n const info = await io.stat(entry.path)\n return `${entry.name}:${entry.kind}:${info.mtimeMs ?? 0}:${info.size ?? 0}`\n } catch {\n return `${entry.name}:${entry.kind}:gone`\n }\n }))\n return `${root.path}\\u0001${stamps.sort().join(',')}`\n}\n\n/** Read and parse one skill file; `undefined` when missing or unparsable. */\nasync function readSkill(path: string, io: WslSkillIo, signal?: AbortSignal): Promise<ParsedSkill | undefined> {\n signal?.throwIfAborted()\n let raw: string\n try {\n raw = await io.readFile(path, { encoding: 'utf8' })\n } catch {\n return undefined\n }\n signal?.throwIfAborted()\n return parseSkillFrontmatter(raw, path)\n}\n\n/**\n * Parse the frontmatter subset skill files use: `---` fenced YAML with\n * `name` / `description` / `whenToUse` / `user-invocable` /\n * `disable-model-invocation`. Single-line scalars and block scalars\n * (`|` literal, `>` folded) are understood; anything else is skipped,\n * matching the shipped provider's leniency: a bad file must not fail\n * the catalog.\n */\nfunction parseSkillFrontmatter(raw: string, path: string): ParsedSkill | undefined {\n // Windows editors save UTF-8 with a BOM; a leading BOM must not make the\n // opening `---` line unmatchable and silently drop the skill.\n if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1)\n const firstLineEnd = raw.indexOf('\\n')\n if (firstLineEnd < 0) return undefined\n if (raw.slice(0, firstLineEnd).replace(/\\r$/, '') !== '---') return undefined\n const start = firstLineEnd + 1\n const closing = findFrontmatterEnd(raw, start)\n if (closing === undefined) return undefined\n const lines = raw.slice(start, closing).split('\\n')\n const fields = new Map<string, string>()\n for (let index = 0; index < lines.length; index += 1) {\n const line = (lines[index] ?? '').replace(/\\r$/, '')\n const block = /^([A-Za-z0-9-]+):\\s*([|>])[+-]?\\s*$/.exec(line)\n if (block !== null) {\n const value = parseBlockScalar(lines, index, block[2] === '>')\n index = value.nextLineIndex\n if (value.text !== '') fields.set(block[1] ?? '', value.text)\n continue\n }\n const match = /^([A-Za-z0-9-]+):\\s*(.*)$/.exec(line)\n if (match === null) continue\n const value = match[2]?.trim() ?? ''\n if (value !== '') fields.set(match[1] ?? '', unquote(value))\n }\n const name = fields.get('name') ?? ''\n const description = fields.get('description') ?? ''\n if (!SKILL_NAME.test(name) || description === '') {\n return undefined\n }\n const whenToUse = fields.get('whenToUse')\n return {\n name,\n description,\n ...whenToUse !== undefined && whenToUse !== '' ? { whenToUse } : {},\n invocation: {\n modelInvocable: !frontmatterBoolean(fields, 'disable-model-invocation'),\n userInvocable: frontmatterBoolean(fields, 'user-invocable', true),\n },\n // `findFrontmatterEnd` returns the index of the body's FIRST character\n // (the newline after the closing `---` plus one), so slicing at `closing`\n // is what keeps it: `closing + 1` dropped the body's first character and\n // made the byte after the delimiter look like the body.\n content: raw.slice(closing).trim(),\n }\n}\n\n/**\n * Collect a YAML block scalar (`key: |` literal or `key: >` folded) starting\n * at `startIndex`'s following lines. The block runs until the first\n * non-indented, non-blank line; its common indentation is stripped.\n * @returns the scalar text and the index of the last consumed line.\n */\nfunction parseBlockScalar(\n lines: string[],\n startIndex: number,\n folded: boolean,\n): { text: string; nextLineIndex: number } {\n const collected: string[] = []\n let indent: string | undefined\n let index = startIndex\n while (index + 1 < lines.length) {\n index += 1\n const next = (lines[index] ?? '').replace(/\\r$/, '')\n if (next.trim() === '') {\n collected.push('')\n continue\n }\n const indented = /^([ \\t]+)(.*)$/.exec(next)\n if (indented === null) {\n index -= 1\n break\n }\n indent ??= indented[1]\n collected.push(indented[1]?.startsWith(indent) === true ? indented[2] : indented[1].replace(/^[ \\t]+/, '') + indented[2])\n }\n while (collected.length > 0 && collected[collected.length - 1] === '') collected.pop()\n const text = (folded ? collected.filter(line => line !== '').join(' ') : collected.join('\\n')).trim()\n return { text, nextLineIndex: index }\n}\n\n/** Locate the closing `---` line of a frontmatter block. */\nfunction findFrontmatterEnd(raw: string, start: number): number | undefined {\n let lineStart = start\n while (lineStart <= raw.length) {\n const nextNewline = raw.indexOf('\\n', lineStart)\n const lineEnd = nextNewline < 0 ? raw.length : nextNewline\n if (raw.slice(lineStart, lineEnd).replace(/\\r$/, '') === '---') return lineEnd + 1\n if (nextNewline < 0) return undefined\n lineStart = nextNewline + 1\n }\n return undefined\n}\n\n/** Strip one level of matching quotes from a scalar value. */\nfunction unquote(value: string): string {\n if (value.length >= 2) {\n const first = value[0]\n const last = value[value.length - 1]\n if ((first === '\"' && last === '\"') || (first === \"'\" && last === \"'\")) {\n return value.slice(1, -1)\n }\n }\n return value\n}\n\n/** Boolean semantics for `user-invocable` / `disable-model-invocation` (matches the host parser). */\nfunction frontmatterBoolean(fields: Map<string, string>, key: string, dflt = false): boolean {\n const value = fields.get(key)\n if (value === undefined) return dflt\n switch (value.toLowerCase()) {\n case 'true':\n case 'yes':\n case 'on':\n case '1':\n return true\n case 'false':\n case 'no':\n case 'off':\n case '0':\n return false\n default:\n return dflt\n }\n}\n\n/**\n * The WSL workspace skill provider. Registered on the host's `ctx.skills`\n * registry; serves only lookups whose cwd is a WSL UNC workspace path.\n *\n * Completed `list()` lookups are cached per scan root and served as-is: a lookup\n * is on the session's request path, and re-walking the tree there is what made\n * every turn in a large workspace wait for a 4096-directory scan of the 9P\n * share. `get()` always re-reads the skill file so body edits are picked up\n * immediately.\n *\n * A scan root that has been served is re-checked every REFRESH_POLL_MS: the\n * host cannot watch a `\\\\wsl.localhost\\…` path (which is why the generated\n * preset pins `watch: false`), so this provider watches for it instead. A\n * changed directory listing clears the cache and calls `control.invalidate()`,\n * which bumps the registry's revision; the catalog middleware re-collects on\n * the session's next request, so a skill added mid-session reaches the model\n * without starting a new session, and the cache is only ever refilled by a\n * lookup the detector has already invalidated.\n */\nexport class WslSkillsProvider {\n readonly name = 'wsl-workspace'\n private readonly control: WslSkillProviderControl\n private readonly io: WslSkillIo\n private readonly refreshMs: number\n /** Cheap polls between two full discovery walks (at least one). */\n private readonly walkEveryPolls: number\n /** The published catalog per scan root, served until its detector drops it. */\n private readonly cache = new Map<string, WslSkillCandidate[]>()\n /** One change detector per served scan root, keyed like {@link cache}. */\n private readonly detectors = new Map<string, {\n timer: ReturnType<typeof setInterval>\n signature: string\n roots: readonly SkillRoot[]\n polls: number\n /** True while a poll is still in flight, so a slow pass cannot stack up. */\n busy: boolean\n }>()\n\n constructor(\n control: WslSkillProviderControl,\n io: WslSkillIo = nodeSkillIo,\n refreshMs: number = REFRESH_POLL_MS,\n discoveryMs: number = DISCOVERY_POLL_MS,\n ) {\n this.control = control\n this.io = io\n this.refreshMs = refreshMs\n // Counted in polls rather than read off the clock: a poll is what the\n // cadence is measured in, and an injected clock (tests) may stand still.\n this.walkEveryPolls = Math.max(1, Math.round(discoveryMs / refreshMs))\n }\n\n /**\n * Discover nested project skills for a WSL UNC session workspace.\n * @param options - lookup options; `cwd` selects the WSL workspace.\n * @returns candidates for every `.dsh/skills` / `.agents/skills` under the\n * session's scan root — the nearest `.git` ancestor of the cwd, else the\n * cwd itself — or an empty array for non-WSL lookups.\n */\n async list(options: WslSkillLookupOptions): Promise<WslSkillCandidate[]> {\n this.control.signal.throwIfAborted()\n options.signal?.throwIfAborted()\n const unc = options.cwd === undefined ? null : parseWslUnc(options.cwd)\n if (unc === null) return []\n // Host parity: the session's project root is the nearest `.git` ancestor\n // of the cwd, so lookups from inside a project subtree still see that\n // project's skills; nested projects below it join via the bounded BFS.\n // Without a `.git` ancestor the session cwd itself is the scan root (the\n // issue #10 workspace layout).\n const scanRoot = (await nearestGitAncestor(unc.distro, unc.linuxPath, this.io)) ?? unc.linuxPath\n const cacheKey = `${unc.distro}\\u0000${scanRoot}`\n const cached = this.cache.get(cacheKey)\n if (cached !== undefined) {\n // Published already: serve it without touching the share again. The\n // detector this lookup once started is what notices a change, and it\n // drops this entry before the next request can be served from it.\n this.cache.delete(cacheKey)\n this.cache.set(cacheKey, cached)\n return [...cached]\n }\n const roots = await discoverSkillRoots(unc.distro, scanRoot, this.io)\n const candidates: WslSkillCandidate[] = []\n const seenSkills = new Set<string>()\n // The catalog's shape (roots, entry names, kinds and one modification stamp\n // per skill file) is collected as the candidates are built, so the change\n // detector starts from what this lookup actually saw instead of paying for a\n // second walk and a second round of stats.\n const signature: string[] = []\n for (const root of roots) {\n const entries = await listSkillEntries(root, this.io)\n signature.push(await shapeOfRoot(root, entries, this.io))\n for (const entry of entries) {\n options.signal?.throwIfAborted()\n const parsed = await readSkill(entry.path, this.io, options.signal)\n if (parsed === undefined) continue\n // A project reachable through both its real path and a directory\n // symlink yields aliasing roots whose locators differ; publish each\n // distinct (name, body) once so the catalog shows no duplicates.\n const fingerprint = `${parsed.name}\\u0000${parsed.content}`\n if (seenSkills.has(fingerprint)) continue\n seenSkills.add(fingerprint)\n candidates.push({\n name: parsed.name,\n description: parsed.description,\n ...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},\n invocation: parsed.invocation,\n source: root.source,\n provider: this.name,\n rank: root.rank,\n locator: {\n path: entry.path,\n directory: entry.kind === 'bundle'\n ? joinWindowsPath(entry.path, '..')\n : root.path,\n },\n path: entry.path,\n })\n }\n }\n this.cache.set(cacheKey, candidates)\n while (this.cache.size > CACHE_MAX_ENTRIES) {\n const oldest = this.cache.keys().next().value\n if (oldest === undefined) break\n this.cache.delete(oldest)\n }\n this.watch(cacheKey, unc.distro, scanRoot, signature.join('\\u0002'), roots)\n // A copy, so a caller that mutates the list it got cannot poison the cache.\n return [...candidates]\n }\n\n /**\n * Keep one scan root's catalog honest for as long as this provider is\n * registered: the host cannot watch a UNC workspace, so the provider polls the\n * shape it just published and, on any difference, drops its own cache and asks\n * the registry to re-collect for the session's next request.\n *\n * Two passes keep that affordable: the cheap one (every\n * {@link REFRESH_POLL_MS}) re-reads the skills directories already published\n * and re-stats their skill files, so an added, removed or edited skill is\n * noticed on its own; the full re-discovery walk (every\n * {@link DISCOVERY_POLL_MS}) is what can find a skills directory that did not\n * exist before. Neither pass reads a skill file: the description the catalog\n * shows changes only through the registry's revision.\n * @param cacheKey - this provider's key for the scan root.\n * @param distro - the WSL distribution.\n * @param scanRoot - the Linux path the lookup scanned.\n * @param signature - the shape the lookup just published.\n * @param roots - the skills directories that lookup found.\n */\n private watch(cacheKey: string, distro: string, scanRoot: string, signature: string, roots: readonly SkillRoot[]): void {\n const existing = this.detectors.get(cacheKey)\n if (existing !== undefined) {\n existing.signature = signature\n existing.roots = roots\n return\n }\n const detector = {\n timer: setInterval(() => void this.detect(cacheKey, distro, scanRoot), this.refreshMs),\n signature,\n roots,\n polls: 0,\n busy: false,\n }\n // A pending poll must never hold the host process open (or outlive it).\n if (typeof detector.timer.unref === 'function') detector.timer.unref()\n this.detectors.set(cacheKey, detector)\n this.control.signal.addEventListener('abort', () => {\n clearInterval(detector.timer)\n this.detectors.delete(cacheKey)\n }, { once: true })\n }\n\n /** One poll: the cheap pass always, the discovery walk on its own cadence. */\n private async detect(cacheKey: string, distro: string, scanRoot: string): Promise<void> {\n const detector = this.detectors.get(cacheKey)\n if (detector === undefined || this.control.signal.aborted) return\n // A pass over a slow share can outlast the interval; letting the timer start\n // another one would stack `wsl.exe` calls and make every later poll read a\n // half-finished shape. One pass at a time per scan root.\n if (detector.busy) return\n detector.busy = true\n try {\n detector.polls += 1\n const walk = detector.polls % this.walkEveryPolls === 0\n let signature: string\n try {\n signature = await this.shape(distro, walk ? await discoverSkillRoots(distro, scanRoot, this.io) : detector.roots)\n } catch {\n // A transient read failure keeps the last known shape and retries.\n return\n }\n if (signature === detector.signature) return\n detector.signature = signature\n this.cache.delete(cacheKey)\n this.control.invalidate()\n } finally {\n detector.busy = false\n }\n }\n\n /** The shape of one root set: paths, entry names, kinds and file stamps. */\n private async shape(distro: string, roots: readonly SkillRoot[]): Promise<string> {\n const parts: string[] = []\n for (const root of roots) {\n parts.push(await shapeOfRoot(root, await listSkillEntries(root, this.io), this.io))\n }\n return parts.join('\\u0002')\n }\n\n /**\n * Load a complete skill body for a previously listed candidate.\n * @param candidate - the candidate this provider returned.\n * @param options - lookup options whose signal cancels the read.\n * @returns the full skill, or `undefined` if the file disappeared.\n */\n async get(candidate: WslSkillCandidate, options: WslSkillLookupOptions): Promise<WslSkillDefinition | undefined> {\n this.control.signal.throwIfAborted()\n const parsed = await readSkill(candidate.locator.path, this.io, options.signal)\n if (parsed === undefined || parsed.name !== candidate.name) return undefined\n return {\n name: parsed.name,\n description: parsed.description,\n ...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},\n invocation: parsed.invocation,\n source: candidate.source,\n provider: candidate.provider,\n rank: candidate.rank,\n locator: candidate.locator,\n path: candidate.path,\n content: parsed.content,\n }\n }\n}","/**\n * Host half of dsh-wsl-workspace. Three responsibilities:\n *\n * 1. Materialize a `wsl-<mode>` variant for every healthy roster preset\n * under `<dshHome>/.agent-presets/` (the roster's auto-scanned user\n * root), so the WSL execution world — `shell-wsl` + `fs-wsl` behind one\n * entry-local realm, with `tool-bash`/`tool-fs` consumers — composes with\n * ANY mode instead of being a mode itself; the legacy standalone `wsl`\n * preset directory and stale variants are removed on boot. The preset\n * rows name THIS package's built lib files by absolute path, which the\n * preset mount resolves to `file:` URLs without relying on bare specifier\n * resolution from the preset's home directory.\n *\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\n * distribution discovery, one-level directory listing, path checks, and\n * the per-workspace username store — all over the 9P UNC share.\n * Loopback-only, matching the sensitivity of the privileged configuration\n * surface.\n *\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\n * shell executor can resolve a plain Linux `workdir` to the calling\n * session's distribution.\n * @module dsh-wsl-workspace\n */\n\nimport { Context } from '@deepseek-ai/cordis'\nimport z from '@deepseek-ai/schemastery'\nimport { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { dirname, join } from 'node:path'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { homedir } from 'node:os'\nimport { joinUnc, mntToWindowsPath, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\nimport { canonicalWslUnc, getWindowsWorkspace, getWorkspaceUsername, listWorkspaceKeys, registerWindowsWorkspace, setWorkspaceUsername } from './shared/wsl-credentials.ts'\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\nimport { isWslVariantId, transformPresetForWsl, unquoteScalar, variantIdFor } from './host/variants.ts'\nimport { WslSkillsProvider, type WslSkillsRegistryFace } from './host/wsl-skills.ts'\n\n/** The HTTP route this plugin serves (a relative, same-origin path). */\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\n\n/**\n * Bilingual display labels for the shipped source modes, matching the app's\n * own built-in copy in each language — note the `code` preset is \"PTC 模式\"\n * in the Chinese copy but \"Code mode\" in English. The DSH picker localizes\n * only the four built-in ids itself; `wsl-*` variant ids render the\n * preset.yml text verbatim, so the plugin writes one bilingual string so\n * both locales can identify each variant. Custom presets keep their own\n * name.\n */\nconst MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>> = {\n standard: { en: 'Standard mode', zh: '标准模式' },\n code: { en: 'Code mode', zh: 'PTC 模式' },\n minimal: { en: 'Minimal mode', zh: '极简模式' },\n cordis: { en: 'Creator mode', zh: '创造模式' },\n}\n\n/**\n * Quote a value as a single-line YAML single-quoted scalar. Plain scalars\n * cannot contain `: ` (colon + space), which plain English sentences do —\n * written unquoted they make the whole preset.yml unparsable, dropping the\n * name, description and order together.\n */\nfunction yamlScalar(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/** The variant name for one shipped mode (bilingual) or a custom preset. */\nfunction variantName(presetId: string, sourceName: string): string {\n const labels = MODE_DISPLAY_LABELS[presetId]\n return labels === undefined ? `WSL · ${sourceName}` : `WSL · ${labels.en}(${labels.zh})`\n}\n\n/** The variant description for one shipped mode (bilingual) or a custom preset. */\nfunction variantDescription(presetId: string): string {\n const labels = MODE_DISPLAY_LABELS[presetId]\n const display = labels === undefined ? presetId : `${labels.en}(${labels.zh})`\n return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`\n}\n\n/** Plugin config. */\nexport interface Config {\n /** The route under which the dialog data API is served. */\n route?: string\n}\n\n/** The shape after schemastery applied the defaults. */\ntype ResolvedConfig = Required<Config>\n\n/** The `webServer.register` route contract this plugin consumes. */\ninterface WebServerRoute {\n kind: 'exact'\n path: string\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\n}\n\ninterface WebServerService {\n register(route: WebServerRoute): () => void\n}\n\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\ninterface ShellEnvService {\n register(contributor: {\n name: string\n variables: Readonly<Record<string, { description: string }>>\n resolve(execution: {\n agent?: { session: { header: { cwd?: string } } }\n }): Readonly<Partial<Record<string, string>>>\n }): () => void\n}\n\n/** One directory entry the dialog lists. */\ninterface WslDirEntryWire {\n name: string\n kind: 'directory' | 'file' | 'other'\n}\n\n/** One directory level plus its breadcrumb ancestry. */\ninterface WslDirListingWire {\n path: string\n parent: string | null\n entries: WslDirEntryWire[]\n}\n\n/** The wire envelope every method answers with. */\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\n\nconst MAX_BODY_BYTES = 1024 * 1024\n\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\n\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\n\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\nfunction isLoopback(address: string | undefined): boolean {\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\n}\n\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\nfunction hostNameOf(host: string): string {\n if (host.startsWith('[')) {\n const end = host.indexOf(']')\n return end >= 0 ? host.slice(1, end) : host\n }\n return host.split(':')[0] ?? ''\n}\n\n/** True when the request's `Host` header names a loopback host. */\nfunction isLoopbackHost(host: string | undefined): boolean {\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\n}\n\n/**\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\n * an attacker-controlled segment containing separators or `..` would escape\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\n * @param value - the raw wire value.\n * @returns the validated distro name.\n */\nfunction requireDistro(value: unknown): string {\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\n throw new Error('distro must be a valid WSL distribution name')\n }\n return value\n}\n\n/** Human text for an unknown rejection. */\nfunction messageOf(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/** Write one JSON envelope. */\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n 'x-content-type-options': 'nosniff',\n })\n res.end(JSON.stringify(body))\n}\n\n/** Collect and parse the request body, bounded. */\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\n chunks.push(buffer)\n }\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('request body must be a JSON object')\n }\n return parsed as Record<string, unknown>\n}\n\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\nfunction requireLinuxPath(value: unknown, label: string): string {\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\n throw new Error(`${label} must be an absolute Linux path`)\n }\n return normalizeLinuxPath(value)\n}\n\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\nfunction requireWslUnc(value: unknown): string {\n if (typeof value !== 'string') throw new Error('path must be a string')\n const canonical = canonicalWslUnc(value)\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\n return canonical\n}\n\n/**\n * Resolve one directory listing for the dialog. The 9P share (`\\\\wsl.localhost\\…`)\n * serves only the ext4 volume: `/mnt/<drive>` (drvfs) reads return Access\n * denied, so drvfs paths are read through their Windows drive spelling and\n * `/mnt` itself is synthesized from the drives present on the host.\n */\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\n if (linuxPath === '/mnt') {\n const entries: WslDirEntryWire[] = []\n for (let i = 0; i < 26; i++) {\n const letter = String.fromCharCode(65 + i)\n try {\n const info = statSync(`${letter}:\\\\`)\n if (info.isDirectory()) entries.push({ name: letter.toLowerCase(), kind: 'directory' })\n } catch {\n // Absent drive: skip.\n }\n }\n return { path: '/mnt', parent: '/', entries }\n }\n const winPath = mntToWindowsPath(linuxPath)\n const readPath = winPath !== null ? winPath : joinUnc(distro, linuxPath)\n const dirents = readdirSync(readPath, { withFileTypes: true })\n const entries: WslDirEntryWire[] = dirents\n .slice(0, 1000)\n .map((dirent): WslDirEntryWire => {\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\n ? 'directory'\n : dirent.isFile() ? 'file' : 'other'\n return { name: dirent.name, kind }\n })\n .sort((a, b) => {\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\n return a.name.localeCompare(b.name)\n })\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\n return { path: linuxPath, parent, entries }\n}\n\n/** Cached self-description for the dialog's help panel. */\nlet selfDescription: { version: string; releases: { id: string; status: string }[] } | undefined\n\n/**\n * Read this plugin's own `package.json` for the dialog's help panel: the\n * published version and the declared `dsh.compatibility.dshReleases` matrix.\n * A plugin directory that cannot be read reports empty values rather than\n * failing the dialog, and the result is cached for the process lifetime.\n * @returns the self-description served by the `describe` method.\n */\nfunction describeSelf(): { version: string; releases: { id: string; status: string }[] } {\n if (selfDescription !== undefined) return selfDescription\n try {\n const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8')\n const parsed = JSON.parse(raw) as {\n version?: unknown\n dsh?: { compatibility?: { dshReleases?: Record<string, unknown> } }\n }\n selfDescription = {\n version: typeof parsed.version === 'string' ? parsed.version : 'unknown',\n releases: Object.entries(parsed.dsh?.compatibility?.dshReleases ?? {})\n .map(([id, status]) => ({ id, status: String(status) })),\n }\n } catch {\n selfDescription = { version: 'unknown', releases: [] }\n }\n return selfDescription\n}\n\n/** Route one method dispatch. */\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\n switch (method) {\n case 'listDistros': {\n const distros = await listDistros()\n const fallback = await defaultDistro()\n if (fallback !== undefined && distros.includes(fallback)) {\n return [fallback, ...distros.filter(name => name !== fallback)]\n }\n return distros\n }\n case 'listDir': {\n const distro = requireDistro(params.distro)\n const path = requireLinuxPath(params.path, 'path')\n return listWslDir(distro, path)\n }\n case 'check': {\n const distro = requireDistro(params.distro)\n const path = requireLinuxPath(params.path, 'path')\n // drvfs paths (Access denied over 9P) are checked through their drive spelling.\n const winPath = mntToWindowsPath(path)\n const readPath = winPath !== null ? winPath : joinUnc(distro, path)\n try {\n const info = statSync(readPath)\n return { exists: true, isDirectory: info.isDirectory() }\n } catch {\n return { exists: false, isDirectory: false }\n }\n }\n case 'registerWindows': {\n // A `/mnt/<drive>` workspace registers under its Windows drive path\n // (the registry realpath/stats it, and 9P cannot serve drvfs) with the\n // distro stored for the per-session env contributor.\n const distro = requireDistro(params.distro)\n const linuxPath = requireLinuxPath(params.linuxPath, 'path')\n const winPath = mntToWindowsPath(linuxPath)\n if (winPath === null) throw new Error('registerWindows requires a /mnt/<drive> Linux path')\n const username = typeof params.username === 'string' ? params.username : undefined\n registerWindowsWorkspace(winPath, distro, username)\n return null\n }\n case 'listWorkspaces': {\n // Every registered WSL workspace key (UNC and Windows drive spellings):\n // the client uses the drive keys to recognize `/mnt` workspaces.\n return listWorkspaceKeys()\n }\n case 'describe': {\n // The dialog's help panel reports what this build declares, so the\n // compatibility list can never drift from package.json.\n return describeSelf()\n }\n case 'setUser': {\n const path = requireWslUnc(params.path)\n const username = params.username\n if (username === undefined || username === '') {\n setWorkspaceUsername(path, undefined)\n } else {\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\n }\n setWorkspaceUsername(path, username)\n }\n return null\n }\n default:\n throw new Error(`unknown method \"${method}\"`)\n }\n}\n\n/** The `ctx.agentPresets` roster face this plugin consumes (optional service). */\ninterface AgentPresetsService {\n list(): Promise<{ id: string; broken?: string; path: string }[]>\n read(id: string): Promise<string>\n}\n\n/**\n * Publish a fully staged preset directory while preserving the last complete\n * variant if publication fails. Stable sibling names also let the next boot\n * recover an interrupted old-to-backup rename before doing new work.\n */\nfunction publishVariant(staging: string, dest: string): void {\n const previous = `${dest}.previous`\n if (!existsSync(dest) && existsSync(previous)) renameSync(previous, dest)\n if (existsSync(previous)) rmSync(previous, { recursive: true, force: true })\n if (existsSync(dest)) renameSync(dest, previous)\n try {\n renameSync(staging, dest)\n } catch (error) {\n if (!existsSync(dest) && existsSync(previous)) renameSync(previous, dest)\n throw error\n }\n rmSync(previous, { recursive: true, force: true })\n}\n\n/**\n * Whether a host's terminal stack can allocate a PTY process *on this platform*.\n *\n * The world's `bash` is the host's persistent-shell stack, and on Windows that\n * stack needs a platform process inspector. `@deepseek-ai/dsh-subprocess-local`\n * only grew one in `0.1.0-rc.8`: in `0.1.0-rc.7` `spawnTerminal` throws\n * `subprocess-local: terminal inspection is unsupported on platform win32`\n * before the process is started, so *every* persistent shell fails there — a\n * real session shows the model getting that error for each `bash` call while\n * grep/glob (which never touch the PTY) keep working. The host itself ships the\n * same gap: that release's Minimal preset mounts `persistent-bash` with no\n * `disabled:` guard for Windows.\n *\n * The probe asks the substrate the question directly instead of pattern-matching\n * a version: `spawnTerminal` builds its inspector before `node-pty` starts the\n * program, so handing it a program that cannot exist reaches that check and\n * nothing else — no process is created either way, and the failure message says\n * which half failed. A release that can build the inspector reports an ordinary\n * spawn failure instead, which is the \"supported\" answer.\n * @param ctx - plugin context; the `subprocess` service is looked up with `get`\n * and waited for briefly, because the world is generated during profile boot.\n * @returns true when the persistent shell may be mounted.\n */\nasync function supportsPersistentShell(ctx: Context): Promise<boolean> {\n // POSIX hosts have an inspector on every declared release, and a WSL world is\n // Windows-only anyway.\n if (process.platform !== 'win32') return true\n const subprocess = await waitForSubprocess(ctx)\n if (subprocess?.spawnTerminal === undefined) return true\n try {\n const handle = await subprocess.spawnTerminal({\n argv: ['dsh-wsl-workspace-pty-probe-does-not-exist'],\n cwd: process.cwd(),\n rows: 24,\n cols: 80,\n graceMs: 1_000,\n })\n // Unexpectedly alive: this host starts a PTY for a missing program, so the\n // terminal stack works. Take the probe process down again.\n await handle?.terminate?.()\n return true\n } catch (error) {\n return !isTerminalInspectionUnsupported(error)\n }\n}\n\n/**\n * Whether one spawn failure is the missing-platform-inspector error.\n * @param error - the rejection from `spawnTerminal`.\n * @returns true when the host cannot inspect terminal processes here.\n */\nexport function isTerminalInspectionUnsupported(error: unknown): boolean {\n return /terminal inspection is unsupported on platform/i.test(messageOf(error))\n}\n\n/** The `subprocess` service face the probe needs. */\ninterface SubprocessProbeFace {\n spawnTerminal(spec: {\n argv: readonly string[]\n cwd: string\n rows: number\n cols: number\n graceMs: number\n }): Promise<{ terminate?: () => Promise<void> | void } | undefined>\n}\n\n/**\n * Look up the `subprocess` service, giving profile boot a moment to publish it.\n *\n * Bounded: the world is generated in a fire-and-forget effect, so this wait never\n * blocks profile boot, and the service is normally already published by the base\n * bundles the web app mounts before this plugin's own injection resolves. An\n * absent service is answered as \"supported\" by the caller, which is the\n * behaviour this plugin shipped before the probe existed.\n * @param ctx - plugin context.\n * @returns the service, or undefined when this deployment has none yet.\n */\nasync function waitForSubprocess(ctx: Context): Promise<SubprocessProbeFace | undefined> {\n for (let attempt = 0; attempt < 20; attempt += 1) {\n const service = ctx.get('subprocess') as unknown as SubprocessProbeFace | undefined\n if (service !== undefined) return service\n await new Promise(resolve => setTimeout(resolve, 100))\n }\n return undefined\n}\n\n/** Materialize one WSL variant per healthy source preset. */\nasync function materializeVariants(\n agentPresets: AgentPresetsService,\n dshHome: string,\n paths: { shell: string; fs: string; relay: string; node: string; sandbox: string; search: string; jobs: string },\n persistentShell: boolean,\n): Promise<void> {\n const presets = await agentPresets.list()\n const userRoot = join(dshHome, '.agent-presets')\n const generated = new Set<string>()\n for (const preset of presets) {\n if (preset.broken !== undefined) continue\n if (isWslVariantId(preset.id)) continue\n const variantId = variantIdFor(preset.id)\n const source = await agentPresets.read(preset.id)\n const transformed = transformPresetForWsl(source, paths.shell, paths.fs, persistentShell ? {\n relayPath: paths.relay,\n nodePath: paths.node,\n sandboxPath: paths.sandbox,\n } : undefined, paths.search, paths.jobs)\n const dir = join(userRoot, variantId)\n const staging = `${dir}.staging`\n rmSync(staging, { recursive: true, force: true })\n // A preset directory is an opaque, self-contained plugin unit. Mirror it\n // without interpreting local code or asset names, then overwrite only the\n // two files owned by this generator.\n cpSync(dirname(preset.path), staging, { recursive: true, force: true })\n writeFileSync(join(staging, 'agent.cordis.yml'), transformed, 'utf8')\n const labels = MODE_DISPLAY_LABELS[preset.id]\n let name = variantName(preset.id, preset.id)\n let orderLine = ''\n try {\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\n if (labels === undefined) {\n // Custom presets keep their own display name; shipped modes use the\n // bilingual labels above so both locales can identify the variant.\n const match = /^name:\\s*(.+)$/m.exec(meta)\n if (match?.[1] !== undefined && match[1].trim() !== '') {\n // The scalar is copied out of the source's YAML as written, so a\n // quoted `name: 'Data mode'` would otherwise reach the picker with\n // its quotes doubled into the variant's own scalar.\n name = variantName(preset.id, unquoteScalar(match[1].trim()))\n }\n }\n // Inherit the source's declared order so the WSL variants line up with\n // the local modes in the roster (standard, PTC, minimal, cordis).\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\n } catch {\n // Absent or unreadable display metadata falls back to the id-based name.\n }\n writeFileSync(\n join(staging, 'preset.yml'),\n `name: ${yamlScalar(name)}\\n`\n + orderLine\n + `description: ${yamlScalar(variantDescription(preset.id))}\\n`,\n 'utf8',\n )\n publishVariant(staging, dir)\n generated.add(variantId)\n }\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n if (entry.name === 'wsl') {\n // The legacy standalone WSL mode: folded into the variants above.\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\n continue\n }\n if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue\n if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })\n }\n}\n\n/** Function-plugin plugin contract. */\nexport const name = 'dsh-wsl-workspace'\n\n/** Required services. */\nexport const inject = ['webServer']\n\n/** Validated plugin config (schemastery applied the defaults). */\nexport const Config: z<Config> = z.object({\n route: z.string().default(DEFAULT_ROUTE),\n})\n\n/**\n * Apply the host half: materialize a `wsl-<mode>` variant for every healthy\n * roster preset, register the data route, and\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\n * shell executor can resolve a plain Linux `workdir` to the calling\n * session's distribution.\n * @param ctx - the host plugin context.\n * @param config - the validated configuration.\n */\nexport function apply(ctx: Context, config: Config): void {\n const resolved = config as ResolvedConfig\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\n // The persistent shell runs the host PTY backend on the relay, which starts\n // `wsl.exe … bash` under that PTY: two paths the generated preset must carry.\n const relayPath = join(packageRoot, 'lib', 'wsl-relay.js').replace(/\\\\/g, '/')\n const sandboxPath = join(packageRoot, 'lib', 'wsl-sandbox.js').replace(/\\\\/g, '/')\n // The in-distribution `grep`/`glob` twin that replaces the host search suite.\n const searchPath = join(packageRoot, 'lib', 'wsl-search.js').replace(/\\\\/g, '/')\n // The persistent shell has no `run_in_background` of its own; this is the\n // producer that gives the world's `job_*` tools something to track.\n const jobsPath = join(packageRoot, 'lib', 'wsl-jobs.js').replace(/\\\\/g, '/')\n const nodePath = process.execPath.replace(/\\\\/g, '/')\n\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\n if (agentPresets !== undefined) {\n ctx.effect(() => {\n void (async () => {\n const persistentShell = await supportsPersistentShell(ctx)\n await materializeVariants(agentPresets, dshHome, {\n shell: shellPath,\n fs: fsPath,\n relay: relayPath,\n node: nodePath,\n sandbox: sandboxPath,\n search: searchPath,\n jobs: jobsPath,\n }, persistentShell)\n })().catch((error) => {\n // Variant generation is best-effort over a live roster: a missing or\n // unreadable source preset must not take the whole plugin down, but\n // the failure is surfaced loudly rather than hidden.\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\n })\n return () => {}\n }, 'dsh-wsl-workspace: WSL preset variants')\n }\n\n const skills = ctx.get('skills') as unknown as WslSkillsRegistryFace | undefined\n if (skills !== undefined && typeof skills.registerProvider === 'function') {\n // The shipped skill-filesystem provider scans only the session cwd's\n // project root, so WSL workspaces whose `.dsh/skills` live in nested\n // projects would show an empty skill catalog. This provider mirrors the\n // host's project discovery for WSL UNC workspaces, bounded to the\n // workspace root (issue #10). The method check keeps a host whose\n // `skills` service exists with a different shape from breaking plugin\n // load; the provider is an enhancement, never a load-time requirement.\n ctx.effect(() => skills.registerProvider(\n control => new WslSkillsProvider(control),\n ), 'dsh-wsl-workspace: WSL workspace skills provider')\n }\n\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\n if (shellEnv !== undefined) {\n ctx.effect(() => shellEnv.register({\n name: 'wsl-workspace-distro',\n variables: {\n DSH_WSL_DISTRO: {\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\n },\n DSH_WSL_USER: {\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\n },\n },\n resolve(execution) {\n const cwd = execution.agent?.session.header.cwd\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\n if (unc !== null) {\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\n return username === undefined || username === ''\n ? { DSH_WSL_DISTRO: unc.distro }\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\n }\n // A Windows-drive cwd belongs to a `/mnt/<drive>` WSL workspace (9P\n // cannot serve drvfs, so those register under their drive path): the\n // stored distro drives `wsl.exe -d` for bash.\n if (cwd !== undefined && /^[A-Za-z]:[\\\\/]/.test(cwd)) {\n const entry = getWindowsWorkspace(cwd)\n if (entry !== undefined && entry.distro !== undefined && entry.distro !== '') {\n return entry.username === undefined || entry.username === ''\n ? { DSH_WSL_DISTRO: entry.distro }\n : { DSH_WSL_DISTRO: entry.distro, DSH_WSL_USER: entry.username }\n }\n }\n return {}\n },\n }), 'dsh-wsl-workspace: per-session distro env fact')\n }\n\n const webServer = ctx.get('webServer') as unknown as WebServerService\n ctx.effect(() => webServer.register({\n kind: 'exact',\n path: resolved.route,\n handler: async (req, res) => {\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\n json(res, 403, { ok: false, error: 'loopback-only' })\n return\n }\n if (req.method !== 'POST') {\n json(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n let body: Record<string, unknown>\n try {\n body = await readBody(req)\n } catch (error) {\n json(res, 400, { ok: false, error: messageOf(error) })\n return\n }\n const method = typeof body.method === 'string' ? body.method : ''\n const params = body.params === undefined ? {} : body.params\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\n json(res, 400, { ok: false, error: 'params must be an object' })\n return\n }\n try {\n const value = await dispatch(method, params as Record<string, unknown>)\n json(res, 200, { ok: true, value })\n } catch (error) {\n json(res, 200, { ok: false, error: messageOf(error) })\n }\n },\n }), 'dsh-wsl-workspace: dialog data route')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["spans: { start: number; end: number }[]","kept: string[]","nodeSkillIo: WslSkillIo","entries: Dirent[] | undefined","children: [string, number][]","links: [string, number][]","roots: SkillRoot[]","frontier: [string, number][]","layer: [string, number][]","nextLayer: [string, number][]","result: SkillRoot[]","dirents: Dirent[]","entries: SkillEntry[]","joinWindowsPath","raw: string","value","name","collected: string[]","indent: string | undefined","candidates: WslSkillCandidate[]","signature: string[]","signature: string","parts: string[]","MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>>","chunks: Buffer[]","entries: WslDirEntryWire[]","kind: WslDirEntryWire['kind']","selfDescription: { version: string; releases: { id: string; status: string }[] } | undefined","name","declaration: PresetDeclaration","Config: z<Config>","disposers: (() => Promise<void>)[]","body: Record<string, unknown>"],"sources":["../src/host/variants.ts","../src/host/wsl-skills.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', 'str-replace-editor', 'tool-str-replace-editor', 'wsl-world', 'filesystem', 'persistent-shell', 'persistent-bash', 'persistent-pwsh', 'terminal-bash', 'terminal-pwsh', 'custom-bash', 'bootstrap-filesystem'])\n\n/** Execution-world rows that register the model-facing editor tool. */\nconst EDITOR_ROWS = new Set(['str-replace-editor', 'tool-str-replace-editor'])\n\n/** The row ids this generator mounts inside its own world group. */\nconst WSL_WORLD_PROVIDER_IDS = ['shell-wsl', 'fs-wsl']\n\n/**\n * Whether a top-level row is this generator's own WSL world group.\n *\n * A preset copied from a generated variant (a user renaming `wsl-standard` to\n * their own mode, say) carries the group under whatever id the copy kept, so\n * the mounted provider ids identify it even when the row id changed. Without\n * that check the variant would carry two world groups and the loader refuses\n * the whole preset with `duplicate loader entry id: wsl-world`.\n * @param block - the row's lines.\n * @returns true when the row mounts this generator's WSL providers.\n */\nfunction isWslWorldGroup(block: readonly string[]): boolean {\n const text = block.join('\\n')\n return WSL_WORLD_PROVIDER_IDS.some((id) => new RegExp(`^\\\\s+- id: ${id}$`, 'm').test(text))\n}\n\n/**\n * The persistent-shell rows: the host's own PTY registry and its config-driven\n * backend, told to run this plugin's relay (so the shell is a WSL one), plus\n * the persistent tool that consumes them.\n *\n * Two shapes matter here. The registry is an agent-owned service — the shipped\n * `persistent-shell` group keeps it in its own `terminals` realm for that\n * reason — so this is a nested group of the world rather than three flat rows.\n * And the persistent tool registers the tool name **`bash`**, exactly like the\n * one-shot `dsh-tool-bash` row it replaces: both cannot be mounted (the tools\n * registry rejects the duplicate and the whole preset fails to load), which is\n * also why DSH's own Minimal mode describes itself as \"a single-tool agent with\n * a persistent shell\". So a world with the relay paths swaps the shell tool\n * instead of adding one, and a world without them keeps the one-shot row.\n */\nfunction persistentShellRows(relayPath: string, nodePath: string): string[] {\n return [\n ' # Persistent shell: the host PTY registry and its backend, running',\n ' # this plugin\\'s relay, which hands the PTY to `wsl.exe … bash`',\n ' # (distribution and user come from the session). It registers the',\n ' # `bash` tool, so it takes the place of the one-shot row above.',\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 ' - id: terminal-wsl',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' backendType: wsl',\n ' shellDialect: bash',\n ` shellPath: '${nodePath.replace(/'/g, \"''\")}'`,\n ' shellArgs:',\n ` - '${relayPath.replace(/'/g, \"''\")}'`,\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' backendType: wsl',\n ...SHELL_DESCRIPTION_ROWS,\n ]\n}\n\n/**\n * The persistent tool's model-facing description, replacing the host default.\n *\n * The host tool's default says only that state persists, and its own Minimal\n * preset goes further in the wrong direction by suggesting `sleep 10 &`. Both\n * facts a WSL session needs are missing, and both were learned the hard way:\n *\n * - The shell is one process for the whole Agent, so a `cd` in one call decides\n * where the *next* call starts. A model that read the one-shot tool's contract\n * (\"each call runs in a fresh shell — pass `workdir` instead of using `cd`\")\n * will be surprised, and a probe left in `/tmp` makes every later relative path\n * resolve somewhere it never named.\n * - The host wraps each command as `eval -- $'…'`. A trailing `&` therefore\n * backgrounds the *whole* wrapped command: the tool's completion marker is\n * printed before the work runs, so the call reports no output and exit code 0\n * while the real output arrives later and can land inside the next call's\n * output window. `( … ) &` on its own line keeps the `&` on the subshell and\n * leaves the wrapper's sequencing intact.\n *\n * `description` is a supported key on every declared release (its `Config` schema\n * carries it from 0.1.0-rc.7 on), so the override is version-safe.\n */\nconst SHELL_DESCRIPTION_ROWS = [\n ' description: |-',\n ' Run commands in a persistent bash shell inside this WSL distribution. State, including',\n ' the current directory and exported environment variables, persists across calls: use',\n ' absolute paths or an explicit `cd` at the start of a command instead of relying on where',\n ' the previous call left the shell. The shell runs as the workspace\\'s Linux user; Windows',\n ' files are reachable as /mnt/<drive>, and no toolchain install is required.',\n ' * This tool takes `command` only. It has no `run_in_background` parameter, and passing',\n ' one is ignored - use the `bash_background` tool when the mode provides it, or background',\n ' a subshell as below.',\n ' * To leave work running without a tracked job, put it in a subshell with the `&` on its',\n ' own line: `( long-job > log 2>&1 ) &`. Then poll the log file in a later call.',\n ' * Never end a `&&` chain with `&`. That backgrounds the whole command, so the call',\n ' returns immediately with no output and exit code 0, and the real output arrives later,',\n ' possibly inside the next call\\'s output.',\n ' * Avoid commands that wait for stdin: an interactive foreground child can run until the',\n ' command timeout, and a timeout resets the shell and discards its state.',\n]\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(\n shellPath: string,\n fsPath: string,\n includeEditor: boolean,\n persistent?: { relayPath: string; nodePath: string; sandboxPath: string },\n searchPath?: string,\n jobsPath?: string,\n sawJobs = false,\n): 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 '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n // The Windows confinement runner cannot describe a Linux path, so the\n // world owns the capability (see the sandbox row below) whenever it mounts\n // a shell that would otherwise call it.\n ...(persistent === undefined ? [] : [' sandbox: true']),\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ...(persistent === undefined\n ? []\n : [\n ' - id: sandbox-wsl',\n ` name: '${persistent.sandboxPath.replace(/'/g, \"''\")}'`,\n ]),\n // The shell tool comes from the persistent-shell group when this world has\n // one (same `bash` name, so only one of the two may be mounted).\n ...(persistent === undefined\n ? [' - id: tool-bash', \" name: '@deepseek-ai/dsh-tool-bash'\"]\n : []),\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n // `grep`/`glob` run inside the distribution, not through the host suite's\n // Windows ripgrep: same tool names, same model-facing contract, Linux paths\n // and Linux symlinks. Older editor builds pass no cwd, so the provider\n // inherits it from the current tool execution.\n ...(searchPath === undefined\n ? []\n : [\n ' - id: search-wsl',\n ` name: '${searchPath.replace(/'/g, \"''\")}'`,\n ]),\n // The world's `bash` is the host's *persistent* tool, whose schema declares\n // only `command`: nothing here can start a tracked background job, so the\n // host's `job_*` tools would always answer \"no background jobs\" and a\n // `run_in_background` argument would be silently ignored. This row restores\n // the producer — but only for a source that also mounts the `job_*` control\n // tools, because a producer without a reader hands out ids nothing can use\n // (Minimal mode mounts neither). A world that keeps the one-shot bash row\n // needs no producer either: that tool carries `run_in_background` itself.\n ...(jobsPath === undefined || persistent === undefined || !sawJobs\n ? []\n : [\n ' - id: jobs-wsl',\n ` name: '${jobsPath.replace(/'/g, \"''\")}'`,\n ]),\n // The editor resolves through this entry-local WSL fs.\n // Anchored-family presets require this name during bootstrap.\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 ...(persistent === undefined ? [] : persistentShellRows(persistent.relayPath, persistent.nodePath)),\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 upstream local-skill provider row, whose watcher cannot watch a UNC share. */\nconst SKILL_FILESYSTEM_ROW = 'skill-filesystem'\n\n/**\n * Turn the local skill provider's watcher off inside a WSL variant.\n *\n * `@deepseek-ai/dsh-skill-filesystem` reports an INCOMPLETE observation when\n * its watcher fails to start, and `dsh-tool-skill` withholds the whole catalog\n * while a snapshot is incomplete. chokidar cannot watch\n * `\\\\wsl.localhost\\<distro>\\...`, so in a WSL session the model never sees the\n * catalog at all - `skill` still loads one by name, but nothing tells the model\n * which skills exist. With the watcher off the discovery completes (the\n * plugin's own provider rescans the share on demand, and the next session gets\n * a fresh catalog); the price is no live refresh inside one running session,\n * which never worked on this substrate anyway.\n * @param block - the row's lines.\n * @returns the row's lines with `watch: false` merged into its config.\n */\nfunction disableSkillWatch(block: readonly string[]): string[] {\n const lines = [...block]\n // An explicit `watch:` from the author wins.\n if (lines.some(line => /^\\s*watch:/.test(line))) return lines\n const configIndex = lines.findIndex(line => /^\\s*config:\\s*$/.test(line))\n if (configIndex >= 0) {\n const childIndent = `${/^(\\s*)/.exec(lines[configIndex] ?? '')?.[1] ?? ' '} `\n lines.splice(configIndex + 1, 0, `${childIndent}watch: false`)\n return lines\n }\n const rowIndent = /^(\\s*)/.exec(lines[0] ?? '')?.[1] ?? ''\n // Insert before the row block's trailing blank line, not after it.\n let insertAt = lines.length\n while (insertAt > 0 && (lines[insertAt - 1] ?? '').trim() === '') insertAt -= 1\n lines.splice(insertAt, 0, `${rowIndent} config:`, `${rowIndent} watch: false`)\n return lines\n}\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/** Persona config scalars that carry model-facing text, in append preference order. */\nconst PERSONA_TARGET_FIELDS = ['suffix', 'text', 'prefix'] as const\n\n/** One persona config scalar: where it sits, and whether it is a block header. */\ninterface PersonaTarget {\n /** Line index of the scalar inside the persona block. */\n index: number\n /** Field name: `suffix`, `text` or `prefix`. */\n field: string\n /** Indentation of the scalar's own line. */\n indent: number\n /** Block header (`>-`, `|-`, …) when the scalar is folded/literal, else ''. */\n header: string\n /** Unquoted inline value when the scalar is inline, else ''. */\n inline: string\n}\n\n/** Strip one layer of YAML quoting so a value can move into a block scalar. */\nexport function unquoteScalar(value: string): string {\n const single = /^'(.*)'$/s.exec(value)\n if (single !== null) return (single[1] ?? '').replace(/''/g, \"'\")\n const double = /^\"(.*)\"$/s.exec(value)\n return double !== null ? (double[1] ?? '') : value\n}\n\n/**\n * Locate the persona scalar this transform amends.\n *\n * DSH moved the model-facing prompt text across releases: `text` up to\n * v0.1.2-rc.1, and `prefix` + `suffix` from v0.1.3-alpha.2 on. `suffix` wins\n * when both exist because it carries the working-directory sentence, so the\n * appended note lands exactly where the legacy `text` block put it; `prefix`\n * is the last resort for a composition that has neither. A known field with an\n * empty value is not amendable.\n * @param block - the persona row's lines.\n * @returns the target scalar, or undefined when the row carries none.\n */\nfunction personaTarget(block: readonly string[]): PersonaTarget | undefined {\n for (const field of PERSONA_TARGET_FIELDS) {\n for (let index = 0; index < block.length; index += 1) {\n const match = new RegExp(`^(\\\\s*)${field}:\\\\s*(.*)$`).exec(block[index] ?? '')\n if (match === null) continue\n const indent = match[1]?.length ?? 0\n const rest = (match[2] ?? '').trim()\n if (rest === '' || /^[|>][+-]?$/.test(rest)) {\n return { index, field, indent, header: rest, inline: '' }\n }\n return { index, field, indent, header: '', inline: unquoteScalar(rest) }\n }\n }\n return undefined\n}\n\n/**\n * Index of the last line belonging to the block scalar whose header is at\n * `headerIndex`. A non-blank line at or above the header's indentation is the\n * next sibling key and ends the scalar — without that stop a persona carrying\n * both `suffix` and `prefix` blocks would take the note in the wrong one.\n * @param block - the persona row's lines.\n * @param headerIndex - index of the `field: >-` header line.\n * @param indent - the header's own indentation.\n * @returns the last content line's index, or -1 when the scalar is empty.\n */\nfunction lastBlockLine(block: readonly string[], headerIndex: number, indent: number): number {\n let last = -1\n for (let index = headerIndex + 1; index < block.length; index += 1) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n const lineIndent = /^(\\s*)/.exec(line)?.[1]?.length ?? 0\n if (lineIndent <= indent) break\n last = index\n }\n return last\n}\n\n/** Whether a top-level span is a `persona` row this transform may amend. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end)\n // `complete: true` asks the persona plugin for no runtime context at all.\n if (block.join('\\n').includes('complete: true')) return false\n const target = personaTarget(block)\n if (target === undefined) return false\n return target.header === ''\n ? target.inline !== ''\n : lastBlockLine(block, target.index, target.indent) >= 0\n}\n\n/**\n * Append the WSL sentence to a persona row's model-facing scalar, in place.\n *\n * A block scalar takes the sentence as a sibling line, which is what the\n * legacy `text: >-` form always did. An inline scalar cannot: it is folded into\n * a block scalar first, so the sentence joins it the same way (and the model\n * sees the same text it saw before v0.1.3-alpha.2).\n * @param lines - the whole composition's lines.\n * @param span - the persona row's span.\n * @returns the persona row's lines, amended when a target was found.\n */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = [...lines.slice(span.start, span.end)]\n const target = personaTarget(block)\n if (target === undefined) return block\n const pad = ' '.repeat(target.indent)\n if (target.header !== '') {\n const last = lastBlockLine(block, target.index, target.indent)\n if (last < 0) return block\n const textIndent = /^(\\s*)/.exec(block[last] ?? '')?.[1] ?? `${pad} `\n block.splice(last + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return block\n }\n const child = `${pad} `\n block.splice(\n target.index,\n 1,\n `${pad}${target.field}: >-`,\n `${child}${target.inline}`,\n `${child}${PERSONA_APPEND.trim()}`,\n )\n return block\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. A row id that appears twice in the source is kept once, and a\n * world group the source already carries (a preset copied from a generated\n * variant) is replaced rather than duplicated, so the variant always mounts\n * exactly one world pointing at this installation's providers.\n *\n * The source's own persistent-shell group is not re-added as such: it registers\n * the same `bash` tool name as the WSL world's `dsh-tool-bash`, and the tools\n * registry rejects duplicates within one preset layer — the whole variant fails\n * to mount and the session falls back to another preset. Its PTY *backend*, on\n * the other hand, is exactly what a stateful WSL shell needs, and it is\n * config-driven: given the relay in {@link persistentShellRows} it runs\n * `wsl.exe … bash` under the host's PTY, and its tool registers the separate\n * `persistent-bash` name, so the one-shot `bash` above stays as it is.\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 * @param persistent - the relay, interpreter and sandbox provider a stateful\n * shell needs; omit to generate a world without the persistent-shell rows.\n * @param searchPath - absolute path of the plugin's built in-distribution\n * `grep`/`glob` tools; mounted only for a source that had `tool-fs-search`.\n * @param jobsPath - absolute path of the plugin's built background-job producer\n * for the persistent shell; mounted only alongside that shell.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(\n source: string,\n shellPath: string,\n fsPath: string,\n persistent?: { relayPath: string; nodePath: string; sandboxPath: string },\n searchPath?: string,\n jobsPath?: string,\n): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n const seen = new Set<string>()\n let sawEditor = false\n let sawSearch = false\n let sawJobs = 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 // Two top-level rows with one id are a loader error (\"duplicate loader\n // entry id: <id>\"), which makes the whole variant unmountable, so the first\n // occurrence wins and a copied later one is dropped.\n if (seen.has(id)) continue\n seen.add(id)\n const block = lines.slice(span.start, span.end)\n if (WORLD_ROWS.has(id) || isWslWorldGroup(block)) {\n if (EDITOR_ROWS.has(id)) sawEditor = true\n if (id === 'tool-fs-search') sawSearch = true\n continue\n }\n if (id === SKILL_FILESYSTEM_ROW) {\n kept.push(...disableSkillWatch(block))\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(...block)\n }\n // An editor mounted inside a source group (a copied variant, say) also means\n // the composition expects one, so the injected world keeps its editor row. The\n // search tools and the background-job producer follow the same rule: a mode\n // without them (minimal) must not gain them, and a mode with them must not lose\n // them. `tool-jobs` is what registers `job_list`/`job_output`/`job_kill`.\n if (source.includes('str-replace-editor')) sawEditor = true\n if (source.includes('tool-fs-search')) sawSearch = true\n if (source.includes('tool-jobs')) sawJobs = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor, persistent, sawSearch ? searchPath : undefined, jobsPath, sawJobs))\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","/**\n * WSL workspace skill provider (host half).\n *\n * DSH's shipped skill-filesystem provider scans only the session cwd's\n * project root (the nearest `.git` ancestor) for `.dsh/skills` / `.agents/skills`\n * and never descends into nested projects. A WSL workspace whose project\n * folders live below the registered workspace root therefore shows an empty\n * skill catalog, even though the same layout works when the session cwd is\n * the project folder itself (issue #10).\n *\n * This provider mirrors the host's discovery rules for WSL UNC session\n * workspaces: it starts at the session cwd's nearest `.git` ancestor (the\n * host's project-root rule; the cwd itself when no ancestor has a `.git`\n * marker), then walks that root (depth- and budget-bounded). Directory\n * symlinks are followed when the substrate resolves them, and — the case the\n * `\\\\wsl.localhost` 9P share creates, where a link is listed but its Linux\n * target cannot be resolved Windows-side — the distribution itself resolves\n * them through bounded, concurrent `wsl.exe … readlink -f` calls. The walk\n * collects every\n * `.dsh/skills` and `.agents/skills` directory it finds — including nested\n * projects, linked-in projects anywhere on the Linux filesystem, and projects\n * reachable through more than one path — and publishes their skills with the\n * same project ranks and sources the host uses, so precedence and duplicate\n * resolution behave identically. Non-WSL lookups return nothing and leave\n * the host's own providers untouched.\n *\n * All filesystem reads go through `node:fs` against the `\\\\wsl.localhost\\…`\n * 9P share (the same substrate `WslFileSystem` uses); the distribution-side\n * resolution rides `wsl.exe` through `execFile` (no shell interpolation), and\n * an injectable IO face keeps the discovery logic unit-testable without a\n * live distro.\n *\n * @module dsh-wsl-workspace/host/wsl-skills\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { join as joinWindowsPath, posix } from 'node:path'\nimport { isAbsoluteLinuxPath, joinUnc, parseWslUnc, uncToLinux } from '../shared/paths.ts'\nimport { resolveLinuxSymlinks } from '../shared/links.ts'\n\n/** Project ranks copied from @deepseek-ai/dsh-skill-filesystem so WSL and host entries interleave identically. */\nconst PROJECT_DSH_RANK = 100\nconst PROJECT_AGENTS_RANK = 200\n\n/** How many directory levels below the workspace root are scanned. */\nconst MAX_SCAN_DEPTH = 4\n/** Maximum distinct skill directories published per lookup. */\nconst MAX_SKILL_ROOTS = 64\n/** Maximum directories visited per lookup (an absolute blast-radius cap). */\nconst MAX_VISITED_DIRECTORIES = 4096\n/**\n * How many Linux symlinks one lookup may hand to the distribution (a second\n * blast-radius cap, and a latency cap: each resolution is a short `wsl.exe`\n * call, so a tree with hundreds of links cannot stall the catalog).\n */\nconst MAX_LINK_RESOLUTIONS = 32\n/** How many parent levels above the session cwd are searched for a `.git` project marker. */\nconst MAX_ANCESTOR_WALK = 64\n/**\n * How many directories one discovery layer may probe at once.\n *\n * A `readdir` over the `\\\\wsl.localhost\\…` share measured 15.6 ms against 1.6 ms\n * for a `stat`, and a budget-sized walk is 4096 directories: probing one\n * directory at a time costs 20-30 s, which is what a turn in a large workspace\n * used to wait for. The layer stays bounded so the share is never flooded; node's\n * own filesystem thread pool is what ultimately caps the real parallelism.\n */\nconst WALK_CONCURRENCY = 16\n/** Maximum cached lookups (one entry per distinct scan root across sessions). */\nconst CACHE_MAX_ENTRIES = 32\n/**\n * How often a served scan root is re-checked for catalog changes: the skills\n * directories it published, plus one modification stamp per skill file. This is\n * the pass that makes a skill added — or a description edited — mid-session\n * visible on the model's next request.\n */\nconst REFRESH_POLL_MS = 3_000\n/**\n * How often the full re-discovery walk runs for a served scan root. Only a walk\n * can find a skills directory that did not exist before (a new nested project,\n * say), and it costs one `readdir` per visited directory, so it runs on its own\n * slower cadence instead of on every poll. It used to be the only pass, at\n * {@link REFRESH_POLL_MS}'s old value of 10 s.\n */\nconst DISCOVERY_POLL_MS = 30_000\n\n/** Kebab-case skill names, matching the host grammar. */\nconst SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/\n\n/** Directory names that never contain project skill roots (safe to prune while walking). */\nconst PRUNED_DIRECTORY_NAMES = new Set([\n '.git', '.hg', '.svn', '.bzr', 'node_modules', '.venv', 'venv', '.tox',\n '.pants.d', '.next', '.nuxt', 'dist', 'build', 'out', 'coverage',\n '__pycache__', '.mypy_cache', '.pytest_cache', '.ruff_cache', '.cache',\n '.idea', '.vscode', '.serverless', '.terraform', '.yarn', '.pnpm-store',\n])\n\n/** One `name: value` frontmatter line pair the parser understands. */\ninterface ParsedSkill {\n name: string\n description: string\n whenToUse?: string\n invocation: { modelInvocable: boolean; userInvocable: boolean }\n content: string\n}\n\n/** The provider's minimal skill-candidate contract (mirrors @deepseek-ai/dsh-skill). */\nexport interface WslSkillCandidate {\n readonly name: string\n readonly description: string\n readonly whenToUse?: string\n readonly invocation: { modelInvocable: boolean; userInvocable: boolean }\n readonly source: string\n readonly provider: string\n readonly rank: number\n readonly locator: { path: string; directory: string }\n readonly path: string\n}\n\n/** The provider's minimal skill-definition contract (candidate plus body). */\nexport interface WslSkillDefinition extends WslSkillCandidate {\n readonly content: string\n}\n\n/** Lookup options the registry passes to `list`/`get`. */\nexport interface WslSkillLookupOptions {\n readonly cwd?: string\n readonly signal?: AbortSignal\n}\n\n/** Registration-scoped lifecycle face passed to the provider constructor. */\nexport interface WslSkillProviderControl {\n readonly signal: AbortSignal\n readonly invalidate: () => void\n}\n\n/** The `ctx.skills` registry face this provider registers on (optional service). */\nexport interface WslSkillsRegistryFace {\n registerProvider(create: (control: WslSkillProviderControl) => {\n readonly name: string\n list(options: WslSkillLookupOptions): Promise<unknown>\n get(candidate: WslSkillCandidate, options: WslSkillLookupOptions): Promise<unknown>\n }): () => void\n}\n\n/** Injectable filesystem face (defaults to node:fs/promises on the real 9P share). */\nexport interface WslSkillIo {\n readdir(path: string, options: { withFileTypes: true }): Promise<Dirent[]>\n readFile(path: string, options: { encoding: 'utf8' }): Promise<string>\n /**\n * Stat one path. `mtimeMs`/`size` are optional because the change detector\n * uses them only where the substrate reports them: a face without them still\n * lists skills, it just cannot notice an edit to an existing skill file.\n */\n stat(path: string): Promise<{ isDirectory(): boolean; mtimeMs?: number; size?: number }>\n /**\n * Resolve Linux symlink targets the share itself cannot follow, returning\n * each real path in the same UNC spelling (`undefined` where a link cannot\n * be resolved). Optional: a substrate that follows links locally leaves this\n * unimplemented, and the walk then never pays for a distribution round trip.\n */\n resolveLinks?(uncPaths: readonly string[]): Promise<(string | undefined)[]>\n}\n\n/** The node:fs/promises implementation the provider uses in production. */\nexport const nodeSkillIo: WslSkillIo = {\n readdir: async (path, options) => readdir(path, options),\n readFile: async (path, options) => readFile(path, options),\n stat: async path => stat(path),\n resolveLinks: async uncPaths => resolveLinuxSymlinks(uncPaths),\n}\n\n/** One discovered skill directory under a WSL workspace. */\ninterface SkillRoot {\n /** Absolute UNC path of the skills directory (`…\\.dsh\\skills`). */\n path: string\n /** Host source label ('project-dsh' | 'project-agents'). */\n source: 'project-dsh' | 'project-agents'\n /** Host project rank so same-name wins and precedence stay consistent. */\n rank: number\n}\n\n/** Whether a skills-directory entry is a directory-bundle or a flat markdown skill. */\ninterface SkillEntry {\n name: string\n kind: 'bundle' | 'flat'\n path: string\n}\n\n/**\n * Locate the nearest ancestor of `linuxDir` (the directory itself included)\n * containing a `.git` marker, mirroring the host skill-filesystem's\n * project-root rule. `.git` may be a directory or a worktree pointer file;\n * existence is enough. Bounded so a pathological path cannot spin the walk.\n * @param distro - the WSL distribution name.\n * @param linuxDir - the session cwd's absolute Linux path.\n * @param io - filesystem face.\n * @returns the project root's Linux path, or `undefined` when no ancestor carries a `.git`.\n */\nasync function nearestGitAncestor(distro: string, linuxDir: string, io: WslSkillIo): Promise<string | undefined> {\n let current = linuxDir\n for (let levels = 0; levels <= MAX_ANCESTOR_WALK; levels += 1) {\n try {\n await io.stat(joinUnc(distro, posix.join(current, '.git')))\n return current\n } catch {\n // No `.git` marker at this level; keep walking towards the filesystem root.\n }\n const parent = posix.dirname(current)\n if (parent === current) return undefined\n current = parent\n }\n return undefined\n}\n\n/** What one walk step learns about one directory. */\ninterface DirectoryProbe {\n /** The skill roots the directory publishes, `.dsh` before `.agents`. */\n readonly roots: SkillRoot[]\n /** Its child directories to descend into, as `[linux path, depth]`. */\n readonly children: [string, number][]\n /** Links its listing showed that the share itself could not follow. */\n readonly links: [string, number][]\n}\n\n/**\n * Everything the walk learns from one directory, in the order the round trips\n * are worth paying for: the listing first — it also says which skill markers can\n * exist there at all — then the markers it showed.\n * @param distro - the WSL distribution name.\n * @param dir - the directory's Linux path.\n * @param depth - its depth below the scan root.\n * @param io - filesystem face.\n * @returns the directory's roots, child directories and unresolved links.\n */\nasync function probeDirectory(distro: string, dir: string, depth: number, io: WslSkillIo): Promise<DirectoryProbe> {\n let entries: Dirent[] | undefined\n if (depth < MAX_SCAN_DEPTH) {\n try {\n entries = await io.readdir(joinUnc(distro, dir), { withFileTypes: true })\n } catch {\n // An unreadable directory (permissions, vanished mid-walk) prunes its\n // subtree, but the directory's own markers may still be worth probing.\n }\n }\n const listed = entries ?? []\n const allMarkers = PROJECT_SKILL_MARKERS.map(([marker]) => marker)\n const markers = entries === undefined\n ? allMarkers\n : allMarkers.filter(marker => listed.some(entry => entry.name === marker))\n const roots = await skillRootsOfDirectory(distro, dir, io, markers)\n const children: [string, number][] = []\n const links: [string, number][] = []\n for (const entry of listed) {\n if (PRUNED_DIRECTORY_NAMES.has(entry.name)) continue\n if (entry.name.startsWith('.') && entry.name !== '.dsh' && entry.name !== '.agents') continue\n if (entry.name === '.dsh' || entry.name === '.agents') continue\n const childPath = posix.join(dir, entry.name)\n if (entry.isDirectory()) {\n children.push([childPath, depth + 1])\n continue\n }\n if (!entry.isSymbolicLink()) continue\n // A project may be linked into the workspace via a directory symlink;\n // follow it when the target is a directory. Symlink cycles stay\n // bounded: every hop increments the depth (capped by MAX_SCAN_DEPTH)\n // and the walk as a whole by MAX_VISITED_DIRECTORIES.\n try {\n const target = await io.stat(joinUnc(distro, childPath))\n // A substrate that resolves Linux links itself answers here; a link\n // to a file is not a project directory either way.\n if (target.isDirectory()) children.push([childPath, depth + 1])\n continue\n } catch {\n // The `\\\\wsl.localhost` 9P share reports the link entry but cannot\n // resolve its Linux target; the distribution below can.\n }\n links.push([childPath, depth + 1])\n }\n return { roots, children, links }\n}\n\n/**\n * Scan a WSL workspace root for nested skill directories.\n *\n * A layer is probed concurrently but published in frontier order, so the\n * catalog never depends on which probe happened to finish first, and the budget\n * is claimed before any probe starts: concurrency must not change what is\n * visited, only how long it takes.\n * @param distro - the WSL distribution name.\n * @param linuxRoot - the workspace's absolute Linux path.\n * @param io - filesystem face.\n * @returns discovered skill directories, bounded by depth and budget.\n */\nasync function discoverSkillRoots(distro: string, linuxRoot: string, io: WslSkillIo): Promise<SkillRoot[]> {\n const roots: SkillRoot[] = []\n const visited = new Set<string>()\n let linksResolved = 0\n // BFS layers so the budget prunes the widest, most redundant levels first\n // (shallow skill dirs matter most): [path, depth] pairs.\n let frontier: [string, number][] = [[linuxRoot, 0]]\n while (frontier.length > 0 && roots.length < MAX_SKILL_ROOTS) {\n const layer: [string, number][] = []\n for (const item of frontier) {\n if (visited.size >= MAX_VISITED_DIRECTORIES) break\n if (visited.has(item[0])) continue\n visited.add(item[0])\n layer.push(item)\n }\n if (layer.length === 0) return roots\n const probes = new Array<DirectoryProbe | undefined>(layer.length)\n let next = 0\n await Promise.all(Array.from({ length: Math.min(WALK_CONCURRENCY, layer.length) }, async () => {\n for (let index = next; index < layer.length; index = next) {\n next += 1\n const [dir, depth] = layer[index]!\n probes[index] = await probeDirectory(distro, dir, depth, io)\n }\n }))\n const nextLayer: [string, number][] = []\n // Symlink targets the share could not follow, resolved for this whole\n // layer in one distribution round trip after the layer is enumerated.\n const links: [string, number][] = []\n for (const probe of probes) {\n if (probe === undefined) continue\n if (roots.length < MAX_SKILL_ROOTS) {\n roots.push(...probe.roots.slice(0, MAX_SKILL_ROOTS - roots.length))\n }\n nextLayer.push(...probe.children)\n links.push(...probe.links)\n }\n // The budget ran out inside this layer: stop where the walk stops, without\n // paying for this layer's link resolution either.\n if (visited.size >= MAX_VISITED_DIRECTORIES) return roots\n if (links.length > 0 && io.resolveLinks !== undefined && linksResolved < MAX_LINK_RESOLUTIONS) {\n const batch = links.slice(0, MAX_LINK_RESOLUTIONS - linksResolved)\n linksResolved += batch.length\n const resolved = await io.resolveLinks(batch.map(([path]) => joinUnc(distro, path)))\n for (let index = 0; index < batch.length; index += 1) {\n const real = resolved[index]\n if (real === undefined) continue\n // The walk continues at the link's real path, which also collapses a\n // project reachable both directly and through a link onto one visit.\n try {\n const info = await io.stat(real)\n if (info.isDirectory()) nextLayer.push([uncToLinux(real), batch[index]![1]])\n } catch {\n // The distribution resolved a target this share still cannot stat.\n }\n }\n }\n frontier = nextLayer\n }\n return roots\n}\n\n/** The two project skill markers, with the source and rank each publishes. */\nconst PROJECT_SKILL_MARKERS = [\n ['.dsh', 'project-dsh', PROJECT_DSH_RANK],\n ['.agents', 'project-agents', PROJECT_AGENTS_RANK],\n] as const\n\n/**\n * Publish the skill roots of one scanned directory (its `.dsh/skills` and\n * `.agents/skills`, each with the host's project ranks).\n * @param distro - the WSL distribution name.\n * @param linuxDir - the scanned directory's Linux path.\n * @param io - filesystem face.\n * @param markers - the markers worth probing. The walk narrows this to the ones\n * its listing actually showed, because each probe is a round trip and a\n * directory without a `.dsh` entry cannot hold `.dsh/skills`.\n * @returns the directory's skill roots that exist.\n */\nasync function skillRootsOfDirectory(\n distro: string,\n linuxDir: string,\n io: WslSkillIo,\n markers: readonly string[] = PROJECT_SKILL_MARKERS.map(([marker]) => marker),\n): Promise<SkillRoot[]> {\n const result: SkillRoot[] = []\n for (const [marker, source, rank] of PROJECT_SKILL_MARKERS) {\n if (!markers.includes(marker)) continue\n const path = joinUnc(distro, posix.join(linuxDir, marker, 'skills'))\n try {\n const info = await io.stat(path)\n if (info.isDirectory()) result.push({ path, source, rank })\n } catch {\n // Absent skills directory: nothing to publish.\n }\n }\n return result\n}\n\n/** List one skills directory's entries (directory bundles and flat `.md` skills). */\nasync function listSkillEntries(root: SkillRoot, io: WslSkillIo): Promise<SkillEntry[]> {\n let dirents: Dirent[]\n try {\n dirents = await io.readdir(root.path, { withFileTypes: true })\n } catch {\n return []\n }\n const entries: SkillEntry[] = []\n for (const entry of dirents) {\n if (entry.isDirectory()) {\n entries.push({ name: entry.name, kind: 'bundle', path: joinWindowsPath(root.path, entry.name, 'SKILL.md') })\n } else if (entry.isFile() && entry.name.endsWith('.md')) {\n entries.push({ name: entry.name.slice(0, -3), kind: 'flat', path: joinWindowsPath(root.path, entry.name) })\n }\n }\n return entries.sort((a, b) => a.name.localeCompare(b.name))\n}\n\n/**\n * The published shape of one skills directory: its path, then each skill's name,\n * kind and modification stamp.\n *\n * The stamp is what makes an edit to an *existing* skill visible: the catalog the\n * model sees is rebuilt only when the registry's revision moves, and a directory\n * listing alone cannot tell a rewritten `SKILL.md` from an untouched one. One\n * `stat` per skill file (never a read) is the whole cost, and a file that cannot\n * be stamped reports the same `gone` marker on every pass, so a substrate that\n * does not expose modification times simply never triggers on content.\n * @param root - the skills directory.\n * @param entries - its entries, as just listed.\n * @param io - filesystem face.\n * @returns the deterministic shape string for this directory.\n */\nasync function shapeOfRoot(root: SkillRoot, entries: readonly SkillEntry[], io: WslSkillIo): Promise<string> {\n const stamps = await Promise.all(entries.map(async (entry) => {\n try {\n const info = await io.stat(entry.path)\n return `${entry.name}:${entry.kind}:${info.mtimeMs ?? 0}:${info.size ?? 0}`\n } catch {\n return `${entry.name}:${entry.kind}:gone`\n }\n }))\n return `${root.path}\\u0001${stamps.sort().join(',')}`\n}\n\n/** Read and parse one skill file; `undefined` when missing or unparsable. */\nasync function readSkill(path: string, io: WslSkillIo, signal?: AbortSignal): Promise<ParsedSkill | undefined> {\n signal?.throwIfAborted()\n let raw: string\n try {\n raw = await io.readFile(path, { encoding: 'utf8' })\n } catch {\n return undefined\n }\n signal?.throwIfAborted()\n return parseSkillFrontmatter(raw, path)\n}\n\n/**\n * Parse the frontmatter subset skill files use: `---` fenced YAML with\n * `name` / `description` / `whenToUse` / `user-invocable` /\n * `disable-model-invocation`. Single-line scalars and block scalars\n * (`|` literal, `>` folded) are understood; anything else is skipped,\n * matching the shipped provider's leniency: a bad file must not fail\n * the catalog.\n */\nfunction parseSkillFrontmatter(raw: string, path: string): ParsedSkill | undefined {\n // Windows editors save UTF-8 with a BOM; a leading BOM must not make the\n // opening `---` line unmatchable and silently drop the skill.\n if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1)\n const firstLineEnd = raw.indexOf('\\n')\n if (firstLineEnd < 0) return undefined\n if (raw.slice(0, firstLineEnd).replace(/\\r$/, '') !== '---') return undefined\n const start = firstLineEnd + 1\n const closing = findFrontmatterEnd(raw, start)\n if (closing === undefined) return undefined\n const lines = raw.slice(start, closing).split('\\n')\n const fields = new Map<string, string>()\n for (let index = 0; index < lines.length; index += 1) {\n const line = (lines[index] ?? '').replace(/\\r$/, '')\n const block = /^([A-Za-z0-9-]+):\\s*([|>])[+-]?\\s*$/.exec(line)\n if (block !== null) {\n const value = parseBlockScalar(lines, index, block[2] === '>')\n index = value.nextLineIndex\n if (value.text !== '') fields.set(block[1] ?? '', value.text)\n continue\n }\n const match = /^([A-Za-z0-9-]+):\\s*(.*)$/.exec(line)\n if (match === null) continue\n const value = match[2]?.trim() ?? ''\n if (value !== '') fields.set(match[1] ?? '', unquote(value))\n }\n const name = fields.get('name') ?? ''\n const description = fields.get('description') ?? ''\n if (!SKILL_NAME.test(name) || description === '') {\n return undefined\n }\n const whenToUse = fields.get('whenToUse')\n return {\n name,\n description,\n ...whenToUse !== undefined && whenToUse !== '' ? { whenToUse } : {},\n invocation: {\n modelInvocable: !frontmatterBoolean(fields, 'disable-model-invocation'),\n userInvocable: frontmatterBoolean(fields, 'user-invocable', true),\n },\n // `findFrontmatterEnd` returns the index of the body's FIRST character\n // (the newline after the closing `---` plus one), so slicing at `closing`\n // is what keeps it: `closing + 1` dropped the body's first character and\n // made the byte after the delimiter look like the body.\n content: raw.slice(closing).trim(),\n }\n}\n\n/**\n * Collect a YAML block scalar (`key: |` literal or `key: >` folded) starting\n * at `startIndex`'s following lines. The block runs until the first\n * non-indented, non-blank line; its common indentation is stripped.\n * @returns the scalar text and the index of the last consumed line.\n */\nfunction parseBlockScalar(\n lines: string[],\n startIndex: number,\n folded: boolean,\n): { text: string; nextLineIndex: number } {\n const collected: string[] = []\n let indent: string | undefined\n let index = startIndex\n while (index + 1 < lines.length) {\n index += 1\n const next = (lines[index] ?? '').replace(/\\r$/, '')\n if (next.trim() === '') {\n collected.push('')\n continue\n }\n const indented = /^([ \\t]+)(.*)$/.exec(next)\n if (indented === null) {\n index -= 1\n break\n }\n indent ??= indented[1]\n collected.push(indented[1]?.startsWith(indent) === true ? indented[2] : indented[1].replace(/^[ \\t]+/, '') + indented[2])\n }\n while (collected.length > 0 && collected[collected.length - 1] === '') collected.pop()\n const text = (folded ? collected.filter(line => line !== '').join(' ') : collected.join('\\n')).trim()\n return { text, nextLineIndex: index }\n}\n\n/** Locate the closing `---` line of a frontmatter block. */\nfunction findFrontmatterEnd(raw: string, start: number): number | undefined {\n let lineStart = start\n while (lineStart <= raw.length) {\n const nextNewline = raw.indexOf('\\n', lineStart)\n const lineEnd = nextNewline < 0 ? raw.length : nextNewline\n if (raw.slice(lineStart, lineEnd).replace(/\\r$/, '') === '---') return lineEnd + 1\n if (nextNewline < 0) return undefined\n lineStart = nextNewline + 1\n }\n return undefined\n}\n\n/** Strip one level of matching quotes from a scalar value. */\nfunction unquote(value: string): string {\n if (value.length >= 2) {\n const first = value[0]\n const last = value[value.length - 1]\n if ((first === '\"' && last === '\"') || (first === \"'\" && last === \"'\")) {\n return value.slice(1, -1)\n }\n }\n return value\n}\n\n/** Boolean semantics for `user-invocable` / `disable-model-invocation` (matches the host parser). */\nfunction frontmatterBoolean(fields: Map<string, string>, key: string, dflt = false): boolean {\n const value = fields.get(key)\n if (value === undefined) return dflt\n switch (value.toLowerCase()) {\n case 'true':\n case 'yes':\n case 'on':\n case '1':\n return true\n case 'false':\n case 'no':\n case 'off':\n case '0':\n return false\n default:\n return dflt\n }\n}\n\n/**\n * The WSL workspace skill provider. Registered on the host's `ctx.skills`\n * registry; serves only lookups whose cwd is a WSL UNC workspace path.\n *\n * Completed `list()` lookups are cached per scan root and served as-is: a lookup\n * is on the session's request path, and re-walking the tree there is what made\n * every turn in a large workspace wait for a 4096-directory scan of the 9P\n * share. `get()` always re-reads the skill file so body edits are picked up\n * immediately.\n *\n * A scan root that has been served is re-checked every REFRESH_POLL_MS: the\n * host cannot watch a `\\\\wsl.localhost\\…` path (which is why the generated\n * preset pins `watch: false`), so this provider watches for it instead. A\n * changed directory listing clears the cache and calls `control.invalidate()`,\n * which bumps the registry's revision; the catalog middleware re-collects on\n * the session's next request, so a skill added mid-session reaches the model\n * without starting a new session, and the cache is only ever refilled by a\n * lookup the detector has already invalidated.\n */\nexport class WslSkillsProvider {\n readonly name = 'wsl-workspace'\n private readonly control: WslSkillProviderControl\n private readonly io: WslSkillIo\n private readonly refreshMs: number\n /** Cheap polls between two full discovery walks (at least one). */\n private readonly walkEveryPolls: number\n /** The published catalog per scan root, served until its detector drops it. */\n private readonly cache = new Map<string, WslSkillCandidate[]>()\n /** One change detector per served scan root, keyed like {@link cache}. */\n private readonly detectors = new Map<string, {\n timer: ReturnType<typeof setInterval>\n signature: string\n roots: readonly SkillRoot[]\n polls: number\n /** True while a poll is still in flight, so a slow pass cannot stack up. */\n busy: boolean\n }>()\n\n constructor(\n control: WslSkillProviderControl,\n io: WslSkillIo = nodeSkillIo,\n refreshMs: number = REFRESH_POLL_MS,\n discoveryMs: number = DISCOVERY_POLL_MS,\n ) {\n this.control = control\n this.io = io\n this.refreshMs = refreshMs\n // Counted in polls rather than read off the clock: a poll is what the\n // cadence is measured in, and an injected clock (tests) may stand still.\n this.walkEveryPolls = Math.max(1, Math.round(discoveryMs / refreshMs))\n }\n\n /**\n * Discover nested project skills for a WSL UNC session workspace.\n * @param options - lookup options; `cwd` selects the WSL workspace.\n * @returns candidates for every `.dsh/skills` / `.agents/skills` under the\n * session's scan root — the nearest `.git` ancestor of the cwd, else the\n * cwd itself — or an empty array for non-WSL lookups.\n */\n async list(options: WslSkillLookupOptions): Promise<WslSkillCandidate[]> {\n this.control.signal.throwIfAborted()\n options.signal?.throwIfAborted()\n const unc = options.cwd === undefined ? null : parseWslUnc(options.cwd)\n if (unc === null) return []\n // Host parity: the session's project root is the nearest `.git` ancestor\n // of the cwd, so lookups from inside a project subtree still see that\n // project's skills; nested projects below it join via the bounded BFS.\n // Without a `.git` ancestor the session cwd itself is the scan root (the\n // issue #10 workspace layout).\n const scanRoot = (await nearestGitAncestor(unc.distro, unc.linuxPath, this.io)) ?? unc.linuxPath\n const cacheKey = `${unc.distro}\\u0000${scanRoot}`\n const cached = this.cache.get(cacheKey)\n if (cached !== undefined) {\n // Published already: serve it without touching the share again. The\n // detector this lookup once started is what notices a change, and it\n // drops this entry before the next request can be served from it.\n this.cache.delete(cacheKey)\n this.cache.set(cacheKey, cached)\n return [...cached]\n }\n const roots = await discoverSkillRoots(unc.distro, scanRoot, this.io)\n const candidates: WslSkillCandidate[] = []\n const seenSkills = new Set<string>()\n // The catalog's shape (roots, entry names, kinds and one modification stamp\n // per skill file) is collected as the candidates are built, so the change\n // detector starts from what this lookup actually saw instead of paying for a\n // second walk and a second round of stats.\n const signature: string[] = []\n for (const root of roots) {\n const entries = await listSkillEntries(root, this.io)\n signature.push(await shapeOfRoot(root, entries, this.io))\n for (const entry of entries) {\n options.signal?.throwIfAborted()\n const parsed = await readSkill(entry.path, this.io, options.signal)\n if (parsed === undefined) continue\n // A project reachable through both its real path and a directory\n // symlink yields aliasing roots whose locators differ; publish each\n // distinct (name, body) once so the catalog shows no duplicates.\n const fingerprint = `${parsed.name}\\u0000${parsed.content}`\n if (seenSkills.has(fingerprint)) continue\n seenSkills.add(fingerprint)\n candidates.push({\n name: parsed.name,\n description: parsed.description,\n ...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},\n invocation: parsed.invocation,\n source: root.source,\n provider: this.name,\n rank: root.rank,\n locator: {\n path: entry.path,\n directory: entry.kind === 'bundle'\n ? joinWindowsPath(entry.path, '..')\n : root.path,\n },\n path: entry.path,\n })\n }\n }\n this.cache.set(cacheKey, candidates)\n while (this.cache.size > CACHE_MAX_ENTRIES) {\n const oldest = this.cache.keys().next().value\n if (oldest === undefined) break\n this.cache.delete(oldest)\n }\n this.watch(cacheKey, unc.distro, scanRoot, signature.join('\\u0002'), roots)\n // A copy, so a caller that mutates the list it got cannot poison the cache.\n return [...candidates]\n }\n\n /**\n * Keep one scan root's catalog honest for as long as this provider is\n * registered: the host cannot watch a UNC workspace, so the provider polls the\n * shape it just published and, on any difference, drops its own cache and asks\n * the registry to re-collect for the session's next request.\n *\n * Two passes keep that affordable: the cheap one (every\n * {@link REFRESH_POLL_MS}) re-reads the skills directories already published\n * and re-stats their skill files, so an added, removed or edited skill is\n * noticed on its own; the full re-discovery walk (every\n * {@link DISCOVERY_POLL_MS}) is what can find a skills directory that did not\n * exist before. Neither pass reads a skill file: the description the catalog\n * shows changes only through the registry's revision.\n * @param cacheKey - this provider's key for the scan root.\n * @param distro - the WSL distribution.\n * @param scanRoot - the Linux path the lookup scanned.\n * @param signature - the shape the lookup just published.\n * @param roots - the skills directories that lookup found.\n */\n private watch(cacheKey: string, distro: string, scanRoot: string, signature: string, roots: readonly SkillRoot[]): void {\n const existing = this.detectors.get(cacheKey)\n if (existing !== undefined) {\n existing.signature = signature\n existing.roots = roots\n return\n }\n const detector = {\n timer: setInterval(() => void this.detect(cacheKey, distro, scanRoot), this.refreshMs),\n signature,\n roots,\n polls: 0,\n busy: false,\n }\n // A pending poll must never hold the host process open (or outlive it).\n if (typeof detector.timer.unref === 'function') detector.timer.unref()\n this.detectors.set(cacheKey, detector)\n this.control.signal.addEventListener('abort', () => {\n clearInterval(detector.timer)\n this.detectors.delete(cacheKey)\n }, { once: true })\n }\n\n /** One poll: the cheap pass always, the discovery walk on its own cadence. */\n private async detect(cacheKey: string, distro: string, scanRoot: string): Promise<void> {\n const detector = this.detectors.get(cacheKey)\n if (detector === undefined || this.control.signal.aborted) return\n // A pass over a slow share can outlast the interval; letting the timer start\n // another one would stack `wsl.exe` calls and make every later poll read a\n // half-finished shape. One pass at a time per scan root.\n if (detector.busy) return\n detector.busy = true\n try {\n detector.polls += 1\n const walk = detector.polls % this.walkEveryPolls === 0\n let signature: string\n try {\n signature = await this.shape(distro, walk ? await discoverSkillRoots(distro, scanRoot, this.io) : detector.roots)\n } catch {\n // A transient read failure keeps the last known shape and retries.\n return\n }\n if (signature === detector.signature) return\n detector.signature = signature\n this.cache.delete(cacheKey)\n this.control.invalidate()\n } finally {\n detector.busy = false\n }\n }\n\n /** The shape of one root set: paths, entry names, kinds and file stamps. */\n private async shape(distro: string, roots: readonly SkillRoot[]): Promise<string> {\n const parts: string[] = []\n for (const root of roots) {\n parts.push(await shapeOfRoot(root, await listSkillEntries(root, this.io), this.io))\n }\n return parts.join('\\u0002')\n }\n\n /**\n * Load a complete skill body for a previously listed candidate.\n * @param candidate - the candidate this provider returned.\n * @param options - lookup options whose signal cancels the read.\n * @returns the full skill, or `undefined` if the file disappeared.\n */\n async get(candidate: WslSkillCandidate, options: WslSkillLookupOptions): Promise<WslSkillDefinition | undefined> {\n this.control.signal.throwIfAborted()\n const parsed = await readSkill(candidate.locator.path, this.io, options.signal)\n if (parsed === undefined || parsed.name !== candidate.name) return undefined\n return {\n name: parsed.name,\n description: parsed.description,\n ...parsed.whenToUse !== undefined ? { whenToUse: parsed.whenToUse } : {},\n invocation: parsed.invocation,\n source: candidate.source,\n provider: candidate.provider,\n rank: candidate.rank,\n locator: candidate.locator,\n path: candidate.path,\n content: parsed.content,\n }\n }\n}","/**\n * Host half of dsh-wsl-workspace. Three responsibilities:\n *\n * 1. Publish a `wsl-<mode>` variant for every healthy roster preset, so the\n * WSL execution world — `shell-wsl` + `fs-wsl` behind one entry-local\n * realm, with `tool-bash`/`tool-fs` consumers — composes with ANY mode\n * instead of being a mode itself. The channel follows the release:\n * `0.1.7-alpha.1+` stopped scanning any user preset root and builds its roster\n * from declarative `@deepseek-ai/dsh-agent-preset` rows, so there a variant\n * is a *declaration registered through `ctx.agentPresets`* — and only that\n * path rewrites the absolute provider specifiers below into `file:` URLs,\n * because a registered preset's entry tree does not translate them the way\n * the boot-time Include does; earlier releases still get a variant directory\n * under `<dshHome>/.agent-presets/`, whose legacy standalone `wsl` directory\n * and stale variants are removed on boot.\n *\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\n * distribution discovery, one-level directory listing, path checks, and\n * the per-workspace username store — all over the 9P UNC share.\n * Loopback-only, matching the sensitivity of the privileged configuration\n * surface.\n *\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\n * shell executor can resolve a plain Linux `workdir` to the calling\n * session's distribution.\n * @module dsh-wsl-workspace\n */\n\nimport { Context } from '@deepseek-ai/cordis'\nimport z from '@deepseek-ai/schemastery'\nimport { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport { homedir } from 'node:os'\nimport { joinUnc, mntToWindowsPath, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\nimport { canonicalWslUnc, getWindowsWorkspace, getWorkspaceUsername, listWorkspaceKeys, registerWindowsWorkspace, setWorkspaceUsername } from './shared/wsl-credentials.ts'\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\nimport { isWslVariantId, transformPresetForWsl, unquoteScalar, variantIdFor } from './host/variants.ts'\nimport { WslSkillsProvider, type WslSkillsRegistryFace } from './host/wsl-skills.ts'\n\n/** The HTTP route this plugin serves (a relative, same-origin path). */\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\n\n/**\n * Bilingual display labels for the shipped source modes, matching the app's\n * own built-in copy in each language — note the `code` preset is \"PTC 模式\"\n * in the Chinese copy but \"Code mode\" in English. The DSH picker localizes\n * only the four built-in ids itself; `wsl-*` variant ids render the\n * preset.yml text verbatim, so the plugin writes one bilingual string so\n * both locales can identify each variant. Custom presets keep their own\n * name.\n *\n * This table is consulted before anything the release publishes, so every id\n * the mode has ever carried needs an entry. `code` was the id up to\n * `0.1.0-rc.8` and `ptc` is the id from `0.1.1` on; without the `ptc` entry the\n * lookup returned undefined and the variant fell through to the release's own\n * display name — which `0.1.7` does not publish at all, leaving the mode named\n * `WSL · ptc` with the generic description `WSL execution world for ptc: …`.\n */\nconst MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>> = {\n standard: { en: 'Standard mode', zh: '标准模式' },\n code: { en: 'Code mode', zh: 'PTC 模式' },\n ptc: { en: 'PTC mode', zh: 'PTC 模式' },\n minimal: { en: 'Minimal mode', zh: '极简模式' },\n cordis: { en: 'Creator mode', zh: '创造模式' },\n}\n\n/**\n * Quote a value as a single-line YAML single-quoted scalar. Plain scalars\n * cannot contain `: ` (colon + space), which plain English sentences do —\n * written unquoted they make the whole preset.yml unparsable, dropping the\n * name, description and order together.\n */\nfunction yamlScalar(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/** The variant name for one shipped mode (bilingual) or a custom preset. */\nfunction variantName(presetId: string, sourceName: string): string {\n const labels = MODE_DISPLAY_LABELS[presetId]\n return labels === undefined ? `WSL · ${sourceName}` : `WSL · ${labels.en}(${labels.zh})`\n}\n\n/** The variant description for one shipped mode (bilingual) or a custom preset. */\nfunction variantDescription(presetId: string): string {\n const labels = MODE_DISPLAY_LABELS[presetId]\n const display = labels === undefined ? presetId : `${labels.en}(${labels.zh})`\n return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`\n}\n\n/** Plugin config. */\nexport interface Config {\n /** The route under which the dialog data API is served. */\n route?: string\n}\n\n/** The shape after schemastery applied the defaults. */\ntype ResolvedConfig = Required<Config>\n\n/** The `webServer.register` route contract this plugin consumes. */\ninterface WebServerRoute {\n kind: 'exact'\n path: string\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\n}\n\ninterface WebServerService {\n register(route: WebServerRoute): () => void\n}\n\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\ninterface ShellEnvService {\n register(contributor: {\n name: string\n variables: Readonly<Record<string, { description: string }>>\n resolve(execution: {\n agent?: { session: { header: { cwd?: string } } }\n }): Readonly<Partial<Record<string, string>>>\n }): () => void\n}\n\n/** One directory entry the dialog lists. */\ninterface WslDirEntryWire {\n name: string\n kind: 'directory' | 'file' | 'other'\n}\n\n/** One directory level plus its breadcrumb ancestry. */\ninterface WslDirListingWire {\n path: string\n parent: string | null\n entries: WslDirEntryWire[]\n}\n\n/** The wire envelope every method answers with. */\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\n\nconst MAX_BODY_BYTES = 1024 * 1024\n\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\n\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\n\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\nfunction isLoopback(address: string | undefined): boolean {\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\n}\n\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\nfunction hostNameOf(host: string): string {\n if (host.startsWith('[')) {\n const end = host.indexOf(']')\n return end >= 0 ? host.slice(1, end) : host\n }\n return host.split(':')[0] ?? ''\n}\n\n/** True when the request's `Host` header names a loopback host. */\nfunction isLoopbackHost(host: string | undefined): boolean {\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\n}\n\n/**\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\n * an attacker-controlled segment containing separators or `..` would escape\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\n * @param value - the raw wire value.\n * @returns the validated distro name.\n */\nfunction requireDistro(value: unknown): string {\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\n throw new Error('distro must be a valid WSL distribution name')\n }\n return value\n}\n\n/** Human text for an unknown rejection. */\nfunction messageOf(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/** Write one JSON envelope. */\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\n res.writeHead(status, {\n 'content-type': 'application/json; charset=utf-8',\n 'cache-control': 'no-store',\n 'x-content-type-options': 'nosniff',\n })\n res.end(JSON.stringify(body))\n}\n\n/** Collect and parse the request body, bounded. */\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\n chunks.push(buffer)\n }\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('request body must be a JSON object')\n }\n return parsed as Record<string, unknown>\n}\n\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\nfunction requireLinuxPath(value: unknown, label: string): string {\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\n throw new Error(`${label} must be an absolute Linux path`)\n }\n return normalizeLinuxPath(value)\n}\n\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\nfunction requireWslUnc(value: unknown): string {\n if (typeof value !== 'string') throw new Error('path must be a string')\n const canonical = canonicalWslUnc(value)\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\n return canonical\n}\n\n/**\n * Resolve one directory listing for the dialog. The 9P share (`\\\\wsl.localhost\\…`)\n * serves only the ext4 volume: `/mnt/<drive>` (drvfs) reads return Access\n * denied, so drvfs paths are read through their Windows drive spelling and\n * `/mnt` itself is synthesized from the drives present on the host.\n */\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\n if (linuxPath === '/mnt') {\n const entries: WslDirEntryWire[] = []\n for (let i = 0; i < 26; i++) {\n const letter = String.fromCharCode(65 + i)\n try {\n const info = statSync(`${letter}:\\\\`)\n if (info.isDirectory()) entries.push({ name: letter.toLowerCase(), kind: 'directory' })\n } catch {\n // Absent drive: skip.\n }\n }\n return { path: '/mnt', parent: '/', entries }\n }\n const winPath = mntToWindowsPath(linuxPath)\n const readPath = winPath !== null ? winPath : joinUnc(distro, linuxPath)\n const dirents = readdirSync(readPath, { withFileTypes: true })\n const entries: WslDirEntryWire[] = dirents\n .slice(0, 1000)\n .map((dirent): WslDirEntryWire => {\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\n ? 'directory'\n : dirent.isFile() ? 'file' : 'other'\n return { name: dirent.name, kind }\n })\n .sort((a, b) => {\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\n return a.name.localeCompare(b.name)\n })\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\n return { path: linuxPath, parent, entries }\n}\n\n/** Cached self-description for the dialog's help panel. */\nlet selfDescription: { version: string; releases: { id: string; status: string }[] } | undefined\n\n/**\n * Read this plugin's own `package.json` for the dialog's help panel: the\n * published version and the declared `dsh.compatibility.dshReleases` matrix.\n * A plugin directory that cannot be read reports empty values rather than\n * failing the dialog, and the result is cached for the process lifetime.\n * @returns the self-description served by the `describe` method.\n */\nfunction describeSelf(): { version: string; releases: { id: string; status: string }[] } {\n if (selfDescription !== undefined) return selfDescription\n try {\n const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8')\n const parsed = JSON.parse(raw) as {\n version?: unknown\n dsh?: { compatibility?: { dshReleases?: Record<string, unknown> } }\n }\n selfDescription = {\n version: typeof parsed.version === 'string' ? parsed.version : 'unknown',\n releases: Object.entries(parsed.dsh?.compatibility?.dshReleases ?? {})\n .map(([id, status]) => ({ id, status: String(status) })),\n }\n } catch {\n selfDescription = { version: 'unknown', releases: [] }\n }\n return selfDescription\n}\n\n/** Route one method dispatch. */\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\n switch (method) {\n case 'listDistros': {\n const distros = await listDistros()\n const fallback = await defaultDistro()\n if (fallback !== undefined && distros.includes(fallback)) {\n return [fallback, ...distros.filter(name => name !== fallback)]\n }\n return distros\n }\n case 'listDir': {\n const distro = requireDistro(params.distro)\n const path = requireLinuxPath(params.path, 'path')\n return listWslDir(distro, path)\n }\n case 'check': {\n const distro = requireDistro(params.distro)\n const path = requireLinuxPath(params.path, 'path')\n // drvfs paths (Access denied over 9P) are checked through their drive spelling.\n const winPath = mntToWindowsPath(path)\n const readPath = winPath !== null ? winPath : joinUnc(distro, path)\n try {\n const info = statSync(readPath)\n return { exists: true, isDirectory: info.isDirectory() }\n } catch {\n return { exists: false, isDirectory: false }\n }\n }\n case 'registerWindows': {\n // A `/mnt/<drive>` workspace registers under its Windows drive path\n // (the registry realpath/stats it, and 9P cannot serve drvfs) with the\n // distro stored for the per-session env contributor.\n const distro = requireDistro(params.distro)\n const linuxPath = requireLinuxPath(params.linuxPath, 'path')\n const winPath = mntToWindowsPath(linuxPath)\n if (winPath === null) throw new Error('registerWindows requires a /mnt/<drive> Linux path')\n const username = typeof params.username === 'string' ? params.username : undefined\n registerWindowsWorkspace(winPath, distro, username)\n return null\n }\n case 'listWorkspaces': {\n // Every registered WSL workspace key (UNC and Windows drive spellings):\n // the client uses the drive keys to recognize `/mnt` workspaces.\n return listWorkspaceKeys()\n }\n case 'describe': {\n // The dialog's help panel reports what this build declares, so the\n // compatibility list can never drift from package.json.\n return describeSelf()\n }\n case 'setUser': {\n const path = requireWslUnc(params.path)\n const username = params.username\n if (username === undefined || username === '') {\n setWorkspaceUsername(path, undefined)\n } else {\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\n }\n setWorkspaceUsername(path, username)\n }\n return null\n }\n default:\n throw new Error(`unknown method \"${method}\"`)\n }\n}\n\n/** One roster entry as the `ctx.agentPresets` face reports it. */\ninterface AgentPresetRosterEntry {\n id: string\n name?: string\n description?: string\n /** Roster position; `list()` sorts by it. */\n order?: number\n broken?: string\n /** Legacy (≤ 0.1.6-alpha.2) only: the source preset's directory. */\n path?: string\n}\n\n/** One preset's declared composition, as the declaration channel reports it. */\ninterface AgentPresetDocument {\n agentPreset: string\n /** The declared child plugin list as entry-list YAML, `!!js` expressions included. */\n content: string\n name?: string\n description?: string\n}\n\n/** A declaration this plugin publishes; mirrors the Host's `PresetDefinition`. */\ninterface PresetDeclaration {\n id: string\n name?: string\n description?: string\n order?: number\n plugins: readonly unknown[]\n}\n\n/**\n * The `ctx.agentPresets` roster face this plugin consumes (optional service).\n *\n * Two generations sit behind this one shape, so both are probed by capability\n * rather than by version:\n *\n * - `0.1.0-rc.7 … 0.1.6-alpha.2`: `list()` reports each preset's directory in\n * `path`, and `read(id)` returns the composition text. A variant is a\n * *directory* under the roster's scanned user root (`$DSH_HOME/.agent-presets/`).\n * - `0.1.7-alpha.1+`: `read()` is renamed `readDocument()` and returns a document\n * instead of a bare string, `AgentPreset.path` is gone, and **nothing scans\n * the user root any more** — the roster is built from declarative\n * `@deepseek-ai/dsh-agent-preset` rows carried by bundle patches. A variant\n * is therefore a declaration row published through `register()`.\n */\ninterface AgentPresetsService {\n list(): Promise<AgentPresetRosterEntry[]>\n /** Legacy (≤ 0.1.6-alpha.2): the source composition text. */\n read?(id: string): Promise<string>\n /** 0.1.7-alpha.1+: the declared composition beside its published metadata. */\n readDocument?(id: string): Promise<AgentPresetDocument>\n /** 0.1.7-alpha.1+: publish a declaration; the returned disposer retires it. */\n register?(definition: PresetDeclaration): Promise<() => Promise<void>>\n}\n\n/**\n * Read one preset's declared composition across both roster generations.\n * @param agentPresets - the roster face.\n * @param preset - the roster entry to read.\n * @returns the composition text, plus the display name when the face publishes it.\n */\nasync function readPresetComposition(\n agentPresets: AgentPresetsService,\n preset: AgentPresetRosterEntry,\n): Promise<{ content: string; name?: string }> {\n if (typeof agentPresets.readDocument === 'function') {\n const document = await agentPresets.readDocument(preset.id)\n return document.name === undefined\n ? { content: document.content }\n : { content: document.content, name: document.name }\n }\n if (typeof agentPresets.read === 'function') return { content: await agentPresets.read(preset.id) }\n throw new Error(\n `agentPresets: this DSH release exposes neither readDocument() nor read() (preset \"${preset.id}\")`,\n )\n}\n\n/**\n * Parse one transformed composition back into declaration rows.\n *\n * The composition is the entry-list YAML dialect, whose `!!js` scalars are\n * expression nodes the Loader evaluates when it activates the row.\n * `@deepseek-ai/cordis-plugin-include` exports `entryListSchema` precisely so\n * config tooling can round-trip that dialect, and it is the same schema the\n * harness parses preset patches with. Parsing the text back is what lets the\n * (text-level) WSL transform keep working unchanged now that a variant is a\n * declaration row instead of a directory of YAML.\n *\n * Both modules are resolved at call time rather than at module load: they are\n * Host-provided (`dsh` depends on `cordis-plugin-include`, which depends on\n * `js-yaml`), and a release that ever drops them must fail this one variant\n * rather than refuse to load the whole plugin.\n * @param content - the variant composition text.\n * @returns the declaration's plugin rows.\n */\nasync function parseVariantComposition(content: string): Promise<unknown[]> {\n const includeSpecifier = '@deepseek-ai/cordis-plugin-include'\n const yamlSpecifier = 'js-yaml'\n const [include, yamlModule] = await Promise.all([\n import(includeSpecifier) as Promise<{ entryListSchema: unknown }>,\n import(yamlSpecifier) as Promise<{\n load?(source: string, options: { schema: unknown }): unknown\n default?: { load(source: string, options: { schema: unknown }): unknown }\n }>,\n ])\n const load = yamlModule.load ?? yamlModule.default?.load\n if (typeof load !== 'function') throw new Error('js-yaml: no load() export')\n const rows = load(content, { schema: include.entryListSchema })\n if (!Array.isArray(rows)) throw new Error('the transformed composition did not parse as an entry list')\n return toImportableSpecifiers(rows)\n}\n\n/**\n * Rewrite every absolute local module specifier as a `file:` URL.\n *\n * The generated world names THIS package's built providers (`shell.js`,\n * `fs.js`, …) by absolute path. That is what the directory mechanism needed:\n * those rows sat in an Include-backed tree, and the boot-time Include\n * translates an absolute path into a `file:` URL before importing it. A preset\n * mounted from a *declaration* is loaded by the registry's own entry tree,\n * which has no such translation — handing it `C:/…/lib/shell.js` leaves those\n * rows without a fiber, the audit reports them \"never started\", and the whole\n * variant is refused. Rewriting the specifier is therefore part of adapting to\n * the declaration mechanism, not a change to what the variant mounts.\n *\n * Only `name` is touched: config values (the relay path handed to the PTY\n * backend, the interpreter path in `shellPath`) must stay native filesystem\n * paths. Group rows carry their children in a `config` array, so those are\n * walked too.\n * @param rows - the parsed declaration rows.\n * @returns the same rows with their module specifiers made importable.\n */\nfunction toImportableSpecifiers(rows: unknown[]): unknown[] {\n const rewrite = (row: unknown): unknown => {\n if (row === null || typeof row !== 'object') return row\n const entry = row as { name?: unknown; config?: unknown }\n if (typeof entry.name === 'string' && isAbsolute(entry.name)) {\n entry.name = pathToFileURL(entry.name).href\n }\n if (Array.isArray(entry.config)) entry.config = entry.config.map(rewrite)\n return row\n }\n return rows.map(rewrite)\n}\n\n/**\n * Publish a fully staged preset directory while preserving the last complete\n * variant if publication fails. Stable sibling names also let the next boot\n * recover an interrupted old-to-backup rename before doing new work.\n */\nfunction publishVariant(staging: string, dest: string): void {\n const previous = `${dest}.previous`\n if (!existsSync(dest) && existsSync(previous)) renameSync(previous, dest)\n if (existsSync(previous)) rmSync(previous, { recursive: true, force: true })\n if (existsSync(dest)) renameSync(dest, previous)\n try {\n renameSync(staging, dest)\n } catch (error) {\n if (!existsSync(dest) && existsSync(previous)) renameSync(previous, dest)\n throw error\n }\n rmSync(previous, { recursive: true, force: true })\n}\n\n/**\n * Whether a host's terminal stack can allocate a PTY process *on this platform*.\n *\n * The world's `bash` is the host's persistent-shell stack, and on Windows that\n * stack needs a platform process inspector. `@deepseek-ai/dsh-subprocess-local`\n * only grew one in `0.1.0-rc.8`: in `0.1.0-rc.7` `spawnTerminal` throws\n * `subprocess-local: terminal inspection is unsupported on platform win32`\n * before the process is started, so *every* persistent shell fails there — a\n * real session shows the model getting that error for each `bash` call while\n * grep/glob (which never touch the PTY) keep working. The host itself ships the\n * same gap: that release's Minimal preset mounts `persistent-bash` with no\n * `disabled:` guard for Windows.\n *\n * The probe asks the substrate the question directly instead of pattern-matching\n * a version: `spawnTerminal` builds its inspector before `node-pty` starts the\n * program, so handing it a program that cannot exist reaches that check and\n * nothing else — no process is created either way, and the failure message says\n * which half failed. A release that can build the inspector reports an ordinary\n * spawn failure instead, which is the \"supported\" answer.\n * @param ctx - plugin context; the `subprocess` service is looked up with `get`\n * and waited for briefly, because the world is generated during profile boot.\n * @returns true when the persistent shell may be mounted.\n */\nasync function supportsPersistentShell(ctx: Context): Promise<boolean> {\n // POSIX hosts have an inspector on every declared release, and a WSL world is\n // Windows-only anyway.\n if (process.platform !== 'win32') return true\n const subprocess = await waitForSubprocess(ctx)\n if (subprocess?.spawnTerminal === undefined) return true\n try {\n const handle = await subprocess.spawnTerminal({\n argv: ['dsh-wsl-workspace-pty-probe-does-not-exist'],\n cwd: process.cwd(),\n rows: 24,\n cols: 80,\n graceMs: 1_000,\n })\n // Unexpectedly alive: this host starts a PTY for a missing program, so the\n // terminal stack works. Take the probe process down again.\n await handle?.terminate?.()\n return true\n } catch (error) {\n return !isTerminalInspectionUnsupported(error)\n }\n}\n\n/**\n * Whether one spawn failure is the missing-platform-inspector error.\n * @param error - the rejection from `spawnTerminal`.\n * @returns true when the host cannot inspect terminal processes here.\n */\nexport function isTerminalInspectionUnsupported(error: unknown): boolean {\n return /terminal inspection is unsupported on platform/i.test(messageOf(error))\n}\n\n/** The `subprocess` service face the probe needs. */\ninterface SubprocessProbeFace {\n spawnTerminal(spec: {\n argv: readonly string[]\n cwd: string\n rows: number\n cols: number\n graceMs: number\n }): Promise<{ terminate?: () => Promise<void> | void } | undefined>\n}\n\n/**\n * Look up the `subprocess` service, giving profile boot a moment to publish it.\n *\n * Bounded: the world is generated in a fire-and-forget effect, so this wait never\n * blocks profile boot, and the service is normally already published by the base\n * bundles the web app mounts before this plugin's own injection resolves. An\n * absent service is answered as \"supported\" by the caller, which is the\n * behaviour this plugin shipped before the probe existed.\n * @param ctx - plugin context.\n * @returns the service, or undefined when this deployment has none yet.\n */\nasync function waitForSubprocess(ctx: Context): Promise<SubprocessProbeFace | undefined> {\n for (let attempt = 0; attempt < 20; attempt += 1) {\n const service = ctx.get('subprocess') as unknown as SubprocessProbeFace | undefined\n if (service !== undefined) return service\n await new Promise(resolve => setTimeout(resolve, 100))\n }\n return undefined\n}\n\n/** Materialize one WSL variant per healthy source preset. */\nasync function materializeVariants(\n agentPresets: AgentPresetsService,\n dshHome: string,\n paths: { shell: string; fs: string; relay: string; node: string; sandbox: string; search: string; jobs: string },\n persistentShell: boolean,\n track: (dispose: unknown) => void,\n): Promise<void> {\n const presets = await agentPresets.list()\n const userRoot = join(dshHome, '.agent-presets')\n const generated = new Set<string>()\n for (const preset of presets) {\n if (preset.broken !== undefined) continue\n if (isWslVariantId(preset.id)) continue\n const variantId = variantIdFor(preset.id)\n const composition = await readPresetComposition(agentPresets, preset)\n const transformed = transformPresetForWsl(composition.content, paths.shell, paths.fs, persistentShell ? {\n relayPath: paths.relay,\n nodePath: paths.node,\n sandboxPath: paths.sandbox,\n } : undefined, paths.search, paths.jobs)\n // 0.1.7-alpha.1+ publishes a variant as a declaration row (the composition is\n // already the exact entry-list dialect the declaration wants, so the only\n // conversion is YAML back to rows); earlier releases still discover one as\n // a directory under the roster's scanned user root.\n if (typeof agentPresets.register === 'function') {\n const plugins = await parseVariantComposition(transformed)\n // A shipped mode keeps its bilingual label; a custom preset keeps the\n // display name it published, falling back to its id.\n const declaration: PresetDeclaration = {\n id: variantId,\n name: variantName(preset.id, composition.name ?? preset.name ?? preset.id),\n description: variantDescription(preset.id),\n ...(preset.order === undefined ? {} : { order: preset.order }),\n plugins,\n }\n track(await agentPresets.register(declaration))\n continue\n }\n if (preset.path === undefined) {\n throw new Error(`agentPresets: roster entry \"${preset.id}\" carries no path on this release`)\n }\n const dir = join(userRoot, variantId)\n const staging = `${dir}.staging`\n rmSync(staging, { recursive: true, force: true })\n // A preset directory is an opaque, self-contained plugin unit. Mirror it\n // without interpreting local code or asset names, then overwrite only the\n // two files owned by this generator.\n cpSync(dirname(preset.path), staging, { recursive: true, force: true })\n writeFileSync(join(staging, 'agent.cordis.yml'), transformed, 'utf8')\n const labels = MODE_DISPLAY_LABELS[preset.id]\n let name = variantName(preset.id, preset.id)\n let orderLine = ''\n try {\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\n if (labels === undefined) {\n // Custom presets keep their own display name; shipped modes use the\n // bilingual labels above so both locales can identify the variant.\n const match = /^name:\\s*(.+)$/m.exec(meta)\n if (match?.[1] !== undefined && match[1].trim() !== '') {\n // The scalar is copied out of the source's YAML as written, so a\n // quoted `name: 'Data mode'` would otherwise reach the picker with\n // its quotes doubled into the variant's own scalar.\n name = variantName(preset.id, unquoteScalar(match[1].trim()))\n }\n }\n // Inherit the source's declared order so the WSL variants line up with\n // the local modes in the roster (standard, PTC, minimal, cordis).\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\n } catch {\n // Absent or unreadable display metadata falls back to the id-based name.\n }\n writeFileSync(\n join(staging, 'preset.yml'),\n `name: ${yamlScalar(name)}\\n`\n + orderLine\n + `description: ${yamlScalar(variantDescription(preset.id))}\\n`,\n 'utf8',\n )\n publishVariant(staging, dir)\n generated.add(variantId)\n }\n // Clean up the retired directory mechanism. `generated` holds the\n // directories the directory path wrote on THIS boot; from 0.1.7-alpha.1 on it is\n // always empty, because a variant is a declaration row and the roster no\n // longer scans this root at all — so every `wsl*` entry left here (the legacy\n // standalone `wsl` mode included) is inert leftover, and a `wsl-<mode>/`\n // beside a registered `wsl-<mode>` declaration would only mislead. An absent\n // root (a fresh install that never used the old mechanism) is not a failure.\n if (existsSync(userRoot)) {\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n if (!/^wsl(-[a-z0-9-]+)?$/.test(entry.name)) continue\n if (generated.has(entry.name)) continue\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\n }\n }\n}\n\n/** Function-plugin plugin contract. */\nexport const name = 'dsh-wsl-workspace'\n\n/** Required services. */\nexport const inject = ['webServer']\n\n/** Validated plugin config (schemastery applied the defaults). */\nexport const Config: z<Config> = z.object({\n route: z.string().default(DEFAULT_ROUTE),\n})\n\n/**\n * Apply the host half: materialize a `wsl-<mode>` variant for every healthy\n * roster preset, register the data route, and\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\n * shell executor can resolve a plain Linux `workdir` to the calling\n * session's distribution.\n * @param ctx - the host plugin context.\n * @param config - the validated configuration.\n */\nexport function apply(ctx: Context, config: Config): void {\n const resolved = config as ResolvedConfig\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\n // The persistent shell runs the host PTY backend on the relay, which starts\n // `wsl.exe … bash` under that PTY: two paths the generated preset must carry.\n const relayPath = join(packageRoot, 'lib', 'wsl-relay.js').replace(/\\\\/g, '/')\n const sandboxPath = join(packageRoot, 'lib', 'wsl-sandbox.js').replace(/\\\\/g, '/')\n // The in-distribution `grep`/`glob` twin that replaces the host search suite.\n const searchPath = join(packageRoot, 'lib', 'wsl-search.js').replace(/\\\\/g, '/')\n // The persistent shell has no `run_in_background` of its own; this is the\n // producer that gives the world's `job_*` tools something to track.\n const jobsPath = join(packageRoot, 'lib', 'wsl-jobs.js').replace(/\\\\/g, '/')\n const nodePath = process.execPath.replace(/\\\\/g, '/')\n\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\n if (agentPresets !== undefined) {\n ctx.effect(() => {\n // On 0.1.7-alpha.1+ the variants are declaration rows in Host state rather\n // than files, so the effect owns their disposers: a plugin unload or hot\n // reload retires them instead of leaving orphans the next apply could not\n // replace (`Duplicate agent preset: wsl-<mode>`).\n let stopped = false\n const disposers: (() => Promise<void>)[] = []\n const track = (dispose: unknown): void => {\n if (typeof dispose !== 'function') return\n const retire = dispose as () => Promise<void>\n if (stopped) {\n void Promise.resolve(retire()).catch(() => {})\n return\n }\n disposers.push(retire)\n }\n void (async () => {\n const persistentShell = await supportsPersistentShell(ctx)\n await materializeVariants(agentPresets, dshHome, {\n shell: shellPath,\n fs: fsPath,\n relay: relayPath,\n node: nodePath,\n sandbox: sandboxPath,\n search: searchPath,\n jobs: jobsPath,\n }, persistentShell, track)\n })().catch((error) => {\n // Variant generation is best-effort over a live roster: a missing or\n // unreadable source preset must not take the whole plugin down, but\n // the failure is surfaced loudly rather than hidden.\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\n })\n return () => {\n stopped = true\n for (const retire of disposers.splice(0, disposers.length)) {\n void Promise.resolve(retire()).catch(() => {})\n }\n }\n }, 'dsh-wsl-workspace: WSL preset variants')\n }\n\n const skills = ctx.get('skills') as unknown as WslSkillsRegistryFace | undefined\n if (skills !== undefined && typeof skills.registerProvider === 'function') {\n // The shipped skill-filesystem provider scans only the session cwd's\n // project root, so WSL workspaces whose `.dsh/skills` live in nested\n // projects would show an empty skill catalog. This provider mirrors the\n // host's project discovery for WSL UNC workspaces, bounded to the\n // workspace root (issue #10). The method check keeps a host whose\n // `skills` service exists with a different shape from breaking plugin\n // load; the provider is an enhancement, never a load-time requirement.\n ctx.effect(() => skills.registerProvider(\n control => new WslSkillsProvider(control),\n ), 'dsh-wsl-workspace: WSL workspace skills provider')\n }\n\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\n if (shellEnv !== undefined) {\n ctx.effect(() => shellEnv.register({\n name: 'wsl-workspace-distro',\n variables: {\n DSH_WSL_DISTRO: {\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\n },\n DSH_WSL_USER: {\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\n },\n },\n resolve(execution) {\n const cwd = execution.agent?.session.header.cwd\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\n if (unc !== null) {\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\n return username === undefined || username === ''\n ? { DSH_WSL_DISTRO: unc.distro }\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\n }\n // A Windows-drive cwd belongs to a `/mnt/<drive>` WSL workspace (9P\n // cannot serve drvfs, so those register under their drive path): the\n // stored distro drives `wsl.exe -d` for bash.\n if (cwd !== undefined && /^[A-Za-z]:[\\\\/]/.test(cwd)) {\n const entry = getWindowsWorkspace(cwd)\n if (entry !== undefined && entry.distro !== undefined && entry.distro !== '') {\n return entry.username === undefined || entry.username === ''\n ? { DSH_WSL_DISTRO: entry.distro }\n : { DSH_WSL_DISTRO: entry.distro, DSH_WSL_USER: entry.username }\n }\n }\n return {}\n },\n }), 'dsh-wsl-workspace: per-session distro env fact')\n }\n\n const webServer = ctx.get('webServer') as unknown as WebServerService\n ctx.effect(() => webServer.register({\n kind: 'exact',\n path: resolved.route,\n handler: async (req, res) => {\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\n json(res, 403, { ok: false, error: 'loopback-only' })\n return\n }\n if (req.method !== 'POST') {\n json(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n let body: Record<string, unknown>\n try {\n body = await readBody(req)\n } catch (error) {\n json(res, 400, { ok: false, error: messageOf(error) })\n return\n }\n const method = typeof body.method === 'string' ? body.method : ''\n const params = body.params === undefined ? {} : body.params\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\n json(res, 400, { ok: false, error: 'params must be an object' })\n return\n }\n try {\n const value = await dispatch(method, params as Record<string, unknown>)\n json(res, 200, { ok: true, value })\n } catch (error) {\n json(res, 200, { ok: false, error: messageOf(error) })\n }\n },\n }), 'dsh-wsl-workspace: dialog data route')\n}\n"],"mappings":"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"}
|