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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-wsl-workspace",
|
|
3
3
|
"description": "WSL workspace support for DeepSeek Harness: add a WSL workspace from the web GUI and run the whole agent session (bash + file tools) inside the WSL distribution, VS Code Remote-WSL style. No toolchain install inside WSL required.",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"0.1.2-rc.1": "compatible",
|
|
46
46
|
"0.1.3-alpha.2": "compatible",
|
|
47
47
|
"0.1.5-rc.1": "compatible",
|
|
48
|
-
"0.1.5-rc.2": "compatible"
|
|
48
|
+
"0.1.5-rc.2": "compatible",
|
|
49
|
+
"0.1.7-rc.1": "compatible"
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
},
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
],
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"@deepseek-ai/cordis": "*",
|
|
72
|
+
"@deepseek-ai/cordis-plugin-include": "*",
|
|
71
73
|
"@deepseek-ai/dsh-fs": "*",
|
|
72
74
|
"@deepseek-ai/dsh-fs-local": "*",
|
|
73
75
|
"@deepseek-ai/dsh-sandbox": "*",
|
|
@@ -76,10 +78,12 @@
|
|
|
76
78
|
"@deepseek-ai/dsh-timeout": "*",
|
|
77
79
|
"@deepseek-ai/dsh-tool-fs-search": "*",
|
|
78
80
|
"@deepseek-ai/dsh-tools": "*",
|
|
79
|
-
"@deepseek-ai/schemastery": "*"
|
|
81
|
+
"@deepseek-ai/schemastery": "*",
|
|
82
|
+
"js-yaml": "*"
|
|
80
83
|
},
|
|
81
84
|
"peerDependenciesMeta": {
|
|
82
85
|
"@deepseek-ai/cordis": { "optional": true },
|
|
86
|
+
"@deepseek-ai/cordis-plugin-include": { "optional": true },
|
|
83
87
|
"@deepseek-ai/dsh-fs": { "optional": true },
|
|
84
88
|
"@deepseek-ai/dsh-fs-local": { "optional": true },
|
|
85
89
|
"@deepseek-ai/dsh-sandbox": { "optional": true },
|
|
@@ -88,7 +92,8 @@
|
|
|
88
92
|
"@deepseek-ai/dsh-timeout": { "optional": true },
|
|
89
93
|
"@deepseek-ai/dsh-tool-fs-search": { "optional": true },
|
|
90
94
|
"@deepseek-ai/dsh-tools": { "optional": true },
|
|
91
|
-
"@deepseek-ai/schemastery": { "optional": true }
|
|
95
|
+
"@deepseek-ai/schemastery": { "optional": true },
|
|
96
|
+
"js-yaml": { "optional": true }
|
|
92
97
|
},
|
|
93
98
|
"files": [
|
|
94
99
|
"lib",
|
package/src/client/locales.ts
CHANGED
|
@@ -41,8 +41,8 @@ export const zh: Record<string, string> = {
|
|
|
41
41
|
'help.compat.versionLabel': '插件版本',
|
|
42
42
|
'help.compat.unknown': '未能读取版本与兼容声明(宿主端没有响应)',
|
|
43
43
|
'help.compat.body': '上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\n插件在运行时自动识别 DSH 版本并选用对应的 API;两边都不支持时会明确报错,而不是留下一个空工作区。\n版本不在列表里通常仍然可用,但未经验证。',
|
|
44
|
-
'help.news.title': '本次更新(0.7.
|
|
45
|
-
'help.news.body': '
|
|
44
|
+
'help.news.title': '本次更新(0.7.3)',
|
|
45
|
+
'help.news.body': '插件在 DSH 0.1.7 上重新可加载、模式也回来了:那条线把预设平面改成了声明式的 @deepseek-ai/dsh-agent-preset 行,read() 变成 readDocument()、AgentPreset 不再有 path,于是变体生成器每次启动都抛 agentPresets.read is not a function,选择器里一个 wsl-* 模式都不会出现。现在按能力探测两代接口,两代都服务。\n在 0.1.7 上变体改为通过 ctx.agentPresets.register() 发布成一条声明;注销器由插件的 effect 持有,卸载或热重载会注销这些变体,而不是留下下一次 apply 无法替换的孤儿。更早的版本仍走原来的目录通道,行为不变。\n该通道下世界自己的 provider 行必须以 file: URL 命名:由声明挂载的预设由注册表自己的 entry tree 导入,它不会像启动期的 Include 那样把绝对路径翻译成 file: URL,否则那些行根本不会启动、整个变体被判为不可用而拒绝。relay 与解释器这类配置值仍保持原生路径。\n修掉了 PTC 变体的显示名:标签表里只登记了旧 id code,而 0.1.7 不发布显示名,于是模式显示为 WSL · ptc、描述也退化成 WSL execution world for ptc: …;现在两条通道都显示为 WSL · PTC mode(PTC 模式)。\n兼容性清单增加 0.1.7-rc.1;声明通道需要的两个模块(@deepseek-ai/cordis-plugin-include、js-yaml)在调用时解析,并声明为可选同伴依赖。',
|
|
46
46
|
'help.usage.title': '用法与特性',
|
|
47
47
|
'help.usage.body': '点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 与 persistent-bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。9P 解不开的符号链接交给发行版 readlink 解析出真实路径(每次查找最多 32 条)后继续扫描,链接进来的项目与它下面的嵌套项目都能扫到。\n目录不再冻结:对 UNC 关闭文件监视之后,插件每 3 秒重查一次已发布的技能目录、并比对每个技能文件的修改时间与大小,所以新增、删除与改写都会在下一个回合生效;完整重新发现每 30 秒一次,用于找到此前不存在的技能目录(技能正文始终实时读取)。\n文件搜索由发行版内的 grep / glob 提供,源预设没有这两个工具的模式(极简)不会多出来:grep 用 GNU grep -E(\\d、\\w、(?i) 可用,环视与反向引用不支持),跳过隐藏项与 node_modules,不读 .gitignore;glob 用 GNU find 列文件、按 gitignore 风格匹配(* 不跨目录、** 跨、{a,b}、前导 ! 取反),按修改时间从旧到新排序。\n文件工具在链接处按真实路径工作,并按真实路径判策略:链接指向工作区外就等于工作区外。\nbash 由 PTY 承载的持久 shell 提供:登录环境、起始目录就是会话工作区,cd / export / venv / 后台任务跨调用保留(它取代了原先一次性 bash)。\n需要可跟踪的后台任务时用 bash_background:它立刻返回 job id,job_list / job_output(增量读取)/ job_kill 都作用于它;bash 本身没有 run_in_background 参数,传了会被忽略。job id 只在本次 DSH 进程内唯一,重启后会重新编号,引用前先用 job_list 确认。\nbash 与文件工具的 shell 都在发行版内运行、不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。',
|
|
48
48
|
'help.known.title': '已知问题',
|
|
@@ -89,8 +89,8 @@ export const en: Record<string, string> = {
|
|
|
89
89
|
'help.compat.versionLabel': 'Plugin version',
|
|
90
90
|
'help.compat.unknown': 'Version and compatibility declaration unavailable (the host side did not answer)',
|
|
91
91
|
'help.compat.body': 'The chips above are the DSH releases this build declares, each verified on an isolated instance (own DSH_HOME, dependencies pinned to that release, full check suite).\nThe plugin detects the DSH generation at runtime and picks the matching API; a release exposing neither fails loudly instead of leaving an empty workspace.\nA release outside the list usually still works, but is unverified.',
|
|
92
|
-
'help.news.title': "What's new in 0.7.
|
|
93
|
-
'help.news.body': '
|
|
92
|
+
'help.news.title': "What's new in 0.7.3",
|
|
93
|
+
'help.news.body': 'The plugin loads on DSH 0.1.7 again, and its modes come back: that line turned the preset plane into declarative @deepseek-ai/dsh-agent-preset rows, renamed read() to readDocument() and dropped AgentPreset.path.\nThe variant generator therefore threw agentPresets.read is not a function on every boot and no wsl-* mode reached the picker; both roster generations are now probed by capability.\nOn 0.1.7 a variant is published as a declaration through ctx.agentPresets.register(); the effect installed by the plugin owns the returned disposers, so an unload or hot reload retires the variants instead of leaving orphans the next apply could not replace.\nEarlier releases keep the directory channel, unchanged.\nOn that channel the provider rows of the world must be named as file: URLs: a preset mounted from a declaration is imported by the entry tree of the registry, which does not translate an absolute path the way the boot-time Include does.\nWithout that rewrite those rows never started and the whole variant was refused as unusable. Config values such as the relay and the interpreter stay native paths.\nThe PTC variant is named again: the label table carried only the older id code, and 0.1.7 publishes no display name, so the mode read WSL · ptc with the generic description WSL execution world for ptc: …; both channels now show WSL · PTC mode(PTC 模式).\nThe compatibility manifest adds 0.1.7-rc.1, and the two modules the declaration channel needs (@deepseek-ai/cordis-plugin-include, js-yaml) are resolved at call time and declared as optional peers.',
|
|
94
94
|
'help.usage.title': 'Usage and features',
|
|
95
95
|
'help.usage.body': 'Click the W button at the sidebar foot, pick a distribution, type or browse to a Linux path, press Check, then Create & open.\nIn that session the bash tool and the file tools run inside the distribution, so every path the model sees is a Linux path; Windows drives stay reachable as /mnt/<drive>.\nEach mode (Standard / PTC / Minimal / Creator) has a WSL variant in the mode picker, named like WSL · Standard mode.\nThe optional username behaves like wsl.exe -u <user> for bash and persistent-bash; the file tools go through the Windows-side share and are unaffected.\nThe skill catalog is discovered from the nearest .git ancestor of the session cwd downwards (.dsh/skills and .agents/skills, nested projects included), bounded to 4 levels / 64 skill directories / 4096 visited directories, and cached per scan root for 10 seconds.\nA link the share cannot follow is resolved through the distribution (wsl.exe readlink, at most 32 per lookup) and the scan continues at the real path, so a linked-in project and its own nested projects are found too.\nThe catalog is not frozen: published skills directories are re-checked every 3 seconds (skill file mtime + size), so an add, remove or edit appears on the next turn; a 30-second walk finds a skills directory that did not exist before.\nFile search comes from grep / glob inside the distribution, and a mode that mounts no search suite (Minimal) gains none. grep is GNU grep -E (no lookaround or backreferences), skips hidden entries and node_modules, and does not read .gitignore; glob matches gitignore-style patterns here, oldest first.\nThe file tools work at the resolved real path of a link, and the policy is judged there too: a link out of the workspace is an outside write.\n`bash` is a PTY-backed stateful shell: login environment, starting directory the session workspace, and cd / exports / background jobs survive between calls.\nFor a tracked background job use bash_background: it returns a job id immediately, and job_list / job_output (incremental) / job_kill act on it.\n`bash` itself has no run_in_background parameter and ignores one; job ids are unique within this DSH process, so confirm with job_list before acting on one.\nBoth it and the file tools\' shell run inside the distribution, outside the DSH file policy; read/write/edit are inside it, and workspace-write only writes inside the workspace.',
|
|
96
96
|
'help.known.title': 'Known issues',
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host half of dsh-wsl-workspace. Three responsibilities:
|
|
3
3
|
*
|
|
4
|
-
* 1.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
4
|
+
* 1. Publish a `wsl-<mode>` variant for every healthy roster preset, so the
|
|
5
|
+
* WSL execution world — `shell-wsl` + `fs-wsl` behind one entry-local
|
|
6
|
+
* realm, with `tool-bash`/`tool-fs` consumers — composes with ANY mode
|
|
7
|
+
* instead of being a mode itself. The channel follows the release:
|
|
8
|
+
* `0.1.7-alpha.1+` stopped scanning any user preset root and builds its roster
|
|
9
|
+
* from declarative `@deepseek-ai/dsh-agent-preset` rows, so there a variant
|
|
10
|
+
* is a *declaration registered through `ctx.agentPresets`* — and only that
|
|
11
|
+
* path rewrites the absolute provider specifiers below into `file:` URLs,
|
|
12
|
+
* because a registered preset's entry tree does not translate them the way
|
|
13
|
+
* the boot-time Include does; earlier releases still get a variant directory
|
|
14
|
+
* under `<dshHome>/.agent-presets/`, whose legacy standalone `wsl` directory
|
|
15
|
+
* and stale variants are removed on boot.
|
|
13
16
|
*
|
|
14
17
|
* 2. Serve the browser dialog's data route (`/wsl-workspace/api`):
|
|
15
18
|
* distribution discovery, one-level directory listing, path checks, and
|
|
@@ -26,8 +29,8 @@
|
|
|
26
29
|
import { Context } from '@deepseek-ai/cordis'
|
|
27
30
|
import z from '@deepseek-ai/schemastery'
|
|
28
31
|
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
29
|
-
import { fileURLToPath } from 'node:url'
|
|
30
|
-
import { dirname, join } from 'node:path'
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
33
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
31
34
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
32
35
|
import { homedir } from 'node:os'
|
|
33
36
|
import { joinUnc, mntToWindowsPath, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'
|
|
@@ -47,10 +50,18 @@ export const DEFAULT_ROUTE = '/wsl-workspace/api'
|
|
|
47
50
|
* preset.yml text verbatim, so the plugin writes one bilingual string so
|
|
48
51
|
* both locales can identify each variant. Custom presets keep their own
|
|
49
52
|
* name.
|
|
53
|
+
*
|
|
54
|
+
* This table is consulted before anything the release publishes, so every id
|
|
55
|
+
* the mode has ever carried needs an entry. `code` was the id up to
|
|
56
|
+
* `0.1.0-rc.8` and `ptc` is the id from `0.1.1` on; without the `ptc` entry the
|
|
57
|
+
* lookup returned undefined and the variant fell through to the release's own
|
|
58
|
+
* display name — which `0.1.7` does not publish at all, leaving the mode named
|
|
59
|
+
* `WSL · ptc` with the generic description `WSL execution world for ptc: …`.
|
|
50
60
|
*/
|
|
51
61
|
const MODE_DISPLAY_LABELS: Readonly<Record<string, { en: string; zh: string }>> = {
|
|
52
62
|
standard: { en: 'Standard mode', zh: '标准模式' },
|
|
53
63
|
code: { en: 'Code mode', zh: 'PTC 模式' },
|
|
64
|
+
ptc: { en: 'PTC mode', zh: 'PTC 模式' },
|
|
54
65
|
minimal: { en: 'Minimal mode', zh: '极简模式' },
|
|
55
66
|
cordis: { en: 'Creator mode', zh: '创造模式' },
|
|
56
67
|
}
|
|
@@ -352,10 +363,149 @@ async function dispatch(method: string, params: Record<string, unknown>): Promis
|
|
|
352
363
|
}
|
|
353
364
|
}
|
|
354
365
|
|
|
355
|
-
/**
|
|
366
|
+
/** One roster entry as the `ctx.agentPresets` face reports it. */
|
|
367
|
+
interface AgentPresetRosterEntry {
|
|
368
|
+
id: string
|
|
369
|
+
name?: string
|
|
370
|
+
description?: string
|
|
371
|
+
/** Roster position; `list()` sorts by it. */
|
|
372
|
+
order?: number
|
|
373
|
+
broken?: string
|
|
374
|
+
/** Legacy (≤ 0.1.6-alpha.2) only: the source preset's directory. */
|
|
375
|
+
path?: string
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** One preset's declared composition, as the declaration channel reports it. */
|
|
379
|
+
interface AgentPresetDocument {
|
|
380
|
+
agentPreset: string
|
|
381
|
+
/** The declared child plugin list as entry-list YAML, `!!js` expressions included. */
|
|
382
|
+
content: string
|
|
383
|
+
name?: string
|
|
384
|
+
description?: string
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** A declaration this plugin publishes; mirrors the Host's `PresetDefinition`. */
|
|
388
|
+
interface PresetDeclaration {
|
|
389
|
+
id: string
|
|
390
|
+
name?: string
|
|
391
|
+
description?: string
|
|
392
|
+
order?: number
|
|
393
|
+
plugins: readonly unknown[]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The `ctx.agentPresets` roster face this plugin consumes (optional service).
|
|
398
|
+
*
|
|
399
|
+
* Two generations sit behind this one shape, so both are probed by capability
|
|
400
|
+
* rather than by version:
|
|
401
|
+
*
|
|
402
|
+
* - `0.1.0-rc.7 … 0.1.6-alpha.2`: `list()` reports each preset's directory in
|
|
403
|
+
* `path`, and `read(id)` returns the composition text. A variant is a
|
|
404
|
+
* *directory* under the roster's scanned user root (`$DSH_HOME/.agent-presets/`).
|
|
405
|
+
* - `0.1.7-alpha.1+`: `read()` is renamed `readDocument()` and returns a document
|
|
406
|
+
* instead of a bare string, `AgentPreset.path` is gone, and **nothing scans
|
|
407
|
+
* the user root any more** — the roster is built from declarative
|
|
408
|
+
* `@deepseek-ai/dsh-agent-preset` rows carried by bundle patches. A variant
|
|
409
|
+
* is therefore a declaration row published through `register()`.
|
|
410
|
+
*/
|
|
356
411
|
interface AgentPresetsService {
|
|
357
|
-
list(): Promise<
|
|
358
|
-
|
|
412
|
+
list(): Promise<AgentPresetRosterEntry[]>
|
|
413
|
+
/** Legacy (≤ 0.1.6-alpha.2): the source composition text. */
|
|
414
|
+
read?(id: string): Promise<string>
|
|
415
|
+
/** 0.1.7-alpha.1+: the declared composition beside its published metadata. */
|
|
416
|
+
readDocument?(id: string): Promise<AgentPresetDocument>
|
|
417
|
+
/** 0.1.7-alpha.1+: publish a declaration; the returned disposer retires it. */
|
|
418
|
+
register?(definition: PresetDeclaration): Promise<() => Promise<void>>
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Read one preset's declared composition across both roster generations.
|
|
423
|
+
* @param agentPresets - the roster face.
|
|
424
|
+
* @param preset - the roster entry to read.
|
|
425
|
+
* @returns the composition text, plus the display name when the face publishes it.
|
|
426
|
+
*/
|
|
427
|
+
async function readPresetComposition(
|
|
428
|
+
agentPresets: AgentPresetsService,
|
|
429
|
+
preset: AgentPresetRosterEntry,
|
|
430
|
+
): Promise<{ content: string; name?: string }> {
|
|
431
|
+
if (typeof agentPresets.readDocument === 'function') {
|
|
432
|
+
const document = await agentPresets.readDocument(preset.id)
|
|
433
|
+
return document.name === undefined
|
|
434
|
+
? { content: document.content }
|
|
435
|
+
: { content: document.content, name: document.name }
|
|
436
|
+
}
|
|
437
|
+
if (typeof agentPresets.read === 'function') return { content: await agentPresets.read(preset.id) }
|
|
438
|
+
throw new Error(
|
|
439
|
+
`agentPresets: this DSH release exposes neither readDocument() nor read() (preset "${preset.id}")`,
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Parse one transformed composition back into declaration rows.
|
|
445
|
+
*
|
|
446
|
+
* The composition is the entry-list YAML dialect, whose `!!js` scalars are
|
|
447
|
+
* expression nodes the Loader evaluates when it activates the row.
|
|
448
|
+
* `@deepseek-ai/cordis-plugin-include` exports `entryListSchema` precisely so
|
|
449
|
+
* config tooling can round-trip that dialect, and it is the same schema the
|
|
450
|
+
* harness parses preset patches with. Parsing the text back is what lets the
|
|
451
|
+
* (text-level) WSL transform keep working unchanged now that a variant is a
|
|
452
|
+
* declaration row instead of a directory of YAML.
|
|
453
|
+
*
|
|
454
|
+
* Both modules are resolved at call time rather than at module load: they are
|
|
455
|
+
* Host-provided (`dsh` depends on `cordis-plugin-include`, which depends on
|
|
456
|
+
* `js-yaml`), and a release that ever drops them must fail this one variant
|
|
457
|
+
* rather than refuse to load the whole plugin.
|
|
458
|
+
* @param content - the variant composition text.
|
|
459
|
+
* @returns the declaration's plugin rows.
|
|
460
|
+
*/
|
|
461
|
+
async function parseVariantComposition(content: string): Promise<unknown[]> {
|
|
462
|
+
const includeSpecifier = '@deepseek-ai/cordis-plugin-include'
|
|
463
|
+
const yamlSpecifier = 'js-yaml'
|
|
464
|
+
const [include, yamlModule] = await Promise.all([
|
|
465
|
+
import(includeSpecifier) as Promise<{ entryListSchema: unknown }>,
|
|
466
|
+
import(yamlSpecifier) as Promise<{
|
|
467
|
+
load?(source: string, options: { schema: unknown }): unknown
|
|
468
|
+
default?: { load(source: string, options: { schema: unknown }): unknown }
|
|
469
|
+
}>,
|
|
470
|
+
])
|
|
471
|
+
const load = yamlModule.load ?? yamlModule.default?.load
|
|
472
|
+
if (typeof load !== 'function') throw new Error('js-yaml: no load() export')
|
|
473
|
+
const rows = load(content, { schema: include.entryListSchema })
|
|
474
|
+
if (!Array.isArray(rows)) throw new Error('the transformed composition did not parse as an entry list')
|
|
475
|
+
return toImportableSpecifiers(rows)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Rewrite every absolute local module specifier as a `file:` URL.
|
|
480
|
+
*
|
|
481
|
+
* The generated world names THIS package's built providers (`shell.js`,
|
|
482
|
+
* `fs.js`, …) by absolute path. That is what the directory mechanism needed:
|
|
483
|
+
* those rows sat in an Include-backed tree, and the boot-time Include
|
|
484
|
+
* translates an absolute path into a `file:` URL before importing it. A preset
|
|
485
|
+
* mounted from a *declaration* is loaded by the registry's own entry tree,
|
|
486
|
+
* which has no such translation — handing it `C:/…/lib/shell.js` leaves those
|
|
487
|
+
* rows without a fiber, the audit reports them "never started", and the whole
|
|
488
|
+
* variant is refused. Rewriting the specifier is therefore part of adapting to
|
|
489
|
+
* the declaration mechanism, not a change to what the variant mounts.
|
|
490
|
+
*
|
|
491
|
+
* Only `name` is touched: config values (the relay path handed to the PTY
|
|
492
|
+
* backend, the interpreter path in `shellPath`) must stay native filesystem
|
|
493
|
+
* paths. Group rows carry their children in a `config` array, so those are
|
|
494
|
+
* walked too.
|
|
495
|
+
* @param rows - the parsed declaration rows.
|
|
496
|
+
* @returns the same rows with their module specifiers made importable.
|
|
497
|
+
*/
|
|
498
|
+
function toImportableSpecifiers(rows: unknown[]): unknown[] {
|
|
499
|
+
const rewrite = (row: unknown): unknown => {
|
|
500
|
+
if (row === null || typeof row !== 'object') return row
|
|
501
|
+
const entry = row as { name?: unknown; config?: unknown }
|
|
502
|
+
if (typeof entry.name === 'string' && isAbsolute(entry.name)) {
|
|
503
|
+
entry.name = pathToFileURL(entry.name).href
|
|
504
|
+
}
|
|
505
|
+
if (Array.isArray(entry.config)) entry.config = entry.config.map(rewrite)
|
|
506
|
+
return row
|
|
507
|
+
}
|
|
508
|
+
return rows.map(rewrite)
|
|
359
509
|
}
|
|
360
510
|
|
|
361
511
|
/**
|
|
@@ -469,6 +619,7 @@ async function materializeVariants(
|
|
|
469
619
|
dshHome: string,
|
|
470
620
|
paths: { shell: string; fs: string; relay: string; node: string; sandbox: string; search: string; jobs: string },
|
|
471
621
|
persistentShell: boolean,
|
|
622
|
+
track: (dispose: unknown) => void,
|
|
472
623
|
): Promise<void> {
|
|
473
624
|
const presets = await agentPresets.list()
|
|
474
625
|
const userRoot = join(dshHome, '.agent-presets')
|
|
@@ -477,12 +628,33 @@ async function materializeVariants(
|
|
|
477
628
|
if (preset.broken !== undefined) continue
|
|
478
629
|
if (isWslVariantId(preset.id)) continue
|
|
479
630
|
const variantId = variantIdFor(preset.id)
|
|
480
|
-
const
|
|
481
|
-
const transformed = transformPresetForWsl(
|
|
631
|
+
const composition = await readPresetComposition(agentPresets, preset)
|
|
632
|
+
const transformed = transformPresetForWsl(composition.content, paths.shell, paths.fs, persistentShell ? {
|
|
482
633
|
relayPath: paths.relay,
|
|
483
634
|
nodePath: paths.node,
|
|
484
635
|
sandboxPath: paths.sandbox,
|
|
485
636
|
} : undefined, paths.search, paths.jobs)
|
|
637
|
+
// 0.1.7-alpha.1+ publishes a variant as a declaration row (the composition is
|
|
638
|
+
// already the exact entry-list dialect the declaration wants, so the only
|
|
639
|
+
// conversion is YAML back to rows); earlier releases still discover one as
|
|
640
|
+
// a directory under the roster's scanned user root.
|
|
641
|
+
if (typeof agentPresets.register === 'function') {
|
|
642
|
+
const plugins = await parseVariantComposition(transformed)
|
|
643
|
+
// A shipped mode keeps its bilingual label; a custom preset keeps the
|
|
644
|
+
// display name it published, falling back to its id.
|
|
645
|
+
const declaration: PresetDeclaration = {
|
|
646
|
+
id: variantId,
|
|
647
|
+
name: variantName(preset.id, composition.name ?? preset.name ?? preset.id),
|
|
648
|
+
description: variantDescription(preset.id),
|
|
649
|
+
...(preset.order === undefined ? {} : { order: preset.order }),
|
|
650
|
+
plugins,
|
|
651
|
+
}
|
|
652
|
+
track(await agentPresets.register(declaration))
|
|
653
|
+
continue
|
|
654
|
+
}
|
|
655
|
+
if (preset.path === undefined) {
|
|
656
|
+
throw new Error(`agentPresets: roster entry "${preset.id}" carries no path on this release`)
|
|
657
|
+
}
|
|
486
658
|
const dir = join(userRoot, variantId)
|
|
487
659
|
const staging = `${dir}.staging`
|
|
488
660
|
rmSync(staging, { recursive: true, force: true })
|
|
@@ -524,15 +696,20 @@ async function materializeVariants(
|
|
|
524
696
|
publishVariant(staging, dir)
|
|
525
697
|
generated.add(variantId)
|
|
526
698
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
699
|
+
// Clean up the retired directory mechanism. `generated` holds the
|
|
700
|
+
// directories the directory path wrote on THIS boot; from 0.1.7-alpha.1 on it is
|
|
701
|
+
// always empty, because a variant is a declaration row and the roster no
|
|
702
|
+
// longer scans this root at all — so every `wsl*` entry left here (the legacy
|
|
703
|
+
// standalone `wsl` mode included) is inert leftover, and a `wsl-<mode>/`
|
|
704
|
+
// beside a registered `wsl-<mode>` declaration would only mislead. An absent
|
|
705
|
+
// root (a fresh install that never used the old mechanism) is not a failure.
|
|
706
|
+
if (existsSync(userRoot)) {
|
|
707
|
+
for (const entry of readdirSync(userRoot, { withFileTypes: true })) {
|
|
708
|
+
if (!entry.isDirectory()) continue
|
|
709
|
+
if (!/^wsl(-[a-z0-9-]+)?$/.test(entry.name)) continue
|
|
710
|
+
if (generated.has(entry.name)) continue
|
|
531
711
|
rmSync(join(userRoot, entry.name), { recursive: true, force: true })
|
|
532
|
-
continue
|
|
533
712
|
}
|
|
534
|
-
if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue
|
|
535
|
-
if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })
|
|
536
713
|
}
|
|
537
714
|
}
|
|
538
715
|
|
|
@@ -576,6 +753,21 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
576
753
|
const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined
|
|
577
754
|
if (agentPresets !== undefined) {
|
|
578
755
|
ctx.effect(() => {
|
|
756
|
+
// On 0.1.7-alpha.1+ the variants are declaration rows in Host state rather
|
|
757
|
+
// than files, so the effect owns their disposers: a plugin unload or hot
|
|
758
|
+
// reload retires them instead of leaving orphans the next apply could not
|
|
759
|
+
// replace (`Duplicate agent preset: wsl-<mode>`).
|
|
760
|
+
let stopped = false
|
|
761
|
+
const disposers: (() => Promise<void>)[] = []
|
|
762
|
+
const track = (dispose: unknown): void => {
|
|
763
|
+
if (typeof dispose !== 'function') return
|
|
764
|
+
const retire = dispose as () => Promise<void>
|
|
765
|
+
if (stopped) {
|
|
766
|
+
void Promise.resolve(retire()).catch(() => {})
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
disposers.push(retire)
|
|
770
|
+
}
|
|
579
771
|
void (async () => {
|
|
580
772
|
const persistentShell = await supportsPersistentShell(ctx)
|
|
581
773
|
await materializeVariants(agentPresets, dshHome, {
|
|
@@ -586,14 +778,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
586
778
|
sandbox: sandboxPath,
|
|
587
779
|
search: searchPath,
|
|
588
780
|
jobs: jobsPath,
|
|
589
|
-
}, persistentShell)
|
|
781
|
+
}, persistentShell, track)
|
|
590
782
|
})().catch((error) => {
|
|
591
783
|
// Variant generation is best-effort over a live roster: a missing or
|
|
592
784
|
// unreadable source preset must not take the whole plugin down, but
|
|
593
785
|
// the failure is surfaced loudly rather than hidden.
|
|
594
786
|
console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)
|
|
595
787
|
})
|
|
596
|
-
return () => {
|
|
788
|
+
return () => {
|
|
789
|
+
stopped = true
|
|
790
|
+
for (const retire of disposers.splice(0, disposers.length)) {
|
|
791
|
+
void Promise.resolve(retire()).catch(() => {})
|
|
792
|
+
}
|
|
793
|
+
}
|
|
597
794
|
}, 'dsh-wsl-workspace: WSL preset variants')
|
|
598
795
|
}
|
|
599
796
|
|