dsh-wsl-workspace 0.4.3 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -3
- package/README.zh.md +101 -2
- package/TESTING.md +32 -6
- package/lib/client.js +44 -6
- package/lib/client.js.map +1 -1
- package/lib/fs.js +135 -4
- package/lib/fs.js.map +1 -1
- package/lib/index.js +508 -83
- package/lib/index.js.map +1 -1
- package/lib/links-BQxdJOMy.js +66 -0
- package/lib/links-BQxdJOMy.js.map +1 -0
- package/lib/shell.js +2 -2
- package/lib/{wsl-6VcgOvF9.js → wsl-CECXiGZH.js} +15 -2
- package/lib/{wsl-6VcgOvF9.js.map → wsl-CECXiGZH.js.map} +1 -1
- package/lib/{wsl-credentials-B6wIXsKt.js → wsl-credentials-BJXc9tfi.js} +2 -2
- package/lib/{wsl-credentials-B6wIXsKt.js.map → wsl-credentials-BJXc9tfi.js.map} +1 -1
- package/lib/wsl-jobs.js +139 -0
- package/lib/wsl-jobs.js.map +1 -0
- package/lib/wsl-relay.js +64 -0
- package/lib/wsl-relay.js.map +1 -0
- package/lib/wsl-sandbox.js +27 -0
- package/lib/wsl-sandbox.js.map +1 -0
- package/lib/wsl-search.js +957 -0
- package/lib/wsl-search.js.map +1 -0
- package/package.json +24 -4
- package/src/client/help.tsx +11 -0
- package/src/client/locales.ts +14 -6
- package/src/client/styles.ts +9 -0
- package/src/fs.ts +210 -6
- package/src/host/variants.ts +211 -24
- package/src/host/wsl-jobs.ts +229 -0
- package/src/host/wsl-relay.ts +89 -0
- package/src/host/wsl-sandbox.ts +47 -0
- package/src/host/wsl-search.ts +1166 -0
- package/src/host/wsl-skills.ts +353 -66
- package/src/index.ts +120 -6
- package/src/shared/links.ts +90 -0
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { defaultDistro, isAbsoluteLinuxPath, isValidWslUsername, joinUnc, listDistros, mntToWindowsPath, normalizeLinuxPath, parseWslUnc } from "./wsl-
|
|
2
|
-
import { canonicalWslUnc, getWindowsWorkspace, getWorkspaceUsername, listWorkspaceKeys, registerWindowsWorkspace, setWorkspaceUsername } from "./wsl-credentials-
|
|
1
|
+
import { defaultDistro, isAbsoluteLinuxPath, isValidWslUsername, joinUnc, listDistros, mntToWindowsPath, normalizeLinuxPath, parseWslUnc, uncToLinux } from "./wsl-CECXiGZH.js";
|
|
2
|
+
import { canonicalWslUnc, getWindowsWorkspace, getWorkspaceUsername, listWorkspaceKeys, registerWindowsWorkspace, setWorkspaceUsername } from "./wsl-credentials-BJXc9tfi.js";
|
|
3
|
+
import { resolveLinuxSymlinks } from "./links-BQxdJOMy.js";
|
|
3
4
|
import z from "@deepseek-ai/schemastery";
|
|
4
5
|
import { cpSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,41 +29,151 @@ const WORLD_ROWS = new Set([
|
|
|
28
29
|
"tool-fs",
|
|
29
30
|
"tool-fs-search",
|
|
30
31
|
"str-replace-editor",
|
|
32
|
+
"tool-str-replace-editor",
|
|
33
|
+
"wsl-world",
|
|
31
34
|
"filesystem",
|
|
32
35
|
"persistent-shell",
|
|
36
|
+
"persistent-bash",
|
|
37
|
+
"persistent-pwsh",
|
|
38
|
+
"terminal-bash",
|
|
39
|
+
"terminal-pwsh",
|
|
33
40
|
"custom-bash",
|
|
34
41
|
"bootstrap-filesystem"
|
|
35
42
|
]);
|
|
43
|
+
/** Execution-world rows that register the model-facing editor tool. */
|
|
44
|
+
const EDITOR_ROWS = new Set(["str-replace-editor", "tool-str-replace-editor"]);
|
|
45
|
+
/** The row ids this generator mounts inside its own world group. */
|
|
46
|
+
const WSL_WORLD_PROVIDER_IDS = ["shell-wsl", "fs-wsl"];
|
|
47
|
+
/**
|
|
48
|
+
* Whether a top-level row is this generator's own WSL world group.
|
|
49
|
+
*
|
|
50
|
+
* A preset copied from a generated variant (a user renaming `wsl-standard` to
|
|
51
|
+
* their own mode, say) carries the group under whatever id the copy kept, so
|
|
52
|
+
* the mounted provider ids identify it even when the row id changed. Without
|
|
53
|
+
* that check the variant would carry two world groups and the loader refuses
|
|
54
|
+
* the whole preset with `duplicate loader entry id: wsl-world`.
|
|
55
|
+
* @param block - the row's lines.
|
|
56
|
+
* @returns true when the row mounts this generator's WSL providers.
|
|
57
|
+
*/
|
|
58
|
+
function isWslWorldGroup(block) {
|
|
59
|
+
const text = block.join("\n");
|
|
60
|
+
return WSL_WORLD_PROVIDER_IDS.some((id) => new RegExp(`^\\s+- id: ${id}$`, "m").test(text));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The persistent-shell rows: the host's own PTY registry and its config-driven
|
|
64
|
+
* backend, told to run this plugin's relay (so the shell is a WSL one), plus
|
|
65
|
+
* the persistent tool that consumes them.
|
|
66
|
+
*
|
|
67
|
+
* Two shapes matter here. The registry is an agent-owned service — the shipped
|
|
68
|
+
* `persistent-shell` group keeps it in its own `terminals` realm for that
|
|
69
|
+
* reason — so this is a nested group of the world rather than three flat rows.
|
|
70
|
+
* And the persistent tool registers the tool name **`bash`**, exactly like the
|
|
71
|
+
* one-shot `dsh-tool-bash` row it replaces: both cannot be mounted (the tools
|
|
72
|
+
* registry rejects the duplicate and the whole preset fails to load), which is
|
|
73
|
+
* also why DSH's own Minimal mode describes itself as "a single-tool agent with
|
|
74
|
+
* a persistent shell". So a world with the relay paths swaps the shell tool
|
|
75
|
+
* instead of adding one, and a world without them keeps the one-shot row.
|
|
76
|
+
*/
|
|
77
|
+
function persistentShellRows(relayPath, nodePath) {
|
|
78
|
+
return [
|
|
79
|
+
" # Persistent shell: the host PTY registry and its backend, running",
|
|
80
|
+
" # this plugin's relay, which hands the PTY to `wsl.exe … bash`",
|
|
81
|
+
" # (distribution and user come from the session). It registers the",
|
|
82
|
+
" # `bash` tool, so it takes the place of the one-shot row above.",
|
|
83
|
+
" - id: persistent-shell",
|
|
84
|
+
" name: cordis:group",
|
|
85
|
+
" group: true",
|
|
86
|
+
" isolate:",
|
|
87
|
+
" terminals: true",
|
|
88
|
+
" config:",
|
|
89
|
+
" - id: pty",
|
|
90
|
+
" name: '@deepseek-ai/dsh-terminal'",
|
|
91
|
+
" - id: terminal-wsl",
|
|
92
|
+
" name: '@deepseek-ai/dsh-terminal-bash'",
|
|
93
|
+
" config:",
|
|
94
|
+
" backendType: wsl",
|
|
95
|
+
" shellDialect: bash",
|
|
96
|
+
` shellPath: '${nodePath.replace(/'/g, "''")}'`,
|
|
97
|
+
" shellArgs:",
|
|
98
|
+
` - '${relayPath.replace(/'/g, "''")}'`,
|
|
99
|
+
" - id: persistent-bash",
|
|
100
|
+
" name: '@deepseek-ai/dsh-tool-bash-persistent'",
|
|
101
|
+
" config:",
|
|
102
|
+
" backendType: wsl",
|
|
103
|
+
...SHELL_DESCRIPTION_ROWS
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The persistent tool's model-facing description, replacing the host default.
|
|
108
|
+
*
|
|
109
|
+
* The host tool's default says only that state persists, and its own Minimal
|
|
110
|
+
* preset goes further in the wrong direction by suggesting `sleep 10 &`. Both
|
|
111
|
+
* facts a WSL session needs are missing, and both were learned the hard way:
|
|
112
|
+
*
|
|
113
|
+
* - The shell is one process for the whole Agent, so a `cd` in one call decides
|
|
114
|
+
* where the *next* call starts. A model that read the one-shot tool's contract
|
|
115
|
+
* ("each call runs in a fresh shell — pass `workdir` instead of using `cd`")
|
|
116
|
+
* will be surprised, and a probe left in `/tmp` makes every later relative path
|
|
117
|
+
* resolve somewhere it never named.
|
|
118
|
+
* - The host wraps each command as `eval -- $'…'`. A trailing `&` therefore
|
|
119
|
+
* backgrounds the *whole* wrapped command: the tool's completion marker is
|
|
120
|
+
* printed before the work runs, so the call reports no output and exit code 0
|
|
121
|
+
* while the real output arrives later and can land inside the next call's
|
|
122
|
+
* output window. `( … ) &` on its own line keeps the `&` on the subshell and
|
|
123
|
+
* leaves the wrapper's sequencing intact.
|
|
124
|
+
*
|
|
125
|
+
* `description` is a supported key on every declared release (its `Config` schema
|
|
126
|
+
* carries it from 0.1.0-rc.7 on), so the override is version-safe.
|
|
127
|
+
*/
|
|
128
|
+
const SHELL_DESCRIPTION_ROWS = [
|
|
129
|
+
" description: |-",
|
|
130
|
+
" Run commands in a persistent bash shell inside this WSL distribution. State, including",
|
|
131
|
+
" the current directory and exported environment variables, persists across calls: use",
|
|
132
|
+
" absolute paths or an explicit `cd` at the start of a command instead of relying on where",
|
|
133
|
+
" the previous call left the shell. The shell runs as the workspace's Linux user; Windows",
|
|
134
|
+
" files are reachable as /mnt/<drive>, and no toolchain install is required.",
|
|
135
|
+
" * This tool takes `command` only. It has no `run_in_background` parameter, and passing",
|
|
136
|
+
" one is ignored - use the `bash_background` tool when the mode provides it, or background",
|
|
137
|
+
" a subshell as below.",
|
|
138
|
+
" * To leave work running without a tracked job, put it in a subshell with the `&` on its",
|
|
139
|
+
" own line: `( long-job > log 2>&1 ) &`. Then poll the log file in a later call.",
|
|
140
|
+
" * Never end a `&&` chain with `&`. That backgrounds the whole command, so the call",
|
|
141
|
+
" returns immediately with no output and exit code 0, and the real output arrives later,",
|
|
142
|
+
" possibly inside the next call's output.",
|
|
143
|
+
" * Avoid commands that wait for stdin: an interactive foreground child can run until the",
|
|
144
|
+
" command timeout, and a timeout resets the shell and discards its state."
|
|
145
|
+
];
|
|
36
146
|
/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */
|
|
37
|
-
function wslWorldGroup(shellPath, fsPath, includeEditor) {
|
|
147
|
+
function wslWorldGroup(shellPath, fsPath, includeEditor, persistent, searchPath, jobsPath, sawJobs = false) {
|
|
38
148
|
return [
|
|
39
149
|
"# ── WSL execution world (dsh-wsl-workspace variant) ─────────────────────",
|
|
40
150
|
"# The shell and fs services are provided entry-locally (the isolate",
|
|
41
151
|
"# realm); host services (tools registry, shell-env, jobs) fall through.",
|
|
42
|
-
"# tool-fs-search is intentionally absent: the packaged ripgrep runs on",
|
|
43
|
-
"# the Windows host and cannot open Linux paths; WSL sessions search with",
|
|
44
|
-
"# shell tools instead.",
|
|
45
152
|
"- id: wsl-world",
|
|
46
153
|
" name: cordis:group",
|
|
47
154
|
" group: true",
|
|
48
155
|
" isolate:",
|
|
49
156
|
" shell: true",
|
|
50
157
|
" fs: true",
|
|
158
|
+
...persistent === void 0 ? [] : [" sandbox: true"],
|
|
51
159
|
" config:",
|
|
52
160
|
` - id: shell-wsl`,
|
|
53
161
|
` name: '${shellPath.replace(/'/g, "''")}'`,
|
|
54
162
|
" - id: fs-wsl",
|
|
55
163
|
` name: '${fsPath.replace(/'/g, "''")}'`,
|
|
56
|
-
" - id:
|
|
57
|
-
" name: '@deepseek-ai/dsh-tool-bash'",
|
|
164
|
+
...persistent === void 0 ? [] : [" - id: sandbox-wsl", ` name: '${persistent.sandboxPath.replace(/'/g, "''")}'`],
|
|
165
|
+
...persistent === void 0 ? [" - id: tool-bash", " name: '@deepseek-ai/dsh-tool-bash'"] : [],
|
|
58
166
|
" - id: tool-fs",
|
|
59
167
|
" name: '@deepseek-ai/dsh-tool-fs'",
|
|
168
|
+
...searchPath === void 0 ? [] : [" - id: search-wsl", ` name: '${searchPath.replace(/'/g, "''")}'`],
|
|
169
|
+
...jobsPath === void 0 || persistent === void 0 || !sawJobs ? [] : [" - id: jobs-wsl", ` name: '${jobsPath.replace(/'/g, "''")}'`],
|
|
60
170
|
...includeEditor ? [
|
|
61
171
|
" - id: str-replace-editor",
|
|
62
172
|
" name: '@deepseek-ai/dsh-tool-str-replace-editor'",
|
|
63
173
|
" config:",
|
|
64
174
|
" maxOutputChars: 16000"
|
|
65
175
|
] : [],
|
|
176
|
+
...persistent === void 0 ? [] : persistentShellRows(persistent.relayPath, persistent.nodePath),
|
|
66
177
|
""
|
|
67
178
|
].join("\n");
|
|
68
179
|
}
|
|
@@ -228,24 +339,38 @@ function appendPersona(lines, span) {
|
|
|
228
339
|
/**
|
|
229
340
|
* Transform one source preset composition into its WSL variant: drop the
|
|
230
341
|
* execution-world rows, keep everything else verbatim, and append the WSL
|
|
231
|
-
* world group.
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
342
|
+
* world group. A row id that appears twice in the source is kept once, and a
|
|
343
|
+
* world group the source already carries (a preset copied from a generated
|
|
344
|
+
* variant) is replaced rather than duplicated, so the variant always mounts
|
|
345
|
+
* exactly one world pointing at this installation's providers.
|
|
346
|
+
*
|
|
347
|
+
* The source's own persistent-shell group is not re-added as such: it registers
|
|
348
|
+
* the same `bash` tool name as the WSL world's `dsh-tool-bash`, and the tools
|
|
349
|
+
* registry rejects duplicates within one preset layer — the whole variant fails
|
|
350
|
+
* to mount and the session falls back to another preset. Its PTY *backend*, on
|
|
351
|
+
* the other hand, is exactly what a stateful WSL shell needs, and it is
|
|
352
|
+
* config-driven: given the relay in {@link persistentShellRows} it runs
|
|
353
|
+
* `wsl.exe … bash` under the host's PTY, and its tool registers the separate
|
|
354
|
+
* `persistent-bash` name, so the one-shot `bash` above stays as it is.
|
|
239
355
|
* @param source - the source composition text.
|
|
240
356
|
* @param shellPath - absolute path of the plugin's built WSL shell provider.
|
|
241
357
|
* @param fsPath - absolute path of the plugin's built WSL fs provider.
|
|
358
|
+
* @param persistent - the relay, interpreter and sandbox provider a stateful
|
|
359
|
+
* shell needs; omit to generate a world without the persistent-shell rows.
|
|
360
|
+
* @param searchPath - absolute path of the plugin's built in-distribution
|
|
361
|
+
* `grep`/`glob` tools; mounted only for a source that had `tool-fs-search`.
|
|
362
|
+
* @param jobsPath - absolute path of the plugin's built background-job producer
|
|
363
|
+
* for the persistent shell; mounted only alongside that shell.
|
|
242
364
|
* @returns the variant composition text.
|
|
243
365
|
*/
|
|
244
|
-
function transformPresetForWsl(source, shellPath, fsPath) {
|
|
366
|
+
function transformPresetForWsl(source, shellPath, fsPath, persistent, searchPath, jobsPath) {
|
|
245
367
|
const lines = source.split("\n");
|
|
246
368
|
const spans = topLevelSpans(lines);
|
|
247
369
|
const kept = [];
|
|
370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
248
371
|
let sawEditor = false;
|
|
372
|
+
let sawSearch = false;
|
|
373
|
+
let sawJobs = false;
|
|
249
374
|
let personaAppended = false;
|
|
250
375
|
for (const span of spans) {
|
|
251
376
|
const id = spanId(lines, span);
|
|
@@ -253,9 +378,16 @@ function transformPresetForWsl(source, shellPath, fsPath) {
|
|
|
253
378
|
kept.push(...lines.slice(span.start, span.end));
|
|
254
379
|
continue;
|
|
255
380
|
}
|
|
256
|
-
if (
|
|
381
|
+
if (seen.has(id)) continue;
|
|
382
|
+
seen.add(id);
|
|
383
|
+
const block = lines.slice(span.start, span.end);
|
|
384
|
+
if (WORLD_ROWS.has(id) || isWslWorldGroup(block)) {
|
|
385
|
+
if (EDITOR_ROWS.has(id)) sawEditor = true;
|
|
386
|
+
if (id === "tool-fs-search") sawSearch = true;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
257
389
|
if (id === SKILL_FILESYSTEM_ROW) {
|
|
258
|
-
kept.push(...disableSkillWatch(
|
|
390
|
+
kept.push(...disableSkillWatch(block));
|
|
259
391
|
continue;
|
|
260
392
|
}
|
|
261
393
|
if (id === "persona" && !personaAppended && appendablePersona(lines, span)) {
|
|
@@ -263,13 +395,14 @@ function transformPresetForWsl(source, shellPath, fsPath) {
|
|
|
263
395
|
personaAppended = true;
|
|
264
396
|
continue;
|
|
265
397
|
}
|
|
266
|
-
kept.push(...
|
|
267
|
-
if (id === "str-replace-editor") sawEditor = true;
|
|
398
|
+
kept.push(...block);
|
|
268
399
|
}
|
|
269
400
|
if (source.includes("str-replace-editor")) sawEditor = true;
|
|
401
|
+
if (source.includes("tool-fs-search")) sawSearch = true;
|
|
402
|
+
if (source.includes("tool-jobs")) sawJobs = true;
|
|
270
403
|
const result = [...kept];
|
|
271
404
|
if (result.length > 0 && result[result.length - 1] !== "") result.push("");
|
|
272
|
-
result.push(wslWorldGroup(shellPath, fsPath, sawEditor));
|
|
405
|
+
result.push(wslWorldGroup(shellPath, fsPath, sawEditor, persistent, sawSearch ? searchPath : void 0, jobsPath, sawJobs));
|
|
273
406
|
return result.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
274
407
|
}
|
|
275
408
|
/** Whether an id is one of this plugin's own preset directories. */
|
|
@@ -292,12 +425,41 @@ const MAX_SCAN_DEPTH = 4;
|
|
|
292
425
|
const MAX_SKILL_ROOTS = 64;
|
|
293
426
|
/** Maximum directories visited per lookup (an absolute blast-radius cap). */
|
|
294
427
|
const MAX_VISITED_DIRECTORIES = 4096;
|
|
428
|
+
/**
|
|
429
|
+
* How many Linux symlinks one lookup may hand to the distribution (a second
|
|
430
|
+
* blast-radius cap, and a latency cap: each resolution is a short `wsl.exe`
|
|
431
|
+
* call, so a tree with hundreds of links cannot stall the catalog).
|
|
432
|
+
*/
|
|
433
|
+
const MAX_LINK_RESOLUTIONS = 32;
|
|
295
434
|
/** How many parent levels above the session cwd are searched for a `.git` project marker. */
|
|
296
435
|
const MAX_ANCESTOR_WALK = 64;
|
|
297
|
-
/**
|
|
298
|
-
|
|
436
|
+
/**
|
|
437
|
+
* How many directories one discovery layer may probe at once.
|
|
438
|
+
*
|
|
439
|
+
* A `readdir` over the `\\wsl.localhost\…` share measured 15.6 ms against 1.6 ms
|
|
440
|
+
* for a `stat`, and a budget-sized walk is 4096 directories: probing one
|
|
441
|
+
* directory at a time costs 20-30 s, which is what a turn in a large workspace
|
|
442
|
+
* used to wait for. The layer stays bounded so the share is never flooded; node's
|
|
443
|
+
* own filesystem thread pool is what ultimately caps the real parallelism.
|
|
444
|
+
*/
|
|
445
|
+
const WALK_CONCURRENCY = 16;
|
|
299
446
|
/** Maximum cached lookups (one entry per distinct scan root across sessions). */
|
|
300
447
|
const CACHE_MAX_ENTRIES = 32;
|
|
448
|
+
/**
|
|
449
|
+
* How often a served scan root is re-checked for catalog changes: the skills
|
|
450
|
+
* directories it published, plus one modification stamp per skill file. This is
|
|
451
|
+
* the pass that makes a skill added — or a description edited — mid-session
|
|
452
|
+
* visible on the model's next request.
|
|
453
|
+
*/
|
|
454
|
+
const REFRESH_POLL_MS = 3e3;
|
|
455
|
+
/**
|
|
456
|
+
* How often the full re-discovery walk runs for a served scan root. Only a walk
|
|
457
|
+
* can find a skills directory that did not exist before (a new nested project,
|
|
458
|
+
* say), and it costs one `readdir` per visited directory, so it runs on its own
|
|
459
|
+
* slower cadence instead of on every poll. It used to be the only pass, at
|
|
460
|
+
* {@link REFRESH_POLL_MS}'s old value of 10 s.
|
|
461
|
+
*/
|
|
462
|
+
const DISCOVERY_POLL_MS = 3e4;
|
|
301
463
|
/** Kebab-case skill names, matching the host grammar. */
|
|
302
464
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
303
465
|
/** Directory names that never contain project skill roots (safe to prune while walking). */
|
|
@@ -333,7 +495,8 @@ const PRUNED_DIRECTORY_NAMES = new Set([
|
|
|
333
495
|
const nodeSkillIo = {
|
|
334
496
|
readdir: async (path, options) => readdir(path, options),
|
|
335
497
|
readFile: async (path, options) => readFile(path, options),
|
|
336
|
-
stat: async (path) => stat(path)
|
|
498
|
+
stat: async (path) => stat(path),
|
|
499
|
+
resolveLinks: async (uncPaths) => resolveLinuxSymlinks(uncPaths)
|
|
337
500
|
};
|
|
338
501
|
/**
|
|
339
502
|
* Locate the nearest ancestor of `linuxDir` (the directory itself included)
|
|
@@ -359,7 +522,56 @@ async function nearestGitAncestor(distro, linuxDir, io) {
|
|
|
359
522
|
return void 0;
|
|
360
523
|
}
|
|
361
524
|
/**
|
|
525
|
+
* Everything the walk learns from one directory, in the order the round trips
|
|
526
|
+
* are worth paying for: the listing first — it also says which skill markers can
|
|
527
|
+
* exist there at all — then the markers it showed.
|
|
528
|
+
* @param distro - the WSL distribution name.
|
|
529
|
+
* @param dir - the directory's Linux path.
|
|
530
|
+
* @param depth - its depth below the scan root.
|
|
531
|
+
* @param io - filesystem face.
|
|
532
|
+
* @returns the directory's roots, child directories and unresolved links.
|
|
533
|
+
*/
|
|
534
|
+
async function probeDirectory(distro, dir, depth, io) {
|
|
535
|
+
let entries;
|
|
536
|
+
if (depth < MAX_SCAN_DEPTH) try {
|
|
537
|
+
entries = await io.readdir(joinUnc(distro, dir), { withFileTypes: true });
|
|
538
|
+
} catch {}
|
|
539
|
+
const listed = entries ?? [];
|
|
540
|
+
const allMarkers = PROJECT_SKILL_MARKERS.map(([marker]) => marker);
|
|
541
|
+
const markers = entries === void 0 ? allMarkers : allMarkers.filter((marker) => listed.some((entry) => entry.name === marker));
|
|
542
|
+
const roots = await skillRootsOfDirectory(distro, dir, io, markers);
|
|
543
|
+
const children = [];
|
|
544
|
+
const links = [];
|
|
545
|
+
for (const entry of listed) {
|
|
546
|
+
if (PRUNED_DIRECTORY_NAMES.has(entry.name)) continue;
|
|
547
|
+
if (entry.name.startsWith(".") && entry.name !== ".dsh" && entry.name !== ".agents") continue;
|
|
548
|
+
if (entry.name === ".dsh" || entry.name === ".agents") continue;
|
|
549
|
+
const childPath = posix.join(dir, entry.name);
|
|
550
|
+
if (entry.isDirectory()) {
|
|
551
|
+
children.push([childPath, depth + 1]);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!entry.isSymbolicLink()) continue;
|
|
555
|
+
try {
|
|
556
|
+
const target = await io.stat(joinUnc(distro, childPath));
|
|
557
|
+
if (target.isDirectory()) children.push([childPath, depth + 1]);
|
|
558
|
+
continue;
|
|
559
|
+
} catch {}
|
|
560
|
+
links.push([childPath, depth + 1]);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
roots,
|
|
564
|
+
children,
|
|
565
|
+
links
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
362
569
|
* Scan a WSL workspace root for nested skill directories.
|
|
570
|
+
*
|
|
571
|
+
* A layer is probed concurrently but published in frontier order, so the
|
|
572
|
+
* catalog never depends on which probe happened to finish first, and the budget
|
|
573
|
+
* is claimed before any probe starts: concurrency must not change what is
|
|
574
|
+
* visited, only how long it takes.
|
|
363
575
|
* @param distro - the WSL distribution name.
|
|
364
576
|
* @param linuxRoot - the workspace's absolute Linux path.
|
|
365
577
|
* @param io - filesystem face.
|
|
@@ -368,62 +580,77 @@ async function nearestGitAncestor(distro, linuxDir, io) {
|
|
|
368
580
|
async function discoverSkillRoots(distro, linuxRoot, io) {
|
|
369
581
|
const roots = [];
|
|
370
582
|
const visited = /* @__PURE__ */ new Set();
|
|
583
|
+
let linksResolved = 0;
|
|
371
584
|
let frontier = [[linuxRoot, 0]];
|
|
372
585
|
while (frontier.length > 0 && roots.length < MAX_SKILL_ROOTS) {
|
|
373
|
-
const
|
|
374
|
-
for (const
|
|
375
|
-
if (visited.size >= MAX_VISITED_DIRECTORIES)
|
|
376
|
-
if (visited.has(
|
|
377
|
-
visited.add(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
586
|
+
const layer = [];
|
|
587
|
+
for (const item of frontier) {
|
|
588
|
+
if (visited.size >= MAX_VISITED_DIRECTORIES) break;
|
|
589
|
+
if (visited.has(item[0])) continue;
|
|
590
|
+
visited.add(item[0]);
|
|
591
|
+
layer.push(item);
|
|
592
|
+
}
|
|
593
|
+
if (layer.length === 0) return roots;
|
|
594
|
+
const probes = new Array(layer.length);
|
|
595
|
+
let next = 0;
|
|
596
|
+
await Promise.all(Array.from({ length: Math.min(WALK_CONCURRENCY, layer.length) }, async () => {
|
|
597
|
+
for (let index = next; index < layer.length; index = next) {
|
|
598
|
+
next += 1;
|
|
599
|
+
const [dir, depth] = layer[index];
|
|
600
|
+
probes[index] = await probeDirectory(distro, dir, depth, io);
|
|
388
601
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
602
|
+
}));
|
|
603
|
+
const nextLayer = [];
|
|
604
|
+
const links = [];
|
|
605
|
+
for (const probe of probes) {
|
|
606
|
+
if (probe === void 0) continue;
|
|
607
|
+
if (roots.length < MAX_SKILL_ROOTS) roots.push(...probe.roots.slice(0, MAX_SKILL_ROOTS - roots.length));
|
|
608
|
+
nextLayer.push(...probe.children);
|
|
609
|
+
links.push(...probe.links);
|
|
610
|
+
}
|
|
611
|
+
if (visited.size >= MAX_VISITED_DIRECTORIES) return roots;
|
|
612
|
+
if (links.length > 0 && io.resolveLinks !== void 0 && linksResolved < MAX_LINK_RESOLUTIONS) {
|
|
613
|
+
const batch = links.slice(0, MAX_LINK_RESOLUTIONS - linksResolved);
|
|
614
|
+
linksResolved += batch.length;
|
|
615
|
+
const resolved = await io.resolveLinks(batch.map(([path]) => joinUnc(distro, path)));
|
|
616
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
617
|
+
const real = resolved[index];
|
|
618
|
+
if (real === void 0) continue;
|
|
619
|
+
try {
|
|
620
|
+
const info = await io.stat(real);
|
|
621
|
+
if (info.isDirectory()) nextLayer.push([uncToLinux(real), batch[index][1]]);
|
|
401
622
|
} catch {}
|
|
402
623
|
}
|
|
403
624
|
}
|
|
404
|
-
frontier =
|
|
625
|
+
frontier = nextLayer;
|
|
405
626
|
}
|
|
406
627
|
return roots;
|
|
407
628
|
}
|
|
629
|
+
/** The two project skill markers, with the source and rank each publishes. */
|
|
630
|
+
const PROJECT_SKILL_MARKERS = [[
|
|
631
|
+
".dsh",
|
|
632
|
+
"project-dsh",
|
|
633
|
+
PROJECT_DSH_RANK
|
|
634
|
+
], [
|
|
635
|
+
".agents",
|
|
636
|
+
"project-agents",
|
|
637
|
+
PROJECT_AGENTS_RANK
|
|
638
|
+
]];
|
|
408
639
|
/**
|
|
409
640
|
* Publish the skill roots of one scanned directory (its `.dsh/skills` and
|
|
410
641
|
* `.agents/skills`, each with the host's project ranks).
|
|
411
642
|
* @param distro - the WSL distribution name.
|
|
412
643
|
* @param linuxDir - the scanned directory's Linux path.
|
|
413
644
|
* @param io - filesystem face.
|
|
645
|
+
* @param markers - the markers worth probing. The walk narrows this to the ones
|
|
646
|
+
* its listing actually showed, because each probe is a round trip and a
|
|
647
|
+
* directory without a `.dsh` entry cannot hold `.dsh/skills`.
|
|
414
648
|
* @returns the directory's skill roots that exist.
|
|
415
649
|
*/
|
|
416
|
-
async function skillRootsOfDirectory(distro, linuxDir, io) {
|
|
650
|
+
async function skillRootsOfDirectory(distro, linuxDir, io, markers = PROJECT_SKILL_MARKERS.map(([marker]) => marker)) {
|
|
417
651
|
const result = [];
|
|
418
|
-
for (const [marker, source, rank] of
|
|
419
|
-
|
|
420
|
-
"project-dsh",
|
|
421
|
-
PROJECT_DSH_RANK
|
|
422
|
-
], [
|
|
423
|
-
".agents",
|
|
424
|
-
"project-agents",
|
|
425
|
-
PROJECT_AGENTS_RANK
|
|
426
|
-
]]) {
|
|
652
|
+
for (const [marker, source, rank] of PROJECT_SKILL_MARKERS) {
|
|
653
|
+
if (!markers.includes(marker)) continue;
|
|
427
654
|
const path = joinUnc(distro, posix.join(linuxDir, marker, "skills"));
|
|
428
655
|
try {
|
|
429
656
|
const info = await io.stat(path);
|
|
@@ -457,6 +684,32 @@ async function listSkillEntries(root, io) {
|
|
|
457
684
|
});
|
|
458
685
|
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
459
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* The published shape of one skills directory: its path, then each skill's name,
|
|
689
|
+
* kind and modification stamp.
|
|
690
|
+
*
|
|
691
|
+
* The stamp is what makes an edit to an *existing* skill visible: the catalog the
|
|
692
|
+
* model sees is rebuilt only when the registry's revision moves, and a directory
|
|
693
|
+
* listing alone cannot tell a rewritten `SKILL.md` from an untouched one. One
|
|
694
|
+
* `stat` per skill file (never a read) is the whole cost, and a file that cannot
|
|
695
|
+
* be stamped reports the same `gone` marker on every pass, so a substrate that
|
|
696
|
+
* does not expose modification times simply never triggers on content.
|
|
697
|
+
* @param root - the skills directory.
|
|
698
|
+
* @param entries - its entries, as just listed.
|
|
699
|
+
* @param io - filesystem face.
|
|
700
|
+
* @returns the deterministic shape string for this directory.
|
|
701
|
+
*/
|
|
702
|
+
async function shapeOfRoot(root, entries, io) {
|
|
703
|
+
const stamps = await Promise.all(entries.map(async (entry) => {
|
|
704
|
+
try {
|
|
705
|
+
const info = await io.stat(entry.path);
|
|
706
|
+
return `${entry.name}:${entry.kind}:${info.mtimeMs ?? 0}:${info.size ?? 0}`;
|
|
707
|
+
} catch {
|
|
708
|
+
return `${entry.name}:${entry.kind}:gone`;
|
|
709
|
+
}
|
|
710
|
+
}));
|
|
711
|
+
return `${root.path}\u0001${stamps.sort().join(",")}`;
|
|
712
|
+
}
|
|
460
713
|
/** Read and parse one skill file; `undefined` when missing or unparsable. */
|
|
461
714
|
async function readSkill(path, io, signal) {
|
|
462
715
|
signal?.throwIfAborted();
|
|
@@ -589,22 +842,37 @@ function frontmatterBoolean(fields, key, dflt = false) {
|
|
|
589
842
|
* The WSL workspace skill provider. Registered on the host's `ctx.skills`
|
|
590
843
|
* registry; serves only lookups whose cwd is a WSL UNC workspace path.
|
|
591
844
|
*
|
|
592
|
-
* Completed `list()` lookups are cached per scan root
|
|
593
|
-
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
845
|
+
* Completed `list()` lookups are cached per scan root and served as-is: a lookup
|
|
846
|
+
* is on the session's request path, and re-walking the tree there is what made
|
|
847
|
+
* every turn in a large workspace wait for a 4096-directory scan of the 9P
|
|
848
|
+
* share. `get()` always re-reads the skill file so body edits are picked up
|
|
849
|
+
* immediately.
|
|
850
|
+
*
|
|
851
|
+
* A scan root that has been served is re-checked every REFRESH_POLL_MS: the
|
|
852
|
+
* host cannot watch a `\\wsl.localhost\…` path (which is why the generated
|
|
853
|
+
* preset pins `watch: false`), so this provider watches for it instead. A
|
|
854
|
+
* changed directory listing clears the cache and calls `control.invalidate()`,
|
|
855
|
+
* which bumps the registry's revision; the catalog middleware re-collects on
|
|
856
|
+
* the session's next request, so a skill added mid-session reaches the model
|
|
857
|
+
* without starting a new session, and the cache is only ever refilled by a
|
|
858
|
+
* lookup the detector has already invalidated.
|
|
597
859
|
*/
|
|
598
860
|
var WslSkillsProvider = class {
|
|
599
861
|
name = "wsl-workspace";
|
|
600
862
|
control;
|
|
601
863
|
io;
|
|
602
|
-
|
|
864
|
+
refreshMs;
|
|
865
|
+
/** Cheap polls between two full discovery walks (at least one). */
|
|
866
|
+
walkEveryPolls;
|
|
867
|
+
/** The published catalog per scan root, served until its detector drops it. */
|
|
603
868
|
cache = /* @__PURE__ */ new Map();
|
|
604
|
-
|
|
869
|
+
/** One change detector per served scan root, keyed like {@link cache}. */
|
|
870
|
+
detectors = /* @__PURE__ */ new Map();
|
|
871
|
+
constructor(control, io = nodeSkillIo, refreshMs = REFRESH_POLL_MS, discoveryMs = DISCOVERY_POLL_MS) {
|
|
605
872
|
this.control = control;
|
|
606
873
|
this.io = io;
|
|
607
|
-
this.
|
|
874
|
+
this.refreshMs = refreshMs;
|
|
875
|
+
this.walkEveryPolls = Math.max(1, Math.round(discoveryMs / refreshMs));
|
|
608
876
|
}
|
|
609
877
|
/**
|
|
610
878
|
* Discover nested project skills for a WSL UNC session workspace.
|
|
@@ -621,16 +889,18 @@ var WslSkillsProvider = class {
|
|
|
621
889
|
const scanRoot = await nearestGitAncestor(unc.distro, unc.linuxPath, this.io) ?? unc.linuxPath;
|
|
622
890
|
const cacheKey = `${unc.distro}\u0000${scanRoot}`;
|
|
623
891
|
const cached = this.cache.get(cacheKey);
|
|
624
|
-
if (cached !== void 0
|
|
892
|
+
if (cached !== void 0) {
|
|
625
893
|
this.cache.delete(cacheKey);
|
|
626
894
|
this.cache.set(cacheKey, cached);
|
|
627
|
-
return [...cached
|
|
895
|
+
return [...cached];
|
|
628
896
|
}
|
|
629
897
|
const roots = await discoverSkillRoots(unc.distro, scanRoot, this.io);
|
|
630
898
|
const candidates = [];
|
|
631
899
|
const seenSkills = /* @__PURE__ */ new Set();
|
|
900
|
+
const signature = [];
|
|
632
901
|
for (const root of roots) {
|
|
633
902
|
const entries = await listSkillEntries(root, this.io);
|
|
903
|
+
signature.push(await shapeOfRoot(root, entries, this.io));
|
|
634
904
|
for (const entry of entries) {
|
|
635
905
|
options.signal?.throwIfAborted();
|
|
636
906
|
const parsed = await readSkill(entry.path, this.io, options.signal);
|
|
@@ -654,16 +924,83 @@ var WslSkillsProvider = class {
|
|
|
654
924
|
});
|
|
655
925
|
}
|
|
656
926
|
}
|
|
657
|
-
this.cache.set(cacheKey,
|
|
658
|
-
expiresAt: this.now() + CACHE_TTL_MS,
|
|
659
|
-
candidates
|
|
660
|
-
});
|
|
927
|
+
this.cache.set(cacheKey, candidates);
|
|
661
928
|
while (this.cache.size > CACHE_MAX_ENTRIES) {
|
|
662
929
|
const oldest = this.cache.keys().next().value;
|
|
663
930
|
if (oldest === void 0) break;
|
|
664
931
|
this.cache.delete(oldest);
|
|
665
932
|
}
|
|
666
|
-
|
|
933
|
+
this.watch(cacheKey, unc.distro, scanRoot, signature.join(""), roots);
|
|
934
|
+
return [...candidates];
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Keep one scan root's catalog honest for as long as this provider is
|
|
938
|
+
* registered: the host cannot watch a UNC workspace, so the provider polls the
|
|
939
|
+
* shape it just published and, on any difference, drops its own cache and asks
|
|
940
|
+
* the registry to re-collect for the session's next request.
|
|
941
|
+
*
|
|
942
|
+
* Two passes keep that affordable: the cheap one (every
|
|
943
|
+
* {@link REFRESH_POLL_MS}) re-reads the skills directories already published
|
|
944
|
+
* and re-stats their skill files, so an added, removed or edited skill is
|
|
945
|
+
* noticed on its own; the full re-discovery walk (every
|
|
946
|
+
* {@link DISCOVERY_POLL_MS}) is what can find a skills directory that did not
|
|
947
|
+
* exist before. Neither pass reads a skill file: the description the catalog
|
|
948
|
+
* shows changes only through the registry's revision.
|
|
949
|
+
* @param cacheKey - this provider's key for the scan root.
|
|
950
|
+
* @param distro - the WSL distribution.
|
|
951
|
+
* @param scanRoot - the Linux path the lookup scanned.
|
|
952
|
+
* @param signature - the shape the lookup just published.
|
|
953
|
+
* @param roots - the skills directories that lookup found.
|
|
954
|
+
*/
|
|
955
|
+
watch(cacheKey, distro, scanRoot, signature, roots) {
|
|
956
|
+
const existing = this.detectors.get(cacheKey);
|
|
957
|
+
if (existing !== void 0) {
|
|
958
|
+
existing.signature = signature;
|
|
959
|
+
existing.roots = roots;
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const detector = {
|
|
963
|
+
timer: setInterval(() => void this.detect(cacheKey, distro, scanRoot), this.refreshMs),
|
|
964
|
+
signature,
|
|
965
|
+
roots,
|
|
966
|
+
polls: 0,
|
|
967
|
+
busy: false
|
|
968
|
+
};
|
|
969
|
+
if (typeof detector.timer.unref === "function") detector.timer.unref();
|
|
970
|
+
this.detectors.set(cacheKey, detector);
|
|
971
|
+
this.control.signal.addEventListener("abort", () => {
|
|
972
|
+
clearInterval(detector.timer);
|
|
973
|
+
this.detectors.delete(cacheKey);
|
|
974
|
+
}, { once: true });
|
|
975
|
+
}
|
|
976
|
+
/** One poll: the cheap pass always, the discovery walk on its own cadence. */
|
|
977
|
+
async detect(cacheKey, distro, scanRoot) {
|
|
978
|
+
const detector = this.detectors.get(cacheKey);
|
|
979
|
+
if (detector === void 0 || this.control.signal.aborted) return;
|
|
980
|
+
if (detector.busy) return;
|
|
981
|
+
detector.busy = true;
|
|
982
|
+
try {
|
|
983
|
+
detector.polls += 1;
|
|
984
|
+
const walk = detector.polls % this.walkEveryPolls === 0;
|
|
985
|
+
let signature;
|
|
986
|
+
try {
|
|
987
|
+
signature = await this.shape(distro, walk ? await discoverSkillRoots(distro, scanRoot, this.io) : detector.roots);
|
|
988
|
+
} catch {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (signature === detector.signature) return;
|
|
992
|
+
detector.signature = signature;
|
|
993
|
+
this.cache.delete(cacheKey);
|
|
994
|
+
this.control.invalidate();
|
|
995
|
+
} finally {
|
|
996
|
+
detector.busy = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
/** The shape of one root set: paths, entry names, kinds and file stamps. */
|
|
1000
|
+
async shape(distro, roots) {
|
|
1001
|
+
const parts = [];
|
|
1002
|
+
for (const root of roots) parts.push(await shapeOfRoot(root, await listSkillEntries(root, this.io), this.io));
|
|
1003
|
+
return parts.join("");
|
|
667
1004
|
}
|
|
668
1005
|
/**
|
|
669
1006
|
* Load a complete skill body for a previously listed candidate.
|
|
@@ -972,8 +1309,76 @@ function publishVariant(staging, dest) {
|
|
|
972
1309
|
force: true
|
|
973
1310
|
});
|
|
974
1311
|
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Whether a host's terminal stack can allocate a PTY process *on this platform*.
|
|
1314
|
+
*
|
|
1315
|
+
* The world's `bash` is the host's persistent-shell stack, and on Windows that
|
|
1316
|
+
* stack needs a platform process inspector. `@deepseek-ai/dsh-subprocess-local`
|
|
1317
|
+
* only grew one in `0.1.0-rc.8`: in `0.1.0-rc.7` `spawnTerminal` throws
|
|
1318
|
+
* `subprocess-local: terminal inspection is unsupported on platform win32`
|
|
1319
|
+
* before the process is started, so *every* persistent shell fails there — a
|
|
1320
|
+
* real session shows the model getting that error for each `bash` call while
|
|
1321
|
+
* grep/glob (which never touch the PTY) keep working. The host itself ships the
|
|
1322
|
+
* same gap: that release's Minimal preset mounts `persistent-bash` with no
|
|
1323
|
+
* `disabled:` guard for Windows.
|
|
1324
|
+
*
|
|
1325
|
+
* The probe asks the substrate the question directly instead of pattern-matching
|
|
1326
|
+
* a version: `spawnTerminal` builds its inspector before `node-pty` starts the
|
|
1327
|
+
* program, so handing it a program that cannot exist reaches that check and
|
|
1328
|
+
* nothing else — no process is created either way, and the failure message says
|
|
1329
|
+
* which half failed. A release that can build the inspector reports an ordinary
|
|
1330
|
+
* spawn failure instead, which is the "supported" answer.
|
|
1331
|
+
* @param ctx - plugin context; the `subprocess` service is looked up with `get`
|
|
1332
|
+
* and waited for briefly, because the world is generated during profile boot.
|
|
1333
|
+
* @returns true when the persistent shell may be mounted.
|
|
1334
|
+
*/
|
|
1335
|
+
async function supportsPersistentShell(ctx) {
|
|
1336
|
+
if (process.platform !== "win32") return true;
|
|
1337
|
+
const subprocess = await waitForSubprocess(ctx);
|
|
1338
|
+
if (subprocess?.spawnTerminal === void 0) return true;
|
|
1339
|
+
try {
|
|
1340
|
+
const handle = await subprocess.spawnTerminal({
|
|
1341
|
+
argv: ["dsh-wsl-workspace-pty-probe-does-not-exist"],
|
|
1342
|
+
cwd: process.cwd(),
|
|
1343
|
+
rows: 24,
|
|
1344
|
+
cols: 80,
|
|
1345
|
+
graceMs: 1e3
|
|
1346
|
+
});
|
|
1347
|
+
await handle?.terminate?.();
|
|
1348
|
+
return true;
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
return !isTerminalInspectionUnsupported(error);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Whether one spawn failure is the missing-platform-inspector error.
|
|
1355
|
+
* @param error - the rejection from `spawnTerminal`.
|
|
1356
|
+
* @returns true when the host cannot inspect terminal processes here.
|
|
1357
|
+
*/
|
|
1358
|
+
function isTerminalInspectionUnsupported(error) {
|
|
1359
|
+
return /terminal inspection is unsupported on platform/i.test(messageOf(error));
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Look up the `subprocess` service, giving profile boot a moment to publish it.
|
|
1363
|
+
*
|
|
1364
|
+
* Bounded: the world is generated in a fire-and-forget effect, so this wait never
|
|
1365
|
+
* blocks profile boot, and the service is normally already published by the base
|
|
1366
|
+
* bundles the web app mounts before this plugin's own injection resolves. An
|
|
1367
|
+
* absent service is answered as "supported" by the caller, which is the
|
|
1368
|
+
* behaviour this plugin shipped before the probe existed.
|
|
1369
|
+
* @param ctx - plugin context.
|
|
1370
|
+
* @returns the service, or undefined when this deployment has none yet.
|
|
1371
|
+
*/
|
|
1372
|
+
async function waitForSubprocess(ctx) {
|
|
1373
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
1374
|
+
const service = ctx.get("subprocess");
|
|
1375
|
+
if (service !== void 0) return service;
|
|
1376
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1377
|
+
}
|
|
1378
|
+
return void 0;
|
|
1379
|
+
}
|
|
975
1380
|
/** Materialize one WSL variant per healthy source preset. */
|
|
976
|
-
async function materializeVariants(agentPresets, dshHome,
|
|
1381
|
+
async function materializeVariants(agentPresets, dshHome, paths, persistentShell) {
|
|
977
1382
|
const presets = await agentPresets.list();
|
|
978
1383
|
const userRoot = join(dshHome, ".agent-presets");
|
|
979
1384
|
const generated = /* @__PURE__ */ new Set();
|
|
@@ -982,7 +1387,11 @@ async function materializeVariants(agentPresets, dshHome, shellPath, fsPath) {
|
|
|
982
1387
|
if (isWslVariantId(preset.id)) continue;
|
|
983
1388
|
const variantId = variantIdFor(preset.id);
|
|
984
1389
|
const source = await agentPresets.read(preset.id);
|
|
985
|
-
const transformed = transformPresetForWsl(source,
|
|
1390
|
+
const transformed = transformPresetForWsl(source, paths.shell, paths.fs, persistentShell ? {
|
|
1391
|
+
relayPath: paths.relay,
|
|
1392
|
+
nodePath: paths.node,
|
|
1393
|
+
sandboxPath: paths.sandbox
|
|
1394
|
+
} : void 0, paths.search, paths.jobs);
|
|
986
1395
|
const dir = join(userRoot, variantId);
|
|
987
1396
|
const staging = `${dir}.staging`;
|
|
988
1397
|
rmSync(staging, {
|
|
@@ -1001,7 +1410,7 @@ async function materializeVariants(agentPresets, dshHome, shellPath, fsPath) {
|
|
|
1001
1410
|
const meta = readFileSync(join(dirname(preset.path), "preset.yml"), "utf8");
|
|
1002
1411
|
if (labels === void 0) {
|
|
1003
1412
|
const match = /^name:\s*(.+)$/m.exec(meta);
|
|
1004
|
-
if (match?.[1] !== void 0 && match[1].trim() !== "") name$1 = variantName(preset.id, match[1].trim());
|
|
1413
|
+
if (match?.[1] !== void 0 && match[1].trim() !== "") name$1 = variantName(preset.id, unquoteScalar(match[1].trim()));
|
|
1005
1414
|
}
|
|
1006
1415
|
const orderMatch = /^order:\s*(\d+)\s*$/m.exec(meta);
|
|
1007
1416
|
if (orderMatch?.[1] !== void 0) orderLine = `order: ${orderMatch[1]}\n`;
|
|
@@ -1047,9 +1456,25 @@ function apply(ctx, config) {
|
|
|
1047
1456
|
const packageRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
1048
1457
|
const shellPath = join(packageRoot, "lib", "shell.js").replace(/\\/g, "/");
|
|
1049
1458
|
const fsPath = join(packageRoot, "lib", "fs.js").replace(/\\/g, "/");
|
|
1459
|
+
const relayPath = join(packageRoot, "lib", "wsl-relay.js").replace(/\\/g, "/");
|
|
1460
|
+
const sandboxPath = join(packageRoot, "lib", "wsl-sandbox.js").replace(/\\/g, "/");
|
|
1461
|
+
const searchPath = join(packageRoot, "lib", "wsl-search.js").replace(/\\/g, "/");
|
|
1462
|
+
const jobsPath = join(packageRoot, "lib", "wsl-jobs.js").replace(/\\/g, "/");
|
|
1463
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
1050
1464
|
const agentPresets = ctx.get("agentPresets");
|
|
1051
1465
|
if (agentPresets !== void 0) ctx.effect(() => {
|
|
1052
|
-
|
|
1466
|
+
(async () => {
|
|
1467
|
+
const persistentShell = await supportsPersistentShell(ctx);
|
|
1468
|
+
await materializeVariants(agentPresets, dshHome, {
|
|
1469
|
+
shell: shellPath,
|
|
1470
|
+
fs: fsPath,
|
|
1471
|
+
relay: relayPath,
|
|
1472
|
+
node: nodePath,
|
|
1473
|
+
sandbox: sandboxPath,
|
|
1474
|
+
search: searchPath,
|
|
1475
|
+
jobs: jobsPath
|
|
1476
|
+
}, persistentShell);
|
|
1477
|
+
})().catch((error) => {
|
|
1053
1478
|
console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`);
|
|
1054
1479
|
});
|
|
1055
1480
|
return () => {};
|
|
@@ -1138,5 +1563,5 @@ function apply(ctx, config) {
|
|
|
1138
1563
|
}
|
|
1139
1564
|
|
|
1140
1565
|
//#endregion
|
|
1141
|
-
export { Config, DEFAULT_ROUTE, apply, inject, name };
|
|
1566
|
+
export { Config, DEFAULT_ROUTE, apply, inject, isTerminalInspectionUnsupported, name };
|
|
1142
1567
|
//# sourceMappingURL=index.js.map
|